├── .editorconfig ├── .github └── workflows │ └── package.yml ├── .gitignore ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LFE.KeyboardShortcuts.csproj ├── LICENSE.md ├── README.md ├── images ├── demo.gif ├── demo.mp4 └── logo.png ├── src ├── Commands │ ├── AnimationPatternCommand.cs │ ├── AnimationSpeedChange.cs │ ├── AtomAdd.cs │ ├── AtomCommandBase.cs │ ├── AtomDelete.cs │ ├── AtomDump.cs │ ├── AtomHiddenToggle.cs │ ├── AtomPositionChange.cs │ ├── AtomPositionSetLerp.cs │ ├── AtomRotationChange.cs │ ├── AtomSelect.cs │ ├── AtomSelectNext.cs │ ├── AtomSelectPrev.cs │ ├── AtomSelectTab.cs │ ├── CameraPositionChange.cs │ ├── CameraRotationChange.cs │ ├── Command.cs │ ├── CommandConst.cs │ ├── CommandExecuteEventArgs.cs │ ├── ControllerCommandBase.cs │ ├── ControllerPositionChange.cs │ ├── ControllerPositionSetLerp.cs │ ├── ControllerRotationChange.cs │ ├── ErrorLogToggle.cs │ ├── FreezeAnimationSet.cs │ ├── FreezeAnimationToggle.cs │ ├── HardReset.cs │ ├── MessageLogToggle.cs │ ├── MirrorReflectionsToggle.cs │ ├── MonitorFieldOfViewChange.cs │ ├── MouseRightClickDrag.cs │ ├── MouseWheelScroll.cs │ ├── MsaaChange.cs │ ├── PerformanceMonitorToggle.cs │ ├── PixelLightCountChange.cs │ ├── PlayEditModeSet.cs │ ├── PlayEditModeToggle.cs │ ├── PluginActionCall.cs │ ├── PluginAdd.cs │ ├── PluginBoolSet.cs │ ├── PluginBoolToggle.cs │ ├── PluginFloatChange.cs │ ├── PluginShowUI.cs │ ├── PluginStringChooserChange.cs │ ├── RescanPackages.cs │ ├── SceneLoad.cs │ ├── SceneNew.cs │ ├── SceneSave.cs │ ├── ScreenShotModeOn.cs │ ├── SoftBodyPhysicsToggle.cs │ ├── TimeScaleChange.cs │ ├── UIButtonTriggerCommand.cs │ └── WorldScaleChange.cs ├── Extensions │ ├── AtomExtensions.cs │ ├── EnumerableExtensions.cs │ ├── InputWrapper.cs │ ├── JSONStorableExtensions.cs │ ├── SuperControllerExtensions.cs │ ├── TransformExtensions.cs │ └── UITabSelectorExtensions.cs ├── KeyboardShortcuts.cslist ├── Main │ └── Plugin.cs ├── Models │ ├── BindingEvent.cs │ ├── CommandFactory.cs │ ├── KeyBinding.cs │ ├── KeyChord.cs │ ├── KeyRecorder.cs │ └── ViewModel.cs └── Utils │ ├── MathUtilities.cs │ └── TimingLogger.cs └── vam └── meta.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | # Styles 10 | 11 | dotnet_naming_style.pascal_case.capitalization = pascal_case 12 | 13 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 14 | dotnet_naming_style.prefix_underscore.required_prefix = _ 15 | 16 | # Const 17 | 18 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 19 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 20 | dotnet_naming_symbols.constant_fields.required_modifiers = const 21 | 22 | dotnet_naming_rule.const_pascal_case.symbols = constant_fields 23 | dotnet_naming_rule.const_pascal_case.capitalization = pascal_case 24 | dotnet_naming_rule.const_pascal_case.severity = suggestion 25 | 26 | # Private 27 | 28 | dotnet_naming_symbols.private_fields.applicable_kinds = field 29 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 30 | 31 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 32 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 33 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: KeyboardShortcutsPackage 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Get the version 16 | id: get_version 17 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 18 | - name: Zip the release package 19 | id: zip 20 | run: | 21 | mkdir -p publish/Custom/Scripts/LFE/KeyboardShortcuts 22 | cp -r src publish/Custom/Scripts/LFE/KeyboardShortcuts/ 23 | cp vam/meta.json publish/ 24 | sed -i 's/v0.0.0/${{ steps.get_version.outputs.VERSION }}/' publish/meta.json 25 | cd publish 26 | zip -r "LFE.KeyboardShortcuts.${{ github.run_number }}.var" * 27 | - name: GitHub release 28 | uses: softprops/action-gh-release@v1 29 | if: startsWith(github.ref, 'refs/tags/') 30 | with: 31 | draft: true 32 | files: publish/LFE.KeyboardShortcuts.${{ github.run_number }}.var 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | settings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_h.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *_wpftmp.csproj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # Visual Studio Trace Files 119 | *.e2e 120 | 121 | # TFS 2012 Local Workspace 122 | $tf/ 123 | 124 | # Guidance Automation Toolkit 125 | *.gpState 126 | 127 | # ReSharper is a .NET coding add-in 128 | _ReSharper*/ 129 | *.[Rr]e[Ss]harper 130 | *.DotSettings.user 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Coverlet is a free, cross platform Code Coverage Tool 143 | coverage*[.json, .xml, .info] 144 | 145 | # Visual Studio code coverage results 146 | *.coverage 147 | *.coveragexml 148 | 149 | # NCrunch 150 | _NCrunch_* 151 | .*crunch*.local.xml 152 | nCrunchTemp_* 153 | 154 | # MightyMoose 155 | *.mm.* 156 | AutoTest.Net/ 157 | 158 | # Web workbench (sass) 159 | .sass-cache/ 160 | 161 | # Installshield output folder 162 | [Ee]xpress/ 163 | 164 | # DocProject is a documentation generator add-in 165 | DocProject/buildhelp/ 166 | DocProject/Help/*.HxT 167 | DocProject/Help/*.HxC 168 | DocProject/Help/*.hhc 169 | DocProject/Help/*.hhk 170 | DocProject/Help/*.hhp 171 | DocProject/Help/Html2 172 | DocProject/Help/html 173 | 174 | # Click-Once directory 175 | publish/ 176 | 177 | # Publish Web Output 178 | *.[Pp]ublish.xml 179 | *.azurePubxml 180 | # Note: Comment the next line if you want to checkin your web deploy settings, 181 | # but database connection strings (with potential passwords) will be unencrypted 182 | *.pubxml 183 | *.publishproj 184 | 185 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 186 | # checkin your Azure Web App publish settings, but sensitive information contained 187 | # in these scripts will be unencrypted 188 | PublishScripts/ 189 | 190 | # NuGet Packages 191 | *.nupkg 192 | # NuGet Symbol Packages 193 | *.snupkg 194 | # The packages folder can be ignored because of Package Restore 195 | **/[Pp]ackages/* 196 | # except build/, which is used as an MSBuild target. 197 | !**/[Pp]ackages/build/ 198 | # Uncomment if necessary however generally it will be regenerated when needed 199 | #!**/[Pp]ackages/repositories.config 200 | # NuGet v3's project.json files produces more ignorable files 201 | *.nuget.props 202 | *.nuget.targets 203 | 204 | # Microsoft Azure Build Output 205 | csx/ 206 | *.build.csdef 207 | 208 | # Microsoft Azure Emulator 209 | ecf/ 210 | rcf/ 211 | 212 | # Windows Store app package directories and files 213 | AppPackages/ 214 | BundleArtifacts/ 215 | Package.StoreAssociation.xml 216 | _pkginfo.txt 217 | *.appx 218 | *.appxbundle 219 | *.appxupload 220 | 221 | # Visual Studio cache files 222 | # files ending in .cache can be ignored 223 | *.[Cc]ache 224 | # but keep track of directories ending in .cache 225 | !?*.[Cc]ache/ 226 | 227 | # Others 228 | ClientBin/ 229 | ~$* 230 | *~ 231 | *.dbmdl 232 | *.dbproj.schemaview 233 | *.jfm 234 | *.pfx 235 | *.publishsettings 236 | orleans.codegen.cs 237 | 238 | # Including strong name files can present a security risk 239 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 240 | #*.snk 241 | 242 | # Since there are multiple workflows, uncomment next line to ignore bower_components 243 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 244 | #bower_components/ 245 | 246 | # RIA/Silverlight projects 247 | Generated_Code/ 248 | 249 | # Backup & report files from converting an old project file 250 | # to a newer Visual Studio version. Backup files are not needed, 251 | # because we have git ;-) 252 | _UpgradeReport_Files/ 253 | Backup*/ 254 | UpgradeLog*.XML 255 | UpgradeLog*.htm 256 | ServiceFabricBackup/ 257 | *.rptproj.bak 258 | 259 | # SQL Server files 260 | *.mdf 261 | *.ldf 262 | *.ndf 263 | 264 | # Business Intelligence projects 265 | *.rdl.data 266 | *.bim.layout 267 | *.bim_*.settings 268 | *.rptproj.rsuser 269 | *- [Bb]ackup.rdl 270 | *- [Bb]ackup ([0-9]).rdl 271 | *- [Bb]ackup ([0-9][0-9]).rdl 272 | 273 | # Microsoft Fakes 274 | FakesAssemblies/ 275 | 276 | # GhostDoc plugin setting file 277 | *.GhostDoc.xml 278 | 279 | # Node.js Tools for Visual Studio 280 | .ntvs_analysis.dat 281 | node_modules/ 282 | 283 | # Visual Studio 6 build log 284 | *.plg 285 | 286 | # Visual Studio 6 workspace options file 287 | *.opt 288 | 289 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 290 | *.vbw 291 | 292 | # Visual Studio LightSwitch build output 293 | **/*.HTMLClient/GeneratedArtifacts 294 | **/*.DesktopClient/GeneratedArtifacts 295 | **/*.DesktopClient/ModelManifest.xml 296 | **/*.Server/GeneratedArtifacts 297 | **/*.Server/ModelManifest.xml 298 | _Pvt_Extensions 299 | 300 | # Paket dependency manager 301 | .paket/paket.exe 302 | paket-files/ 303 | 304 | # FAKE - F# Make 305 | .fake/ 306 | 307 | # CodeRush personal settings 308 | .cr/personal 309 | 310 | # Python Tools for Visual Studio (PTVS) 311 | __pycache__/ 312 | *.pyc 313 | 314 | # Cake - Uncomment if you are using it 315 | # tools/** 316 | # !tools/packages.config 317 | 318 | # Tabs Studio 319 | *.tss 320 | 321 | # Telerik's JustMock configuration file 322 | *.jmconfig 323 | 324 | # BizTalk build output 325 | *.btp.cs 326 | *.btm.cs 327 | *.odx.cs 328 | *.xsd.cs 329 | 330 | # OpenCover UI analysis results 331 | OpenCover/ 332 | 333 | # Azure Stream Analytics local run output 334 | ASALocalRun/ 335 | 336 | # MSBuild Binary and Structured Log 337 | *.binlog 338 | 339 | # NVidia Nsight GPU debugger configuration file 340 | *.nvuser 341 | 342 | # MFractors (Xamarin productivity tool) working folder 343 | .mfractor/ 344 | 345 | # Local History for Visual Studio 346 | .localhistory/ 347 | 348 | # BeatPulse healthcheck temp database 349 | healthchecksdb 350 | 351 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 352 | MigrationBackup/ 353 | 354 | # Ionide (cross platform F# VS Code tools) working folder 355 | .ionide/ -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 0.18 2020-07-30 2 | New: Move or Rotate the selected Free Controller for babul 3 | 4 | Version 0.17 2020-06-19 5 | New: Commands to control Animation patterns 6 | Play, Pause, Reset, and so on 7 | New: Command to run all triggers on a UIButton 8 | New: Command to dump basic atom information to messages 9 | 10 | Version 0.16 2020-05-26 11 | New: Commands to set or toggle Play/Edit Mode 12 | 13 | Version 0.15 2020-05-22 14 | New: Command to move camera with shortcuts 15 | New: Command to rotate camera with shortcuts 16 | New: For developers, allow commands to run either in 'Update' or 'FixedUpdate' 17 | 18 | Version 0.14 2020-05-13 19 | New: Command for Rescan Packages 20 | New: Command for Hard Reset 21 | Fix: Timescale change increment is now smaller 22 | Fix: Do not hang when running as a session plugin and loading another scene 23 | 24 | Version 0.13 2020-05-13 25 | Fix: VaM 1.19 support - Package in VAR format from here on out 26 | 27 | Version 0.12 2020-02-03 28 | New: Add Plugin to Atom action 29 | Fix: Reloading plugins or atoms in the scene no longer crash shortcuts 30 | Fix: Load Scene action now opens in edit mode 31 | 32 | Version 0.11 2020-01-31 33 | New: Actions can now target any free controller on an atom 34 | Change: Plugin actions now target specific plugin instances instead 35 | of a category of plugin. This gives more flexibility but 36 | sacrifices convenience. Thoughts towards convenience are 37 | being considered. 38 | New: Action to turn on screenshot mode 39 | Fix: Tab selection would sometimes fail. Now it fails less 40 | 41 | Version 0.10 2020-01-30 42 | New: Mirror Reflection Toggle Action 43 | New: Msaa Change Action 44 | New: Pixel Light Count Change Action 45 | New: Scene Load / Save Action 46 | New: Performance Monitor Toggle Action 47 | New: Add Atom Action 48 | New: Delete Selected Atom Action 49 | New: Plugin String Chooser Next / Previous Actions 50 | New: Plugin UI Show Action 51 | New: Specific Atom Show UI Tab Actions 52 | New: Specific Atom Select Action 53 | New: Recording text now guides usage of ESC key 54 | Fix: Action group slider now works 55 | 56 | Version 0.9 2020-01-27 57 | New: Support bindings in MacGruber postmagic plugin 58 | New: Support increasing/decreasing float params exposed in plugins 59 | 60 | Version 0.8 2020-01-27 61 | Fix: Overlapping / conflicting bindings that are shorter will be skipped if a longer 62 | one ran. For example, if SHIFT-CTRL-A and SHIFT-A are defined, then if SHIFT-CTRL-A 63 | triggered, then SHIFT-A will not also run. 64 | Fix: Don't trigger bindings if recording a new one 65 | Fix: Tab ui don't break if tab name isn't there for an atom type 66 | Refactor: One code file per command class 67 | 68 | Version 0.7 2020-01-24 69 | New: Gamepad Axis support 70 | New: Position actions for absolute position control (from 0 - 1) 71 | New: Position and rotation actions now respect timescale 72 | New: Action names are shorter in the UI 73 | Fix: Keybinding in config that are not in scene don't get deleted from config file 74 | Refactor: all command internals refactored - so might have made more bugs 75 | 76 | Version 0.6 2020-01-20 77 | New: Atom rotation and positional change actions 78 | New: Atom hide and delete actions 79 | New: Plugin registered boolean actions 80 | New: Plugin registered action actions 81 | Fix: Watch scene for changes and update actions that are listed 82 | 83 | Version 0.5 2020-01-18 84 | Fix: Stop listening to shortcuts if user is typing somewhere 85 | Fix: Saving/Loading settings now works properly if added as Session plugin 86 | Fix: Lots of other "added as session plugin" related breaking is fixed 87 | 88 | Version 0.4 2020-01-17 89 | New: Save settings 90 | New: Ability to filter shortcuts by category 91 | New: Toggle softbody physics action 92 | New: All UI panels for Person now listed as showable (thanks itsgus) 93 | New: Timescale and Animation speed actions (thanks itsgus) 94 | Fix: Hitting ESC in an empty binding field properly exits recording 95 | Fix: Actions show now for atom that plugin is attached to 96 | Fix: Shortcut that goes to tab now works if curently on main menu 97 | 98 | Add more UI Tab actions available 99 | Reorganize code so I don't have so much scrolling to do 100 | 101 | Version 0.3 2020-01-16 102 | Add more UI Tab actions available 103 | Reorganize code so I don't have so much scrolling to do 104 | 105 | Version 0.2 2020-01-15 106 | Example shortcuts to select a tab in the UI for an atom 107 | 108 | Version 0.1 2020-01-15 109 | Initial release 110 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you would like make contributions, please: 2 | - fork this repository 3 | - make your changes 4 | - issue a pull request 5 | -------------------------------------------------------------------------------- /LFE.KeyboardShortcuts.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {86C32DA8-DA52-47FB-AC78-B6BBFC7963F5} 8 | Library 9 | Properties 10 | LFE.KeyboardShortcuts 11 | LFE.KeyboardShortcuts 12 | v4.6.1 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\..\..\..\VaM_Data\Managed\Assembly-CSharp.dll 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ..\..\..\..\VaM_Data\Managed\UnityEngine.dll 48 | 49 | 50 | ..\..\..\..\VaM_Data\Managed\UnityEngine.AnimationModule.dll 51 | 52 | 53 | ..\..\..\..\VaM_Data\Managed\UnityEngine.AudioModule.dll 54 | 55 | 56 | ..\..\..\..\VaM_Data\Managed\UnityEngine.BaselibModule.dll 57 | 58 | 59 | ..\..\..\..\VaM_Data\Managed\UnityEngine.ClothModule.dll 60 | 61 | 62 | ..\..\..\..\VaM_Data\Managed\UnityEngine.CoreModule.dll 63 | 64 | 65 | ..\..\..\..\VaM_Data\Managed\UnityEngine.IMGUIModule.dll 66 | 67 | 68 | ..\..\..\..\VaM_Data\Managed\UnityEngine.InputModule.dll 69 | 70 | 71 | ..\..\..\..\VaM_Data\Managed\UnityEngine.Networking.dll 72 | 73 | 74 | False 75 | ..\..\..\..\VaM_Data\Managed\UnityEngine.PhysicsModule.dll 76 | 77 | 78 | ..\..\..\..\VaM_Data\Managed\UnityEngine.TextRenderingModule.dll 79 | 80 | 81 | ..\..\..\..\VaM_Data\Managed\UnityEngine.UI.dll 82 | 83 | 84 | ..\..\..\..\VaM_Data\Managed\UnityEngine.UIElementsModule.dll 85 | 86 | 87 | ..\..\..\..\VaM_Data\Managed\UnityEngine.UIModule.dll 88 | 89 | 90 | ..\..\..\..\VaM_Data\Managed\UnityEngine.VRModule.dll 91 | 92 | 93 | ..\..\..\..\VaM_Data\Managed\UnityEngine.XRModule.dll 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfe999/KeyboardShortcuts/411a818b6e56c1a81212f57cbd196c5189f1da66/LICENSE.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virt-A-Mate Keyboard Shortcuts 2 | 3 | Lets you bind keyboard / joystick bindings to many commands in Virt-A-Mate 4 | 5 | ## Installing 6 | 7 | Requires VaM 1.19 or newer. 8 | 9 | Download `LFE.KeyboardShortcuts.(version).var` from [Releases](https://github.com/lfe999/KeyboardShortcuts/releases) 10 | 11 | Save the `.var` file in the `(VAM_ROOT)\AddonPackages`. 12 | 13 | If you have VaM running already, click on the *Main UI > File (Open/Save) > Rescan Add-on Packages* button so that this plugin shows up. 14 | 15 | ## Demo 16 | 17 | ![](images/demo.gif?raw=true) 18 | 19 | ## Quickstart 20 | 21 | ### Add the plugin to your scene 22 | 23 | - Click `Add Plugin` on either an atom or the `Session Plugins` tag from within VaM. 24 | - Click `Select File...` and choose `LFE.KeyboardShortcuts.(version)` on the left. 25 | - Choose the `ADDME.cslist` file 26 | 27 | ### Configure your keyboard shortcuts 28 | 29 | Click `Open Custom UI` on the KeyboardShortcuts plugin. 30 | 31 | The dropdowns at the top will let you filter the keyboard shortcuts by category and subcategory. 32 | 33 | ### Recording a shortcut 34 | 35 | To record a new keyboard shortcut, find the command you would like to modify and click the grey button to the right of the description. 36 | 37 | Then press the key combination that you would like to use to trigger that command. 38 | 39 | Some joypad axis will work but it is limited by the setup of the Unity engine itself. If it works for you then horray! If a command has some sort of "Increase" or "Decrease", then this program tries its best to map one direction of an axis to the increase side and one to the decrease side based on the context. 40 | 41 | ### Clearing a shortcut 42 | 43 | Click the selected shortcut grey button. 44 | 45 | Hit the `ESC` key 46 | 47 | ### Saving your shortcuts 48 | 49 | Anytime you record or clear a shortcut, the settings are automatically saved in the `(VAM_ROOT)\Saves\lfe_keyboardshortcuts.json` file 50 | 51 | ### Resetting all shortcuts 52 | 53 | Delete the file `(VAM_ROOT)\Saves\lfe_keyboardshortcuts.json` in order to set all shortcuts to their default. 54 | 55 | ## Command List 56 | 57 | ### Global Actions 58 | 59 | These are commands that will likely apply no matter what atoms are in the scene. 60 | 61 | | Command | Notes | 62 | | ------- | ----------- | 63 | Animation Speed > Increase | 64 | Animation Speed > Decrease | 65 | Atom > Select Next | Select the next atom alphabetically even if it is hidden 66 | Atom > Select Prev | Select the previous atom alphabetically even if it is hidden 67 | Atom > Select Next Visible | Select the next visible atom alphabetically 68 | Atom > Select Prev Visible | Select the previous visible atom alphabetically 69 | Camera > Move > `Direction` | Move the main camera in the direction that you want 70 | Camera > Look > `Direction` | Rotater the main camera in the direction that you want 71 | Error Log > Toggle | 72 | Message Log > Toggle | 73 | Field of View > Increase | Increase the FOV by 10 74 | Field of View > Decrease | Decrease the FOV by 10 75 | Freeze Animation > Toggle | 76 | Freeze Animation > On | 77 | Freeze Animation > Off | 78 | Mirror Reflections > Toggle | 79 | MSAA Level > Increase | Choose the next highest MSAA level. Does nothing if already at the lowest. 80 | MSAA Level > Decrease | Choose the next lowest MSAA level. Does nothing if already at the lowest. 81 | Performance Monitor > Toggle | 82 | Pixel Light Count > Increase | 83 | Pixel Light Count > Decrease | 84 | Play/Edit > Set To Edit | Turn on Edit Mode 85 | Play/Edit > Set To Play | Turn on Play Mode 86 | Play/Edit > Toggle | Toggle Edit/Play Mode 87 | Rescan Add-on Packages | 88 | Hard Reset | 89 | Scene > New Scene | 90 | Scene > Open Scene | 91 | Scene > Save Scene | 92 | Screen Shot > Mode > Enable | 93 | Soft Body Physics > Toggle | 94 | Time Scale > Increase | Increase time scale by 0.10 - holding `SHIFT` makes this 0.5 95 | Time Scale > Decrease | Decrease time scale by 0.10 - holding `SHIFT` makes this 0.5 96 | World Scale > Increase | Increase world scale by 0.00025 - holding `SHIFT` makes this 0.001 97 | World Scale > Decrease | Decrease world scale by 0.00025 - holding `SHIFT` makes this 0.001 98 | Add > `Atom Type` > `Atom Name` | Add the atom of the given type to the scene 99 | 100 | ### Selected Controller Actions 101 | 102 | These are commands that will be run against the selected controller. They do nothing if no controller is selected. 103 | 104 | !!!!! Be careful of overlap between setting these and setting selected atom actions. 105 | 106 | Position > `[XYZ]` > Interpolate 0 - 1 | Set the `AXIS` of the atom to something between 0 and 1. Probably only useful when assigning a joypad axis. 107 | Position > `[XYZ]` > Increase / Decrease Small | Increase or decrease the given `AXIS` position by a maximum of 0.5 units per second 108 | Position > `[XYZ]` > Increase / Decrease Medium | Increase or decrease the given `AXIS` position by a maximum of 2.0 units per second 109 | Position > `[XYZ]` > Increase / Decrease Large | Increase or decrease the given `AXIS` position by a maximum of 5.0 units per second 110 | Rotation > `[XYZ]` > Increase / Decrease Small | Increase or decrease the given `AXIS` rotation by a maximum of 0.25 rotations per second 111 | Rotation > `[XYZ]` > Increase / Decrease Medium | Increase or decrease the given `AXIS` rotation by a maximum of 0.5 rotations per second 112 | Rotation > `[XYZ]` > Increase / Decrease Large | Increase or decrease the given `AXIS` rotation by a maximum of 2.0 rotations per second 113 | 114 | ### Selected Atom Actions 115 | 116 | These are commands that will be run against a selected atom. They do nothing if no atom is selected. 117 | 118 | !!!!! Be careful of overlap between setting these and setting selected controller actions. 119 | 120 | | Command | Notes | 121 | | ------- | ----------- | 122 | Delete | Delete the selected atom 123 | Hide > Toggle | Toggle the atom hidden or shown 124 | Show UI > `Tab Name` | Show the selected `Tab Name`. If that tab name does not exist on the selected atom, this does nothing. 125 | Position > `[XYZ]` > Interpolate 0 - 1 | Set the `AXIS` of the atom to something between 0 and 1. Probably only useful when assigning a joypad axis. 126 | Position > `[XYZ]` > Increase / Decrease Small | Increase or decrease the given `AXIS` position by a maximum of 0.5 units per second 127 | Position > `[XYZ]` > Increase / Decrease Medium | Increase or decrease the given `AXIS` position by a maximum of 2.0 units per second 128 | Position > `[XYZ]` > Increase / Decrease Large | Increase or decrease the given `AXIS` position by a maximum of 5.0 units per second 129 | Rotation > `[XYZ]` > Increase / Decrease Small | Increase or decrease the given `AXIS` rotation by a maximum of 0.25 rotations per second 130 | Rotation > `[XYZ]` > Increase / Decrease Medium | Increase or decrease the given `AXIS` rotation by a maximum of 0.5 rotations per second 131 | Rotation > `[XYZ]` > Increase / Decrease Large | Increase or decrease the given `AXIS` rotation by a maximum of 2.0 rotations per second 132 | 133 | ### Specific Atom Actions 134 | 135 | These are commands that will be run against the atom with the selected `ID`, EVEN IF IT IS NOT SELECTED. 136 | 137 | This means that a command to `ShowUI > Plugins` will first, select the atom, and then run your selected command. 138 | 139 | Additionally, commands to target specific **free controllers** and **plugins** can be 140 | 141 | | Command | Notes | 142 | | ------- | ----------- | 143 | see "Selected Atom Commands" | 144 | 145 | ### Targeting Plugin Commands 146 | 147 | Any `Params` that a plugin exposes can be targeted. 148 | 149 | In order to trigger a plugin `Params` with a keybinding: 150 | - select the specific atom that has the plugin installed in the left hand dropdown 151 | - select the specific plugin in the right hand dropdown 152 | - set keybinding for the dynamically generated commands available 153 | 154 | | Commands | Notes | 155 | | ------- | ----------- | 156 | `Plugin` > Show UI | open the plugins configuration UI 157 | `Boolean Param` > Toggle | Toggle the `Boolean Param` on or off for the plugin 158 | `Boolean Param` > On | Set the `Boolean Param` On for the plugin 159 | `Boolean Param` > Off | Set the `Boolean Param` Off for the plugin 160 | `Action Param` > Call | Trigger the custom `Action Param` for the plugin 161 | `Float Param` > +0.01 | Increase the float param by a maximum of 0.01 (if joypad axis is bound, increased/decreased amount is interpolated) 162 | `Float Param` > -0.01 | 163 | `Float Param` > +0.10 | 164 | `Float Param` > -0.10 | 165 | `Float Param` > +1.00 | 166 | `Float Param` > -1.00 | 167 | `String Chooser Param` > Next | Select the next item in for the given `String Chooser Param` of the plugin 168 | `String Chooser Param` > Prev | Select the previous item in for the given `String Chooser Param` of the plugin 169 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfe999/KeyboardShortcuts/411a818b6e56c1a81212f57cbd196c5189f1da66/images/demo.gif -------------------------------------------------------------------------------- /images/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfe999/KeyboardShortcuts/411a818b6e56c1a81212f57cbd196c5189f1da66/images/demo.mp4 -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lfe999/KeyboardShortcuts/411a818b6e56c1a81212f57cbd196c5189f1da66/images/logo.png -------------------------------------------------------------------------------- /src/Commands/AnimationPatternCommand.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class AnimationPatternCommand : AtomCommandBase 7 | { 8 | public const int NOTHING = 0; 9 | public const int PLAY = 1; 10 | public const int PAUSE = 2; 11 | public const int UNPAUSE = 3; 12 | public const int TOGGLE_PAUSE = 4; 13 | public const int RESET_AND_PLAY = 5; 14 | public const int RESET = 6; 15 | 16 | 17 | private int _action; 18 | 19 | public AnimationPatternCommand() : this((Func)null, NOTHING) { } 20 | public AnimationPatternCommand(Atom atom, int action) : this((a) => a.uid.Equals(atom.uid), action) { } 21 | public AnimationPatternCommand(Func predicate, int action) : base(predicate) { 22 | _action = action; 23 | } 24 | 25 | public override bool Execute(CommandExecuteEventArgs args) 26 | { 27 | var selected = TargetAtom(args); 28 | if (selected != null) { 29 | var pattern = selected.GetComponentInChildren(); 30 | if(pattern != null) { 31 | switch(_action) { 32 | case PLAY: 33 | pattern.Play(); 34 | break; 35 | case RESET_AND_PLAY: 36 | pattern.ResetAndPlay(); 37 | break; 38 | case PAUSE: 39 | pattern.Pause(); 40 | break; 41 | case UNPAUSE: 42 | pattern.UnPause(); 43 | break; 44 | case TOGGLE_PAUSE: 45 | pattern.TogglePause(); 46 | break; 47 | case RESET: 48 | pattern.ResetAnimation(); 49 | break; 50 | } 51 | } 52 | } 53 | return true; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/AnimationSpeedChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using UnityEngine; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class AnimationSpeedChange : Command 7 | { 8 | private const float _multiplier = 1.0f; 9 | private const float _min = -3.0f; 10 | private const float _max = 5.0f; 11 | 12 | private float _amount; 13 | 14 | public AnimationSpeedChange(float amount) 15 | { 16 | _amount = amount; 17 | } 18 | 19 | public override bool Execute(CommandExecuteEventArgs args) 20 | { 21 | if (args.KeyBinding.KeyChord.HasAxis) 22 | { 23 | // make sure the keybinding and value change are going the 24 | // same "direction" (for the case where the axis is involved) 25 | if (!MathUtilities.SameSign(args.Data, _amount)) { return false; } 26 | } 27 | 28 | SuperController sc = SuperController.singleton; 29 | var multiplier = _multiplier * Mathf.Abs(args.Data); 30 | 31 | if (InputWrapper.GetKey(KeyCode.LeftShift) || InputWrapper.GetKey(KeyCode.RightShift)) 32 | { 33 | multiplier *= 5.0f; 34 | } 35 | 36 | var scale = sc.motionAnimationMaster.playbackSpeed + (_amount * multiplier); 37 | sc.motionAnimationMaster.playbackSpeed = Mathf.Clamp(scale, _min, _max); 38 | return true; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Commands/AtomAdd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class AtomAdd : Command 7 | { 8 | private string _type; 9 | public AtomAdd(string type) 10 | { 11 | _type = type; 12 | } 13 | 14 | public override bool Execute(CommandExecuteEventArgs args) 15 | { 16 | SuperController.singleton.StartCoroutine(SuperController.singleton.AddAtomByType(_type, useuid: null, userInvoked: false)); 17 | return true; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Commands/AtomCommandBase.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public abstract class AtomCommandBase : Command { 9 | protected Func _predicate; 10 | protected AtomCommandBase(Func predicate) 11 | { 12 | _predicate = predicate; 13 | } 14 | 15 | public virtual Atom TargetAtom(CommandExecuteEventArgs args) 16 | { 17 | if(_predicate == null) 18 | { 19 | return SuperController.singleton.GetSelectedAtom(); 20 | } 21 | else 22 | { 23 | return SelectableAtoms().Where(_predicate).FirstOrDefault(); 24 | } 25 | } 26 | 27 | protected IEnumerable SelectableAtoms() 28 | { 29 | return SuperController.singleton.GetSelectableAtoms().OrderBy((a) => a.uid); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/AtomDelete.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class AtomDelete : AtomCommandBase 7 | { 8 | public AtomDelete() : this(null) { } 9 | public AtomDelete(Func predicate) : base(predicate) { } 10 | public override bool Execute(CommandExecuteEventArgs args) 11 | { 12 | var selected = TargetAtom(args); 13 | if (selected != null) { 14 | SuperController.singleton.StartCoroutine(DeleteAtom(selected)); 15 | } 16 | return true; 17 | } 18 | 19 | private IEnumerator DeleteAtom(Atom atom) 20 | { 21 | SuperController.singleton.RemoveAtom(atom); 22 | yield return null; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Commands/AtomDump.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using LFE.KeyboardShortcuts.Extensions; 4 | 5 | namespace LFE.KeyboardShortcuts.Commands 6 | { 7 | public class AtomDump : AtomCommandBase 8 | { 9 | public AtomDump() : this((Func)null) { } 10 | public AtomDump(Atom atom) : this((a) => a.uid.Equals(atom.uid)) { } 11 | public AtomDump(Func predicate) : base(predicate) { } 12 | 13 | // TODO: consider integrating with acidbubbles debug plugin 14 | public override bool Execute(CommandExecuteEventArgs args) 15 | { 16 | var selected = TargetAtom(args); 17 | if (selected != null) { 18 | DumpObject(selected); 19 | } 20 | return true; 21 | } 22 | 23 | private void DumpObject(object thing) { 24 | if(thing is Atom) { 25 | DumpAtom(thing as Atom); 26 | 27 | } 28 | else { 29 | Log($"{thing}"); 30 | } 31 | } 32 | 33 | private void DumpAtom(Atom atom) { 34 | Log($"atom.name = \"{atom.name}\""); 35 | Log($"atom.uid = \"{atom.uid}\""); 36 | Log($"atom.type = \"{atom.type}\""); 37 | 38 | var paramNames = string.Join(", ", atom.GetAllParamAndActionNames().ToArray()); 39 | Log($"atom.params = [{paramNames}]"); 40 | 41 | Log("atom.components = ["); 42 | foreach(var c in atom.GetComponentsInChildren()) { 43 | Log($" {c}"); 44 | } 45 | Log("]"); 46 | 47 | } 48 | 49 | private void Log(string message) { 50 | SuperController.LogMessage(message); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Commands/AtomHiddenToggle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class AtomHiddenToggle : AtomCommandBase 6 | { 7 | public AtomHiddenToggle() : this((Func)null) { } 8 | public AtomHiddenToggle(Atom atom) : this((a) => a.uid.Equals(atom.uid)) { } 9 | public AtomHiddenToggle(Func predicate) : base(predicate) { } 10 | 11 | public override bool Execute(CommandExecuteEventArgs args) 12 | { 13 | var target = TargetAtom(args); 14 | if (target != null) { target.hidden = !target.hidden; } 15 | return true; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/AtomPositionChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class AtomPositionChange : AtomCommandBase 9 | { 10 | private Axis _axis; 11 | private float _unitsPerSecond; 12 | private string _atomUid; 13 | private string _controllerName; 14 | public AtomPositionChange(Axis axis, float unitPerSecond) : this(axis, unitPerSecond, (FreeControllerV3)null) { } 15 | public AtomPositionChange(Axis axis, float unitPerSecond, Atom atom) : this(axis, unitPerSecond, atom.mainController) { } 16 | public AtomPositionChange(Axis axis, float unitPerSecond, FreeControllerV3 controller) : base(null) 17 | { 18 | _axis = axis; 19 | _unitsPerSecond = unitPerSecond; 20 | if(controller != null) 21 | { 22 | _atomUid = controller.containingAtom.uid; 23 | _controllerName = controller.name; 24 | } 25 | } 26 | 27 | public override bool Execute(CommandExecuteEventArgs args) 28 | { 29 | if (args.KeyBinding.KeyChord.HasAxis) 30 | { 31 | // make sure the keybinding and value change are going the 32 | // same "direction" (for the case where the axis is involved) 33 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 34 | } 35 | 36 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetAtom(args)?.mainController; 37 | if (controller != null) 38 | { 39 | var direction = Vector3.right; 40 | if(_axis == Axis.X) { direction = Vector3.right; } 41 | else if (_axis == Axis.Y) { direction = Vector3.up; } 42 | else if (_axis == Axis.Z) { direction = Vector3.forward; } 43 | 44 | controller.transform.Translate(direction * Time.deltaTime * _unitsPerSecond * Mathf.Abs(args.Data)); 45 | } 46 | return true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/AtomPositionSetLerp.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class AtomPositionSetLerp : AtomCommandBase 9 | { 10 | private Axis _axis; 11 | private float _min; 12 | private float _max; 13 | private string _atomUid; 14 | private string _controllerName; 15 | 16 | public AtomPositionSetLerp(Axis axis, float absolutePositionMin, float absolutePositionMax) : this(axis, absolutePositionMin, absolutePositionMax, (FreeControllerV3)null) { } 17 | public AtomPositionSetLerp(Axis axis, float absolutePositionMin, float absolutePositionMax, Atom atom) : this(axis, absolutePositionMin, absolutePositionMax, atom.mainController) { } 18 | public AtomPositionSetLerp(Axis axis, float absolutePositionMin, float absolutePositionMax, FreeControllerV3 controller) : base(null) 19 | { 20 | _axis = axis; 21 | _min = absolutePositionMin; 22 | _max = absolutePositionMax; 23 | if(controller != null) 24 | { 25 | _atomUid = controller.containingAtom.uid; 26 | _controllerName = controller.name; 27 | } 28 | } 29 | 30 | public override bool Execute(CommandExecuteEventArgs args) 31 | { 32 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetAtom(args)?.mainController; 33 | if (controller != null) 34 | { 35 | float proportion = Mathf.Lerp(0, 1, Mathf.Abs(args.Data)); 36 | var transform = controller.transform; 37 | var newPosition = new Vector3(transform.position.x, transform.position.y, transform.position.z); 38 | var newValue = Mathf.Lerp(_min, _max, proportion); 39 | if (_axis == Axis.X) { newPosition.x = newValue; } 40 | else if (_axis == Axis.Y) { newPosition.y = newValue; } 41 | else if (_axis == Axis.Z) { newPosition.z = newValue; } 42 | 43 | transform.position = newPosition; 44 | } 45 | return true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Commands/AtomRotationChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class AtomRotationChange : AtomCommandBase 9 | { 10 | private Axis _axis; 11 | private float _rotationsPerSecond; 12 | private string _atomUid; 13 | private string _controllerName; 14 | public AtomRotationChange(Axis axis, float rotationsPerSecond) : this(axis, rotationsPerSecond, (FreeControllerV3)null) { } 15 | public AtomRotationChange(Axis axis, float rotationsPerSecond, Atom atom) : this(axis, rotationsPerSecond, atom.mainController) { } 16 | public AtomRotationChange(Axis axis, float rotationsPerSecond, FreeControllerV3 controller) : base(null) 17 | { 18 | _axis = axis; 19 | _rotationsPerSecond = rotationsPerSecond; 20 | if(controller != null) 21 | { 22 | _atomUid = controller.containingAtom.uid; 23 | _controllerName = controller.name; 24 | } 25 | } 26 | 27 | public override bool Execute(CommandExecuteEventArgs args) 28 | { 29 | if (args.KeyBinding.KeyChord.HasAxis) 30 | { 31 | // make sure the keybinding and value change are going the 32 | // same "direction" (for the case where the axis is involved) 33 | if (!MathUtilities.SameSign(args.Data, _rotationsPerSecond)) { return false; } 34 | } 35 | 36 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetAtom(args)?.mainController; 37 | if (controller != null) 38 | { 39 | var rotate = 360 * Time.deltaTime * _rotationsPerSecond * Mathf.Abs(args.Data); 40 | var target = controller.transform; 41 | 42 | if(_axis == Axis.X) { target.Rotate(rotate, 0, 0); } 43 | else if(_axis == Axis.Y) { target.Rotate(0, rotate, 0); } 44 | else if(_axis == Axis.Z) { target.Rotate(0, 0, rotate); } 45 | } 46 | return true; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Commands/AtomSelect.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class AtomSelect : AtomCommandBase 9 | { 10 | private string _atomUid; 11 | private string _controllerName; 12 | 13 | public AtomSelect(FreeControllerV3 controller) : this((a) => a.uid.Equals(controller?.containingAtom?.uid)) { 14 | if(controller != null) 15 | { 16 | _atomUid = controller.containingAtom.uid; 17 | _controllerName = controller.name; 18 | } 19 | } 20 | public AtomSelect(Atom atom) : this((FreeControllerV3)atom?.mainController) { } 21 | public AtomSelect(Func predicate) : base(predicate) { } 22 | 23 | public override bool Execute(CommandExecuteEventArgs args) 24 | { 25 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetAtom(args)?.mainController; 26 | SuperController.singleton.SelectController(controller); 27 | return true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Commands/AtomSelectNext.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Commands 6 | { 7 | public class AtomSelectNext : AtomSelect 8 | { 9 | public AtomSelectNext() : base((x) => true) { } 10 | public AtomSelectNext(Func predicate) : base(predicate) { } 11 | 12 | public override Atom TargetAtom(CommandExecuteEventArgs args) 13 | { 14 | if (args.KeyBinding.KeyChord.HasAxis) 15 | { 16 | // make sure the keybinding and value change are going the 17 | // same "direction" (for the case where the axis is involved) 18 | if (!MathUtilities.SameSign(args.Data, 1)) { return null; } 19 | } 20 | 21 | var current = SuperController.singleton.GetSelectedAtom(); 22 | return SelectableAtoms() 23 | .LoopOnceStartingWhen((a) => a.uid.Equals(current?.uid)) 24 | .First(_predicate) ?? current; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Commands/AtomSelectPrev.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Commands 6 | { 7 | public class AtomSelectPrev : AtomSelect 8 | { 9 | public AtomSelectPrev() : base((x) => true) { } 10 | public AtomSelectPrev(Func predicate) : base(predicate) { } 11 | 12 | public override Atom TargetAtom(CommandExecuteEventArgs args) 13 | { 14 | if (args.KeyBinding.KeyChord.HasAxis) 15 | { 16 | // make sure the keybinding and value change are going the 17 | // same "direction" (for the case where the axis is involved) 18 | if (!MathUtilities.SameSign(args.Data, -1)) { return null; } 19 | } 20 | 21 | var current = SuperController.singleton.GetSelectedAtom(); 22 | return SelectableAtoms() 23 | .Reverse() 24 | .LoopOnceStartingWhen((a) => a.uid.Equals(current?.uid)) 25 | .First(_predicate) ?? current; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/AtomSelectTab.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace LFE.KeyboardShortcuts.Commands 8 | { 9 | public class AtomSelectTab : AtomCommandBase 10 | { 11 | private string _tabName; 12 | private UnityEngine.UI.Button _showSelectedButton; 13 | public AtomSelectTab(string tabName) : this(tabName, (Func)null) { } 14 | public AtomSelectTab(string tabName, Atom atom) : this(tabName, (a) => a.uid.Equals(atom.uid)) { } 15 | public AtomSelectTab(string tabName, Func predicate) : base(predicate) 16 | { 17 | _tabName = tabName; 18 | var mainTabBar = SuperController.singleton.mainMenuUI.parent; 19 | _showSelectedButton = mainTabBar.Find((name) => name.EndsWith("/ButtonSelectedOptions")) 20 | .FirstOrDefault() 21 | ?.GetComponent(); 22 | } 23 | 24 | public override bool Execute(CommandExecuteEventArgs args) 25 | { 26 | var target = TargetAtom(args); 27 | // there is some polling that is done with a timeout 28 | // so do this work async without blocking any run loop 29 | SuperController.singleton.StartCoroutine(DoWorkCoroutine(target)); 30 | return true; 31 | } 32 | 33 | private IEnumerator DoWorkCoroutine(Atom target) 34 | { 35 | if (target == null) { 36 | SuperController.LogMessage($"nothing selected", false); 37 | yield return false; 38 | } 39 | 40 | // main controller has to be selected for the ui tab stuff to work 41 | if(!target.mainController.selected) 42 | { 43 | SuperController.singleton.SelectController(target.mainController); 44 | // there is a race condition where the selected controller doesn't 45 | // happen fast enough before getting the tab selector.. so poll for 46 | // no more than 0.5 absolute seconds 47 | var waitUntil = Time.fixedUnscaledTime + 0.5f; 48 | while(!target.mainController.selected && Time.fixedUnscaledTime < waitUntil) 49 | { 50 | yield return new WaitForSecondsRealtime(0.02f); 51 | } 52 | } 53 | 54 | var ui = target.GetTabSelector(); 55 | if (ui == null) 56 | { 57 | yield return false; 58 | } 59 | 60 | if (!ui.HasTabName(_tabName)) 61 | { 62 | SuperController.LogMessage($"{target}: no tab name {_tabName}", false); 63 | yield return false; 64 | } 65 | 66 | // set the active tab 67 | if (target.mainController != null) 68 | { 69 | SuperController.singleton.SelectController(target.mainController); 70 | } 71 | 72 | // we might be showing the main menu (for example user prefs) even if person is selected 73 | if(_showSelectedButton != null) 74 | { 75 | _showSelectedButton?.onClick?.Invoke(); 76 | } 77 | 78 | ui.SetActiveTab(_tabName); 79 | yield return true; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Commands/CameraPositionChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class CameraPositionChange : Command 9 | { 10 | private Vector3 _direction; 11 | private float _unitsPerSecond; 12 | private Camera _windowCamera; 13 | public CameraPositionChange(Axis axis, float unitPerSecond) 14 | { 15 | RunPhase = CommandConst.RUNPHASE_FIXED_UPDATE; 16 | RepeatSpeed = 0; 17 | RepeatDelay = 0; 18 | _direction = Vector3.right; 19 | if(axis == Axis.X) { _direction = Vector3.right; } 20 | else if (axis == Axis.Y) { _direction = Vector3.up; } 21 | else if (axis == Axis.Z) { _direction = Vector3.forward; } 22 | _unitsPerSecond = unitPerSecond; 23 | _windowCamera = GetWindowCamera(); 24 | } 25 | 26 | public override bool Execute(CommandExecuteEventArgs args) 27 | { 28 | if (args.KeyBinding.KeyChord.HasAxis) 29 | { 30 | // make sure the keybinding and value change are going the 31 | // same "direction" (for the case where the axis is involved) 32 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 33 | 34 | if(_direction == Vector3.forward) { 35 | // invert the direction for forward/back if using an axis 36 | _direction = Vector3.back; 37 | } 38 | } 39 | 40 | if (_windowCamera != null) 41 | { 42 | var multiplier = 1.0f; 43 | if (InputWrapper.GetKey(KeyCode.LeftShift) || InputWrapper.GetKey(KeyCode.RightShift) || args.KeyBinding.KeyChord.HasAxis) 44 | { 45 | multiplier *= 3.0f; 46 | } 47 | _windowCamera.transform.Translate(_direction * Time.deltaTime * _unitsPerSecond * multiplier * Mathf.Abs(args.Data)); 48 | } 49 | return true; 50 | } 51 | 52 | private Camera GetWindowCamera() { 53 | return CameraTarget.centerTarget.targetCamera; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Commands/CameraRotationChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class CameraRotationChange : Command 9 | { 10 | private Axis _axis; 11 | private float _unitsPerSecond; 12 | private Camera _windowCamera; 13 | public CameraRotationChange(Axis axis, float unitPerSecond) 14 | { 15 | RunPhase = CommandConst.RUNPHASE_FIXED_UPDATE; 16 | RepeatSpeed = 0; 17 | RepeatDelay = 0; 18 | _axis = axis; 19 | _unitsPerSecond = unitPerSecond; 20 | _windowCamera = GetWindowCamera(); 21 | } 22 | 23 | public override bool Execute(CommandExecuteEventArgs args) 24 | { 25 | if (args.KeyBinding.KeyChord.HasAxis) 26 | { 27 | // make sure the keybinding and value change are going the 28 | // same "direction" (for the case where the axis is involved) 29 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 30 | } 31 | 32 | if (_windowCamera != null) 33 | { 34 | var rotate = 360 * Time.deltaTime * _unitsPerSecond * Mathf.Abs(args.Data); 35 | var target = _windowCamera.transform; 36 | 37 | if(_axis == Axis.X) { target.Rotate(rotate, 0, 0); } 38 | else if(_axis == Axis.Y) { target.Rotate(0, rotate, 0); } 39 | else if(_axis == Axis.Z) { target.Rotate(0, 0, rotate); } 40 | } 41 | return true; 42 | } 43 | 44 | private Camera GetWindowCamera() { 45 | return CameraTarget.centerTarget.targetCamera; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public abstract class Command 4 | { 5 | public string Name { get; set; } 6 | 7 | private string _displayName; 8 | public string DisplayName 9 | { 10 | get { 11 | if (_displayName == null) 12 | { 13 | _displayName = Name; 14 | if (_displayName.IndexOf("Selected > ") == 0) 15 | { 16 | _displayName = _displayName.Substring("Selected > ".Length); 17 | } 18 | } 19 | return _displayName; 20 | } 21 | set { _displayName = value; } 22 | } 23 | public string Group { get; set; } = CommandConst.CAT_GENERAL; 24 | public string SubGroup { get; set; } = CommandConst.SUBCAT_DEFAULT; 25 | public string RunPhase { get; set; } = CommandConst.RUNPHASE_UPDATE; 26 | public float RepeatDelay { get; set; } = 0.5f; 27 | public float RepeatSpeed { get; set; } = 0.1f; 28 | 29 | public abstract bool Execute(CommandExecuteEventArgs args); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Commands/CommandConst.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | 4 | public static class CommandConst 5 | { 6 | 7 | public const string CAT_GENERAL = "[Global Actions]"; 8 | public const string CAT_SELECTEDATOM = "[Selected Atom Actions]"; 9 | public const string CAT_SELECTED_CONTROLLER = "[Selected Controller]"; 10 | public const string SUBCAT_DEFAULT = ""; 11 | public const string RUNPHASE_UPDATE = "Update"; 12 | public const string RUNPHASE_FIXED_UPDATE = "FixedUpdate"; 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Commands/CommandExecuteEventArgs.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Models; 2 | using System; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class CommandExecuteEventArgs : EventArgs 7 | { 8 | public KeyBinding KeyBinding { get; set; } 9 | public float Data { get; set; } = 0f; 10 | public bool IsRepeat { get; set; } = false; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Commands/ControllerCommandBase.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | internal abstract class ControllerCommandBase : Command { 9 | protected Func _predicate; 10 | protected ControllerCommandBase(Func predicate) 11 | { 12 | _predicate = predicate; 13 | } 14 | 15 | public virtual FreeControllerV3 TargetController(CommandExecuteEventArgs args) 16 | { 17 | if(_predicate == null) 18 | { 19 | return SuperController.singleton.GetSelectedController(); 20 | } 21 | else 22 | { 23 | return SelectableControllers().Where(_predicate).FirstOrDefault(); 24 | } 25 | } 26 | 27 | protected IEnumerable SelectableControllers() 28 | { 29 | return SuperController.singleton.GetSelectableControllers().OrderBy((c) => c.containingAtom.uid); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Commands/ControllerPositionChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | internal class ControllerPositionChange : ControllerCommandBase 9 | { 10 | private Axis _axis; 11 | private float _unitsPerSecond; 12 | private string _atomUid; 13 | private string _controllerName; 14 | public ControllerPositionChange(Axis axis, float unitPerSecond) : this(axis, unitPerSecond, (FreeControllerV3)null) { } 15 | public ControllerPositionChange(Axis axis, float unitPerSecond, FreeControllerV3 controller) : base(null) 16 | { 17 | _axis = axis; 18 | _unitsPerSecond = unitPerSecond; 19 | if(controller != null) 20 | { 21 | _atomUid = controller.containingAtom.uid; 22 | _controllerName = controller.name; 23 | } 24 | } 25 | 26 | public override bool Execute(CommandExecuteEventArgs args) 27 | { 28 | if (args.KeyBinding.KeyChord.HasAxis) 29 | { 30 | // make sure the keybinding and value change are going the 31 | // same "direction" (for the case where the axis is involved) 32 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 33 | } 34 | 35 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetController(args); 36 | if (controller != null) 37 | { 38 | var direction = Vector3.right; 39 | if(_axis == Axis.X) { direction = Vector3.right; } 40 | else if (_axis == Axis.Y) { direction = Vector3.up; } 41 | else if (_axis == Axis.Z) { direction = Vector3.forward; } 42 | 43 | controller.transform.Translate(direction * Time.deltaTime * _unitsPerSecond * Mathf.Abs(args.Data)); 44 | } 45 | return true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Commands/ControllerPositionSetLerp.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | internal class ControllerPositionSetLerp : ControllerCommandBase 9 | { 10 | private Axis _axis; 11 | private float _min; 12 | private float _max; 13 | private string _atomUid; 14 | private string _controllerName; 15 | 16 | public ControllerPositionSetLerp(Axis axis, float absolutePositionMin, float absolutePositionMax) : this(axis, absolutePositionMin, absolutePositionMax, (FreeControllerV3)null) { } 17 | public ControllerPositionSetLerp(Axis axis, float absolutePositionMin, float absolutePositionMax, FreeControllerV3 controller) : base(null) 18 | { 19 | _axis = axis; 20 | _min = absolutePositionMin; 21 | _max = absolutePositionMax; 22 | if(controller != null) 23 | { 24 | _atomUid = controller.containingAtom.uid; 25 | _controllerName = controller.name; 26 | } 27 | } 28 | 29 | public override bool Execute(CommandExecuteEventArgs args) 30 | { 31 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetController(args); 32 | if (controller != null) 33 | { 34 | float proportion = Mathf.Lerp(0, 1, Mathf.Abs(args.Data)); 35 | var transform = controller.transform; 36 | var newPosition = new Vector3(transform.position.x, transform.position.y, transform.position.z); 37 | var newValue = Mathf.Lerp(_min, _max, proportion); 38 | if (_axis == Axis.X) { newPosition.x = newValue; } 39 | else if (_axis == Axis.Y) { newPosition.y = newValue; } 40 | else if (_axis == Axis.Z) { newPosition.z = newValue; } 41 | 42 | transform.position = newPosition; 43 | } 44 | return true; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/ControllerRotationChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | internal class ControllerRotationChange : ControllerCommandBase 9 | { 10 | private Axis _axis; 11 | private float _rotationsPerSecond; 12 | private string _atomUid; 13 | private string _controllerName; 14 | public ControllerRotationChange(Axis axis, float rotationsPerSecond) : this(axis, rotationsPerSecond, (FreeControllerV3)null) { } 15 | public ControllerRotationChange(Axis axis, float rotationsPerSecond, FreeControllerV3 controller) : base(null) 16 | { 17 | _axis = axis; 18 | _rotationsPerSecond = rotationsPerSecond; 19 | if(controller != null) 20 | { 21 | _atomUid = controller.containingAtom.uid; 22 | _controllerName = controller.name; 23 | } 24 | } 25 | 26 | public override bool Execute(CommandExecuteEventArgs args) 27 | { 28 | if (args.KeyBinding.KeyChord.HasAxis) 29 | { 30 | // make sure the keybinding and value change are going the 31 | // same "direction" (for the case where the axis is involved) 32 | if (!MathUtilities.SameSign(args.Data, _rotationsPerSecond)) { return false; } 33 | } 34 | 35 | var controller = SuperController.singleton.GetFreeController(_atomUid, _controllerName) ?? TargetController(args); 36 | if (controller != null) 37 | { 38 | var rotate = 360 * Time.deltaTime * _rotationsPerSecond * Mathf.Abs(args.Data); 39 | var target = controller.transform; 40 | 41 | if(_axis == Axis.X) { target.Rotate(rotate, 0, 0); } 42 | else if(_axis == Axis.Y) { target.Rotate(0, rotate, 0); } 43 | else if(_axis == Axis.Z) { target.Rotate(0, 0, rotate); } 44 | } 45 | return true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Commands/ErrorLogToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class ErrorLogToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | var panel = SuperController.singleton.errorLogPanel.Find("Panel"); 8 | var panelIsShowing = panel?.gameObject.activeInHierarchy ?? false; 9 | if (panelIsShowing) { SuperController.singleton.CloseErrorLogPanel(); } 10 | else { SuperController.singleton.OpenErrorLogPanel(); } 11 | return true; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Commands/FreezeAnimationSet.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class FreezeAnimationSet : Command 4 | { 5 | private bool _value; 6 | public FreezeAnimationSet(bool value) 7 | { 8 | _value = value; 9 | } 10 | 11 | public override bool Execute(CommandExecuteEventArgs args) 12 | { 13 | SuperController.singleton.SetFreezeAnimation(_value); 14 | return true; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Commands/FreezeAnimationToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class FreezeAnimationToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.SetFreezeAnimation(!SuperController.singleton.freezeAnimation); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/HardReset.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class HardReset : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.HardReset(); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/MessageLogToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class MessageLogToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | var panel = SuperController.singleton.msgLogPanel.Find("Panel"); 8 | var panelIsShowing = panel?.gameObject.activeInHierarchy ?? false; 9 | if (panelIsShowing) { SuperController.singleton.CloseMessageLogPanel(); } 10 | else { SuperController.singleton.OpenMessageLogPanel(); } 11 | return true; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Commands/MirrorReflectionsToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class MirrorReflectionsToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | UserPreferences.singleton.mirrorReflections = !UserPreferences.singleton.mirrorReflections; 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/MonitorFieldOfViewChange.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class MonitorFieldOfViewChange : Command 6 | { 7 | private const float _min = 20f; 8 | private const float _max = 100f; 9 | 10 | private float _value; 11 | public MonitorFieldOfViewChange(float value) 12 | { 13 | RunPhase = CommandConst.RUNPHASE_FIXED_UPDATE; 14 | _value = value; 15 | } 16 | 17 | public override bool Execute(CommandExecuteEventArgs args) 18 | { 19 | if (args.KeyBinding.KeyChord.HasAxis) 20 | { 21 | // make sure the keybinding and value change are going the 22 | // same "direction" (for the case where the axis is involved) 23 | if (!MathUtilities.SameSign(args.Data, _value)) { return false; } 24 | } 25 | 26 | SuperController.singleton.monitorCameraFOV = Mathf.Clamp(SuperController.singleton.monitorCameraFOV + _value, _min, _max); 27 | return true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Commands/MouseRightClickDrag.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class MouseRightClickDrag : Command 9 | { 10 | private Axis _axis; 11 | private float _unitsPerSecond; 12 | private Func _target; 13 | public MouseRightClickDrag(Axis axis, float unitPerSecond, Func target = null) 14 | { 15 | Func defaultTarget = () => { 16 | // mimic right mouse click and drag 17 | return 18 | SuperController.singleton.MonitorCenterCamera.transform.position 19 | + SuperController.singleton.MonitorCenterCamera.transform.forward 20 | * SuperController.singleton.focusDistance; 21 | }; 22 | 23 | RunPhase = CommandConst.RUNPHASE_FIXED_UPDATE; 24 | RepeatSpeed = 0; 25 | RepeatDelay = 0; 26 | _axis = axis; 27 | _unitsPerSecond = unitPerSecond; 28 | _target = target ?? defaultTarget; 29 | } 30 | 31 | public override bool Execute(CommandExecuteEventArgs args) 32 | { 33 | if (args.KeyBinding.KeyChord.HasAxis) 34 | { 35 | // make sure the keybinding and value change are going the 36 | // same "direction" (for the case where the axis is involved) 37 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 38 | } 39 | 40 | // this algorithm from Supercontroller.cs ProcessMouseControl() 41 | var navigation = SuperController.singleton.navigationRig; 42 | var target = _target(); 43 | 44 | if(navigation != null && target != null) { 45 | var rotate = 360 * Time.deltaTime * _unitsPerSecond * Mathf.Abs(args.Data); 46 | 47 | if(_axis == Axis.X) { navigation.RotateAround(target.Value, navigation.up, rotate); } 48 | else if(_axis == Axis.Y) { navigation.RotateAround(target.Value, navigation.right, rotate); } 49 | else if(_axis == Axis.Z) { navigation.RotateAround(target.Value, navigation.forward, rotate); } 50 | } 51 | 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Commands/MouseWheelScroll.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using UnityEngine; 4 | using UnityEngine.Animations; 5 | 6 | namespace LFE.KeyboardShortcuts.Commands 7 | { 8 | public class MouseWheelScroll : Command 9 | { 10 | private float _unitsPerSecond; 11 | private Func _target; 12 | public MouseWheelScroll(float unitPerSecond, Func target = null) 13 | { 14 | Func defaultTarget = () => { 15 | // mimic right mouse click and drag 16 | return SuperController.singleton.MonitorCenterCamera.transform; 17 | }; 18 | 19 | RunPhase = CommandConst.RUNPHASE_FIXED_UPDATE; 20 | RepeatSpeed = 0; 21 | RepeatDelay = 0; 22 | _unitsPerSecond = unitPerSecond; 23 | _target = target ?? defaultTarget; 24 | } 25 | 26 | public override bool Execute(CommandExecuteEventArgs args) 27 | { 28 | if (args.KeyBinding.KeyChord.HasAxis) 29 | { 30 | // make sure the keybinding and value change are going the 31 | // same "direction" (for the case where the axis is involved) 32 | if (!MathUtilities.SameSign(args.Data, _unitsPerSecond)) { return false; } 33 | } 34 | 35 | // this algorithm from Supercontroller.cs ProcessMouseControl() 36 | var navigation = SuperController.singleton.navigationRig; 37 | var target = _target(); 38 | 39 | if(navigation != null && target != null) { 40 | var amount = _unitsPerSecond * Mathf.Abs(args.Data) * Time.deltaTime; 41 | 42 | // from SuperController ProcessMouseControl 43 | // float num3 = 0.1f; 44 | // if (amount < -0.5f) 45 | // { 46 | // num3 = 0f - amount; 47 | // } 48 | 49 | Vector3 forward = target.forward; 50 | Vector3 val8 = amount * forward * SuperController.singleton.focusDistance; 51 | Vector3 val9 = navigation.position + val8; 52 | SuperController.singleton.focusDistance *= 1f - amount; 53 | Vector3 up3 = navigation.up; 54 | float num4 = Vector3.Dot(val8, up3); 55 | val9 += up3 * (0f - num4); 56 | 57 | navigation.position = val9; 58 | SuperController.singleton.playerHeightAdjust += num4; 59 | } 60 | 61 | return true; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Commands/MsaaChange.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class MsaaChange : Command 6 | { 7 | private int _indexDelta; 8 | public MsaaChange(int indexDelta) 9 | { 10 | _indexDelta = indexDelta; 11 | } 12 | 13 | public override bool Execute(CommandExecuteEventArgs args) 14 | { 15 | if (args.KeyBinding.KeyChord.HasAxis) 16 | { 17 | // make sure the keybinding and value change are going the 18 | // same "direction" (for the case where the axis is involved) 19 | if (!MathUtilities.SameSign(args.Data, _indexDelta)) { return false; } 20 | } 21 | 22 | var values = UserPreferences.singleton.msaaPopup.popupValues; 23 | var currentIndex = values.ToList().FindIndex((val) => UserPreferences.singleton.msaaPopup.currentValue.Equals(val)); 24 | var newValue = values[Mathf.Clamp(currentIndex + _indexDelta, 0, values.Length - 1)]; 25 | UserPreferences.singleton.SetMsaaFromString(newValue); 26 | return true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Commands/PerformanceMonitorToggle.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System.Linq; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class PerformanceMonitorToggle : Command 7 | { 8 | public override bool Execute(CommandExecuteEventArgs args) 9 | { 10 | var perfMonitorButton = UserPreferences.singleton.transform 11 | .Find((p) => p.EndsWith("PerfMon Toggle")) 12 | .FirstOrDefault() 13 | ?.GetComponent(); 14 | if(perfMonitorButton != null) 15 | { 16 | perfMonitorButton.isOn = !perfMonitorButton.isOn; 17 | } 18 | return true; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Commands/PixelLightCountChange.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class PixelLightCountChange : Command 6 | { 7 | private int _indexDelta; 8 | public PixelLightCountChange(int indexDelta) 9 | { 10 | _indexDelta = indexDelta; 11 | } 12 | 13 | public override bool Execute(CommandExecuteEventArgs args) 14 | { 15 | if (args.KeyBinding.KeyChord.HasAxis) 16 | { 17 | // make sure the keybinding and value change are going the 18 | // same "direction" (for the case where the axis is involved) 19 | if (!MathUtilities.SameSign(args.Data, _indexDelta)) { return false; } 20 | } 21 | 22 | var values = UserPreferences.singleton.pixelLightCountPopup.popupValues; 23 | var currentIndex = values.ToList().FindIndex((val) => UserPreferences.singleton.pixelLightCountPopup.currentValue.Equals(val)); 24 | var newValue = values[Mathf.Clamp(currentIndex + _indexDelta, 0, values.Length - 1)]; 25 | UserPreferences.singleton.SetPixelLightCountFromString(newValue); 26 | return true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Commands/PlayEditModeSet.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class PlayEditModeSet : Command 4 | { 5 | private SuperController.GameMode _mode; 6 | public PlayEditModeSet(SuperController.GameMode mode) 7 | { 8 | _mode = mode; 9 | } 10 | 11 | public override bool Execute(CommandExecuteEventArgs args) 12 | { 13 | SuperController.singleton.gameMode = _mode; 14 | return true; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Commands/PlayEditModeToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class PlayEditModeToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | switch(SuperController.singleton.gameMode) { 8 | case SuperController.GameMode.Edit: 9 | SuperController.singleton.gameMode = SuperController.GameMode.Play; 10 | break; 11 | case SuperController.GameMode.Play: 12 | SuperController.singleton.gameMode = SuperController.GameMode.Edit; 13 | break; 14 | } 15 | return true; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Commands/PluginActionCall.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class PluginActionCall : Command 6 | { 7 | private string _atomUid; 8 | private string _pluginName; 9 | private string _key; 10 | public PluginActionCall(JSONStorable plugin, string key) 11 | { 12 | _atomUid = plugin.containingAtom.uid; 13 | _pluginName = plugin.name; 14 | _key = key; 15 | } 16 | public override bool Execute(CommandExecuteEventArgs args) 17 | { 18 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 19 | plugin?.CallAction(_key); 20 | return true; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Commands/PluginAdd.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using UnityEngine; 7 | 8 | namespace LFE.KeyboardShortcuts.Commands 9 | { 10 | public class PluginAdd : Command 11 | { 12 | private string _atomUid; 13 | private bool _showFilePrompt; 14 | private bool _openPluginUi; 15 | public PluginAdd(Atom atom, bool showFilePrompt = false, bool openPluginUi = false) 16 | { 17 | _atomUid = atom.uid; 18 | _showFilePrompt = showFilePrompt || openPluginUi; 19 | _openPluginUi = openPluginUi; 20 | } 21 | 22 | public override bool Execute(CommandExecuteEventArgs args) 23 | { 24 | var atom = SuperController.singleton.GetAtomByUid(_atomUid); 25 | if(atom == null) { return false; } 26 | 27 | // select the atom plugin tab first 28 | new AtomSelectTab("Plugins", atom).Execute(args); 29 | 30 | // click the "add" button 31 | var ui = atom.GetComponentInChildren(); 32 | if(ui != null) 33 | { 34 | ui?.addPluginButton?.onClick?.Invoke(); 35 | if(_showFilePrompt) 36 | { 37 | // find the newest empty plugin ui section that was added 38 | var lastAddedPluginUi = atom.GetComponentsInChildren() 39 | .Where((x) => x.urlText.text.Equals(string.Empty)) 40 | .LastOrDefault(); 41 | if(lastAddedPluginUi != null) 42 | { 43 | // prompt the user to select a file 44 | lastAddedPluginUi.fileBrowseButton?.onClick?.Invoke(); 45 | // wait up to 120 seconds for user to have made a choice and open 46 | atom.StartCoroutine(Waiter(120, lastAddedPluginUi, (found) => 47 | { 48 | if(found != null && _openPluginUi) 49 | { 50 | found.openUIButton?.onClick?.Invoke(); 51 | } 52 | return; 53 | })); 54 | } 55 | } 56 | } 57 | return true; 58 | } 59 | 60 | private IEnumerator Waiter(int maxSeconds, MVRPluginUI watchPluginUi, Action onComplete) 61 | { 62 | var timer = Stopwatch.StartNew(); 63 | while(timer.Elapsed.TotalSeconds < maxSeconds) 64 | { 65 | yield return new WaitForSeconds(0.25f); 66 | // do we have any script controls yet? 67 | var scriptControllers = watchPluginUi.GetComponentsInChildren().FirstOrDefault(); 68 | if(scriptControllers != null) 69 | { 70 | try { onComplete.Invoke(scriptControllers); } catch(Exception e) { SuperController.LogError(e.ToString()); } 71 | yield break; 72 | } 73 | } 74 | timer.Stop(); 75 | try { onComplete.Invoke(null); } catch(Exception e) { SuperController.LogError(e.ToString()); } 76 | yield break; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Commands/PluginBoolSet.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class PluginBoolSet : Command 6 | { 7 | private string _atomUid; 8 | private string _pluginName; 9 | private string _key; 10 | private bool _value; 11 | public PluginBoolSet(JSONStorable plugin, string key, bool value) 12 | { 13 | _atomUid = plugin.containingAtom.name; 14 | _pluginName = plugin.name; 15 | _key = key; 16 | _value = value; 17 | } 18 | 19 | public override bool Execute(CommandExecuteEventArgs args) 20 | { 21 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 22 | plugin?.SetBoolParamValue(_key, _value); 23 | return true; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/PluginBoolToggle.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class PluginBoolToggle : Command 6 | { 7 | private string _atomUid; 8 | private string _pluginName; 9 | private string _key; 10 | public PluginBoolToggle(JSONStorable plugin, string key) 11 | { 12 | _atomUid = plugin.containingAtom.uid; 13 | _pluginName = plugin.name; 14 | _key = key; 15 | } 16 | public override bool Execute(CommandExecuteEventArgs args) 17 | { 18 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 19 | if(plugin != null) 20 | { 21 | plugin.SetBoolParamValue(_key, !plugin.GetBoolParamValue(_key)); 22 | } 23 | return true; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/PluginFloatChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using UnityEngine; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class PluginFloatChange : Command 7 | { 8 | private string _atomUid; 9 | private string _pluginName; 10 | private string _key; 11 | private float _amount; 12 | public PluginFloatChange(JSONStorable plugin, string key, float amount) 13 | { 14 | _atomUid = plugin.containingAtom.uid; 15 | _pluginName = plugin.name; 16 | _key = key; 17 | _amount = amount; 18 | } 19 | 20 | public override bool Execute(CommandExecuteEventArgs args) 21 | { 22 | if (args.KeyBinding.KeyChord.HasAxis) 23 | { 24 | // make sure the keybinding and value change are going the 25 | // same "direction" (for the case where the axis is involved) 26 | if (!MathUtilities.SameSign(args.Data, _amount)) { return false; } 27 | } 28 | 29 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 30 | 31 | var floatParam = plugin?.GetFloatJSONParam(_key) ?? null; 32 | if(floatParam == null) 33 | { 34 | return false; 35 | } 36 | 37 | var newValue = Mathf.Clamp(floatParam.val + _amount, floatParam.min, floatParam.max); 38 | plugin?.SetFloatParamValue(_key, newValue); 39 | return true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/PluginShowUI.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System.Linq; 3 | using UnityEngine; 4 | 5 | namespace LFE.KeyboardShortcuts.Commands 6 | { 7 | public class PluginShowUI : Command 8 | { 9 | private string _atomUid; 10 | private string _pluginName; 11 | public PluginShowUI(JSONStorable plugin) 12 | { 13 | _atomUid = plugin.containingAtom.uid; 14 | _pluginName = plugin.name; 15 | } 16 | public override bool Execute(CommandExecuteEventArgs args) 17 | { 18 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 19 | if(plugin == null) { return false; } 20 | // make sure the atom plugin tab is showing 21 | if(new AtomSelectTab("Plugins", plugin.containingAtom).Execute(args)) 22 | { 23 | var atom = plugin.containingAtom; 24 | foreach (var scriptController in atom.GetComponentsInChildren()) 25 | { 26 | if(scriptController.label.text.Equals(plugin.name)) 27 | { 28 | scriptController.openUIButton?.onClick?.Invoke(); 29 | return true; 30 | } 31 | } 32 | } 33 | return true; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/PluginStringChooserChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using UnityEngine; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class PluginStringChooserChange : Command 7 | { 8 | private string _atomUid; 9 | private string _pluginName; 10 | private string _key; 11 | private int _incrementBy; 12 | public PluginStringChooserChange(JSONStorable plugin, string key, int incrementBy) 13 | { 14 | _atomUid = plugin.containingAtom.uid; 15 | _pluginName = plugin.name; 16 | _key = key; 17 | _incrementBy = incrementBy; 18 | } 19 | 20 | public override bool Execute(CommandExecuteEventArgs args) 21 | { 22 | if (args.KeyBinding.KeyChord.HasAxis) 23 | { 24 | // make sure the keybinding and value change are going the 25 | // same "direction" (for the case where the axis is involved) 26 | if (!MathUtilities.SameSign(args.Data, _incrementBy)) { return false; } 27 | } 28 | 29 | var plugin = SuperController.singleton.GetPluginStorable(_atomUid, _pluginName); 30 | 31 | var choices = plugin.GetStringChooserJSONParamChoices(_key); 32 | var selectedIndex = choices.FindIndex((c) => c.Equals(plugin.GetStringChooserParamValue(_key))); 33 | var newValue = choices[Mathf.Clamp(selectedIndex + _incrementBy, 0, choices.Count - 1)]; 34 | plugin?.SetStringChooserParamValue(_key, newValue); 35 | return true; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/RescanPackages.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class RescanPackages : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.RescanPackages(); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/SceneLoad.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class SceneLoad : Command 4 | { 5 | private bool _forEdit; 6 | public SceneLoad(bool forEdit = true) 7 | { 8 | _forEdit = forEdit; 9 | } 10 | 11 | public override bool Execute(CommandExecuteEventArgs args) 12 | { 13 | if(_forEdit) 14 | { 15 | SuperController.singleton.LoadSceneForEditDialog(); 16 | } 17 | else 18 | { 19 | SuperController.singleton.LoadSceneDialog(); 20 | } 21 | 22 | return true; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Commands/SceneNew.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class SceneNew : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.NewScene(); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/SceneSave.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class SceneSave : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.SaveSceneDialog(); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/ScreenShotModeOn.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class ScreenShotModeOn : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | SuperController.singleton.SelectModeScreenshot(); 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/SoftBodyPhysicsToggle.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts.Commands 2 | { 3 | public class SoftBodyPhysicsToggle : Command 4 | { 5 | public override bool Execute(CommandExecuteEventArgs args) 6 | { 7 | UserPreferences.singleton.softPhysics = !UserPreferences.singleton.softPhysics; 8 | return true; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Commands/TimeScaleChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using UnityEngine; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class TimeScaleChange : Command 7 | { 8 | private const float _multiplier = 1.0f; 9 | private const float _min = 0.01f; 10 | private const float _max = 1.0f; 11 | 12 | private float _value; 13 | public TimeScaleChange(float value) 14 | { 15 | _value = value; 16 | } 17 | 18 | public override bool Execute(CommandExecuteEventArgs args) 19 | { 20 | if (args.KeyBinding.KeyChord.HasAxis) 21 | { 22 | // make sure the keybinding and value change are going the 23 | // same "direction" (for the case where the axis is involved) 24 | if (!MathUtilities.SameSign(args.Data, _value)) { return false; } 25 | } 26 | 27 | var multiplier = _multiplier; 28 | if (InputWrapper.GetKey(KeyCode.LeftShift) || InputWrapper.GetKey(KeyCode.RightShift)) 29 | { 30 | multiplier *= 5.0f; 31 | } 32 | var scale = TimeControl.singleton.currentScale + (_value * multiplier); 33 | TimeControl.singleton.currentScale = Mathf.Clamp(scale, _min, _max); 34 | return true; 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/UIButtonTriggerCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LFE.KeyboardShortcuts.Commands 4 | { 5 | public class UIButtonTriggerCommand : AtomCommandBase 6 | { 7 | public UIButtonTriggerCommand() : this((Func)null) { } 8 | public UIButtonTriggerCommand(Atom atom) : this((a) => a.uid.Equals(atom.uid)) { } 9 | public UIButtonTriggerCommand(Func predicate) : base(predicate) { } 10 | 11 | public override bool Execute(CommandExecuteEventArgs args) 12 | { 13 | var selected = TargetAtom(args); 14 | if (selected != null) { 15 | var trigger = selected.GetComponentInChildren(); 16 | trigger?.button?.onClick?.Invoke(); 17 | } 18 | return true; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Commands/WorldScaleChange.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using UnityEngine; 3 | 4 | namespace LFE.KeyboardShortcuts.Commands 5 | { 6 | public class WorldScaleChange : Command 7 | { 8 | private const float _multiplier = 1.0f; 9 | private const float _worldScaleMin = 0.01f; 10 | private const float _worldScaleMax = 10.0f; 11 | 12 | private float _amount = 0.0f; 13 | 14 | public WorldScaleChange(float amount) 15 | { 16 | _amount = amount; 17 | } 18 | 19 | public override bool Execute(CommandExecuteEventArgs args) 20 | { 21 | if (args.KeyBinding.KeyChord.HasAxis) 22 | { 23 | // make sure the keybinding and value change are going the 24 | // same "direction" (for the case where the axis is involved) 25 | if (!MathUtilities.SameSign(args.Data, _amount)) { return false; } 26 | } 27 | 28 | SuperController sc = SuperController.singleton; 29 | var multiplier = _multiplier * Mathf.Abs(args.Data); 30 | 31 | if (InputWrapper.GetKey(KeyCode.LeftShift) || InputWrapper.GetKey(KeyCode.RightShift)) 32 | { 33 | multiplier *= 5.0f; 34 | } 35 | 36 | var scale = sc.worldScale + (_amount * multiplier); 37 | SuperController.singleton.worldScale = Mathf.Clamp(scale, _worldScaleMin, _worldScaleMax); 38 | 39 | //// Modify player height with scale 40 | Vector3 dir = Vector3.down; 41 | dir *= multiplier * 0.0011f; 42 | sc.navigationRig.position += dir; 43 | return true; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Extensions/AtomExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace LFE.KeyboardShortcuts.Extensions 5 | { 6 | public static class AtomExtensions { 7 | 8 | public static JSONStorable GetPluginStorable(this Atom atom, string pluginId) 9 | { 10 | foreach(var s in atom.GetPluginStorables()) 11 | { 12 | if(s.name.Equals(pluginId)) 13 | { 14 | //SuperController.LogMessage($"returning {atom.uid} => {s.name}"); 15 | return s; 16 | } 17 | } 18 | //return atom.GetPluginStorables().Where((p) => p.name.Equals(pluginId)).FirstOrDefault(); 19 | return null; 20 | } 21 | 22 | public static IEnumerable GetPluginStorables(this Atom atom) 23 | { 24 | MVRPluginManager manager = atom.GetComponentInChildren(); 25 | if (manager != null) 26 | { 27 | var plugins = manager.GetJSON(true, true)["plugins"].AsObject; 28 | foreach(var pluginId in plugins.Keys) 29 | { 30 | var receivers = atom 31 | .GetStorableIDs() 32 | .Where((sid) => sid.StartsWith(pluginId)) 33 | .Select((sid) => atom.GetStorableByID(sid)); 34 | foreach(var r in receivers) 35 | { 36 | yield return r; 37 | } 38 | } 39 | } 40 | } 41 | 42 | public static UITabSelector GetTabSelector(this Atom atom) 43 | { 44 | if(!atom.freeControllers.Any((fc) => fc.selected)) 45 | { 46 | SuperController.LogError("Unable to get tab selector if a free controller on an atom is not first selected"); 47 | SuperController.LogError("Consider SuperController.singleton.SelectController(atom.mainController)"); 48 | return null; 49 | } 50 | 51 | return atom 52 | .transform 53 | .Find((p) => p.EndsWith("Canvas/Panel/Content")) 54 | .FirstOrDefault() 55 | ?.GetComponent(); 56 | } 57 | 58 | public static IEnumerable GetUITabNames(this Atom atom) 59 | { 60 | switch(atom.type) 61 | { 62 | case "AnimationPattern": 63 | yield return "Animation Triggers"; 64 | yield return "Plugins"; 65 | 66 | yield return "Collision Trigger"; 67 | yield return "Physics Object"; 68 | yield return "Physics Control"; 69 | 70 | yield return "Animation Pattern"; 71 | yield return "Animation"; 72 | yield return "Move"; 73 | 74 | yield return "Preset"; 75 | yield return "Control"; 76 | break; 77 | case "Person": 78 | //first column 79 | yield return "Clothing"; 80 | yield return "Clothing Presets"; 81 | yield return "Hair"; 82 | yield return "Hair Presets"; 83 | yield return "Male Morphs"; 84 | yield return "Female Morphs"; 85 | yield return "Morphs Presets"; 86 | 87 | yield return "Mouth Materials"; 88 | yield return "Tongue Materials"; 89 | yield return "Teeth Materials"; 90 | yield return "M Eyelash Materials"; 91 | yield return "F Eyelash Materials"; 92 | yield return "Lacrimals Materials"; 93 | yield return "Sclera Materials"; 94 | yield return "Iris Materials"; 95 | yield return "Skin Textures"; 96 | yield return "Skin Materials 2"; 97 | yield return "Skin Materials 1"; 98 | yield return "Skin Select"; 99 | yield return "Skin Presets"; 100 | 101 | //second column 102 | yield return "Hand Control"; 103 | yield return "Head Audio"; 104 | yield return "Plugins"; 105 | yield return "Plugin Presets"; 106 | yield return "Auto Behaviours"; 107 | 108 | yield return "Collision Triggers"; 109 | yield return "Misc Physics"; 110 | yield return "M Pectoral Physics"; 111 | yield return "F Breast Physics 2"; 112 | yield return "F Breast Physics 1"; 113 | yield return "F Breast Presets"; 114 | yield return "F Glute Physics"; 115 | yield return "F Glute Presets"; 116 | yield return "Animation"; 117 | yield return "Move"; 118 | yield return "Pose Presets"; 119 | 120 | yield return "Appearance Presets"; 121 | yield return "General Presets"; 122 | yield return "Control & Physics 2"; // 1.20 and up 123 | yield return "Control & Physics 1"; // 1.20 and up 124 | yield return "Control"; // 1.19 and before 125 | break; 126 | case "InvisibleLight": 127 | yield return "Light"; 128 | yield return "Plugins"; 129 | 130 | yield return "Collision Trigger"; 131 | yield return "Physics Object"; 132 | yield return "Physics Control"; 133 | 134 | yield return "Animation"; 135 | yield return "Move"; 136 | 137 | yield return "Preset"; 138 | yield return "Control"; 139 | break; 140 | case "WindowCamera": 141 | yield return "Camera"; 142 | yield return "Plugins"; 143 | 144 | yield return "Collision Trigger"; 145 | yield return "Physics Object"; 146 | yield return "Physics Control"; 147 | 148 | yield return "Animation"; 149 | yield return "Move"; 150 | 151 | yield return "Preset"; 152 | yield return "Control"; 153 | break; 154 | default: 155 | break; 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Extensions 6 | { 7 | public static class EnumerableExtensions 8 | { 9 | public static IEnumerable LoopOnceStartingWhen(this IEnumerable data, Func predicate) 10 | { 11 | var skipped = new List(); 12 | bool predicateMatched = false; 13 | foreach(T item in data) 14 | { 15 | if (!predicateMatched) 16 | { 17 | skipped.Add(item); 18 | if (predicate(item)) 19 | { 20 | predicateMatched = true; 21 | } 22 | } 23 | else 24 | { 25 | yield return item; 26 | } 27 | } 28 | foreach(var item in skipped) 29 | { 30 | yield return item; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Extensions/InputWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace LFE.KeyboardShortcuts.Extensions 7 | { 8 | public static class InputWrapper 9 | { 10 | // Note: The list of axis names here did not work http://wiki.unity3d.com/index.php?title=Xbox360Controller 11 | // Instead, I just looked at the binary data in VaM to find hints on the supported axis names 12 | // using the linux command "strings" 13 | // strings VaM_Data/ globalgamemanagers | egrep - i "axis|trigger|joystick|dpad|mouse" 14 | public static readonly IList AxisNames = new List { "Mouse X", "Mouse Y", "Mouse ScrollWheel", "LeftStickX", "LeftStickY", "RightStickX", "RightStickY", "Triggers", "DPadX", "DPadY", "Axis6", "Axis7", "Axis8" }.AsReadOnly(); 15 | 16 | public static IList KeyCodes = Enum.GetValues(typeof(KeyCode)).Cast().ToList().AsReadOnly(); 17 | 18 | public static float GetAxis(string axisName) 19 | { 20 | return Input.GetAxis(axisName); 21 | } 22 | public static bool AnyAxis() 23 | { 24 | return AxisNames.Any((n) => GetAxis(n) != 0); 25 | } 26 | 27 | public static bool GetKey(KeyCode key) 28 | { 29 | return Input.GetKey(key); 30 | } 31 | 32 | public static bool GetKeyDown(KeyCode key) 33 | { 34 | return Input.GetKeyDown(key); 35 | } 36 | 37 | public static bool AnyKey() 38 | { 39 | return Input.anyKey; 40 | } 41 | 42 | public static bool AnyKeyOrAxis() 43 | { 44 | return AnyKey() || AnyAxis(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Extensions/JSONStorableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Extensions 6 | { 7 | public static class JSONStorableExtensions 8 | { 9 | private static readonly List _commonActionNames = new List 10 | { 11 | "SaveToStore1", "SaveToStore2", "SaveToStore3", 12 | "RestoreAllFromStore1", "RestoreAllFromStore2", "RestoreAllFromStore3", 13 | "RestorePhysicsFromStore1", "RestorePhysicsFromStore2", "RestorePhysicsFromStore3", 14 | "RestoreAppearanceFromStore1", "RestoreAppearanceFromStore2", "RestoreAppearanceFromStore3", 15 | "RestoreAllFromDefaults", "RestorePhysicalFromDefaults", "RestoreAppearanceFromDefaults" 16 | }; 17 | 18 | public static List GetCustomActionNames(this JSONStorable storable) 19 | { 20 | return storable.GetActionNames().Where((a) => !_commonActionNames.Contains(a)).ToList(); 21 | } 22 | 23 | public static string GetShortName(this JSONStorable storable) 24 | { 25 | return storable.GetType().ToString(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Extensions/SuperControllerExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Extensions 6 | { 7 | public static class SuperControllerExtensions 8 | { 9 | /// 10 | /// Get a FreeControllerV3 by name 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static FreeControllerV3 GetFreeController(this SuperController sc, string atomUid, string controllerName) 17 | { 18 | if(atomUid == null || controllerName == null) 19 | { 20 | return null; 21 | } 22 | return sc.GetAtomByUid(atomUid)?.freeControllers?.Where((fc) => fc.name.Equals(controllerName)).FirstOrDefault(); 23 | } 24 | 25 | /// 26 | /// Get all plugin storables 27 | /// 28 | /// 29 | /// 30 | public static IEnumerable GetAllPlugins(this SuperController sc) 31 | { 32 | return sc.GetAtoms().SelectMany((a) => a.GetPluginStorables()); 33 | } 34 | 35 | /// 36 | /// Get a plugin instance 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | public static JSONStorable GetPluginStorable(this SuperController sc, string atomUid, string pluginName) 43 | { 44 | if(atomUid == null || pluginName == null) { return null; } 45 | return sc.GetAtomByUid(atomUid)?.GetPluginStorable(pluginName); 46 | } 47 | 48 | /// 49 | /// Get all of the atoms that have a controller 50 | /// 51 | /// 52 | /// 53 | public static IEnumerable GetSelectableAtoms(this SuperController sc) 54 | { 55 | return sc.GetAtoms().Where((a) => a.mainController != null); 56 | } 57 | 58 | public static IEnumerable GetSelectableControllers(this SuperController sc) 59 | { 60 | return sc.GetSelectableAtoms().SelectMany(a => a.freeControllers, (a, c) => c); 61 | } 62 | 63 | public static IEnumerable GetAtomTypesByCategory(this SuperController sc, string category) 64 | { 65 | if(!sc.GetAtomCategories().Any((c) => c.Equals(category))) 66 | { 67 | yield break; 68 | } 69 | 70 | var originalCategory = sc.atomCategoryPopup.currentValue; 71 | try 72 | { 73 | sc.atomCategoryPopup.currentValue = category; 74 | foreach (var type in sc.atomPrefabPopup.popupValues) 75 | { 76 | yield return type; 77 | } 78 | } 79 | finally 80 | { 81 | sc.atomCategoryPopup.currentValue = originalCategory; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Extensions/TransformExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace LFE.KeyboardShortcuts.Extensions 7 | { 8 | public static class TransformExtensions { 9 | 10 | /// 11 | /// Finds a child where full child path matches the predicate 12 | /// 13 | /// 14 | /// Function that takes full child path name and returns bool if it should be matched 15 | /// 16 | public static IEnumerable Find(this Transform transform, Func predicate) 17 | { 18 | return transform.GetChildPathNames() 19 | .Where(predicate) 20 | .Select((p) => transform.Find(p)) 21 | .Where((t) => t != null); 22 | } 23 | 24 | 25 | 26 | 27 | 28 | /// 29 | /// Return a list of all fully qualified child transform path names recursively 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static IEnumerable GetChildPathNames(this Transform transform, string basePath = null) 35 | { 36 | foreach (Transform child in transform) 37 | { 38 | var path = basePath == null ? child.name : $"{basePath}/{child.name}"; 39 | yield return path; 40 | foreach (var childPath in child.GetChildPathNames(basePath: path)) 41 | { 42 | yield return childPath; 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Extensions/UITabSelectorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using System.Linq; 4 | 5 | namespace LFE.KeyboardShortcuts.Extensions 6 | { 7 | public static class UITabSelectorExtensions 8 | { 9 | 10 | public static bool HasTabName(this UITabSelector tabUi, string name) 11 | { 12 | if(name == null) 13 | { 14 | return false; 15 | } 16 | return tabUi.GetTabNames().Any((n) => n.Equals(name)); 17 | } 18 | 19 | public static IEnumerable GetTabNames(this UITabSelector tabUi) 20 | { 21 | var tabContainer = tabUi?.transform?.Find("ToggleContainer"); 22 | if(tabContainer == null) 23 | { 24 | yield break; 25 | } 26 | 27 | for(var i=0; i< tabContainer.childCount; i++) 28 | { 29 | var tab = tabContainer.GetChild(i) as RectTransform; 30 | if(tab != null) 31 | { 32 | yield return tab.name; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/KeyboardShortcuts.cslist: -------------------------------------------------------------------------------- 1 | Commands/AnimationPatternCommand.cs 2 | Commands/AnimationSpeedChange.cs 3 | Commands/AtomAdd.cs 4 | Commands/AtomCommandBase.cs 5 | Commands/AtomDelete.cs 6 | Commands/AtomDump.cs 7 | Commands/AtomHiddenToggle.cs 8 | Commands/AtomPositionChange.cs 9 | Commands/AtomPositionSetLerp.cs 10 | Commands/AtomRotationChange.cs 11 | Commands/AtomSelect.cs 12 | Commands/AtomSelectNext.cs 13 | Commands/AtomSelectPrev.cs 14 | Commands/AtomSelectTab.cs 15 | Commands/CameraPositionChange.cs 16 | Commands/MouseRightClickDrag.cs 17 | Commands/CameraRotationChange.cs 18 | Commands/Command.cs 19 | Commands/CommandConst.cs 20 | Commands/CommandExecuteEventArgs.cs 21 | Commands/ControllerRotationChange.cs 22 | Commands/ControllerPositionChange.cs 23 | Commands/ControllerCommandBase.cs 24 | Commands/ControllerPositionSetLerp.cs 25 | Commands/MouseWheelScroll.cs 26 | Commands/ErrorLogToggle.cs 27 | Commands/FreezeAnimationSet.cs 28 | Commands/FreezeAnimationToggle.cs 29 | Commands/HardReset.cs 30 | Commands/MessageLogToggle.cs 31 | Commands/MirrorReflectionsToggle.cs 32 | Commands/MsaaChange.cs 33 | Commands/MonitorFieldOfViewChange.cs 34 | Commands/PerformanceMonitorToggle.cs 35 | Commands/PixelLightCountChange.cs 36 | Commands/PlayEditModeSet.cs 37 | Commands/PlayEditModeToggle.cs 38 | Commands/PluginAdd.cs 39 | Commands/PluginActionCall.cs 40 | Commands/PluginBoolSet.cs 41 | Commands/PluginBoolToggle.cs 42 | Commands/PluginFloatChange.cs 43 | Commands/PluginShowUI.cs 44 | Commands/PluginStringChooserChange.cs 45 | Commands/RescanPackages.cs 46 | Commands/SceneLoad.cs 47 | Commands/SceneNew.cs 48 | Commands/SceneSave.cs 49 | Commands/ScreenShotModeOn.cs 50 | Commands/SoftBodyPhysicsToggle.cs 51 | Commands/TimeScaleChange.cs 52 | Commands/UIButtonTriggerCommand.cs 53 | Commands/WorldScaleChange.cs 54 | Extensions/AtomExtensions.cs 55 | Extensions/EnumerableExtensions.cs 56 | Extensions/InputWrapper.cs 57 | Extensions/JSONStorableExtensions.cs 58 | Extensions/SuperControllerExtensions.cs 59 | Extensions/TransformExtensions.cs 60 | Extensions/UITabSelectorExtensions.cs 61 | Models/BindingEvent.cs 62 | Models/CommandFactory.cs 63 | Models/KeyBinding.cs 64 | Models/KeyChord.cs 65 | Models/KeyRecorder.cs 66 | Models/ViewModel.cs 67 | Utils/MathUtilities.cs 68 | Utils/TimingLogger.cs 69 | Main/Plugin.cs -------------------------------------------------------------------------------- /src/Main/Plugin.cs: -------------------------------------------------------------------------------- 1 | /*********************************************************************************** 2 | KeyboardShortcuts by LFE#9677 3 | 4 | Allows defining custom keyboard bindings to trigger actions 5 | 6 | KNOWN BUGS 7 | VaM built in shortcuts (like "T" or "E") will always fire even if you set 8 | your own in this plugin. Try and avoid overlapping with VaM shortcuts. 9 | 10 | FEATURE REQUESTS / TODO: 11 | 12 | - Ability to share shortcuts with others and let them import them 13 | 14 | ***********************************************************************************/ 15 | using UnityEngine; 16 | using UnityEngine.EventSystems; 17 | using System; 18 | using System.Linq; 19 | using LFE.KeyboardShortcuts.Models; 20 | using LFE.KeyboardShortcuts.Extensions; 21 | using System.Collections.Generic; 22 | using LFE.KeyboardShortcuts.Commands; 23 | 24 | namespace LFE.KeyboardShortcuts.Main 25 | { 26 | public class Plugin : MVRScript 27 | { 28 | 29 | private ViewModel model; 30 | 31 | public override void Init() 32 | { 33 | try 34 | { 35 | model = new ViewModel(this); 36 | } 37 | catch (Exception e) 38 | { 39 | SuperController.LogError("Exception caught: " + e); 40 | } 41 | } 42 | 43 | public void Start() 44 | { 45 | SaveBindingSettings(); 46 | } 47 | 48 | private HashSet _activeBindings = new HashSet(); 49 | 50 | private float _chordRepeatTimestamp; 51 | 52 | protected void Update() 53 | { 54 | model.CheckPluginsHaveChanged(); 55 | 56 | if (model.IsRecording) 57 | { 58 | model.RecordUpdate(); 59 | } 60 | 61 | // If no keys are pressed then just exit out 62 | if (!InputWrapper.AnyKeyOrAxis()) 63 | { 64 | if(model.IsRecording) 65 | { 66 | model.RecordFinish(); 67 | } 68 | 69 | foreach(var noLongerPressed in _activeBindings) 70 | { 71 | // axis commands really benefit from a final "reset to 0" command run -- so do that 72 | if(noLongerPressed.KeyChord.HasAxis) 73 | { 74 | model.EnqueueAction( 75 | new BindingEvent { 76 | EventName = BindingEvent.ON_BINDING_REPEAT, 77 | Command = noLongerPressed.Command, 78 | Binding = noLongerPressed, 79 | Args = new CommandExecuteEventArgs 80 | { 81 | KeyBinding = noLongerPressed, 82 | Data = 0, 83 | IsRepeat = true 84 | } 85 | } 86 | ); 87 | } 88 | } 89 | _activeBindings.Clear(); 90 | 91 | return; 92 | } 93 | 94 | if (model.IsRecording) 95 | { 96 | return; 97 | } 98 | 99 | // If we are typing somewhere else, don't listen for keybindings 100 | var focusedObject = EventSystem.current.currentSelectedGameObject; 101 | if(focusedObject != null && focusedObject.GetComponent() != null) 102 | { 103 | return; 104 | } 105 | 106 | // get all bindings that match the keys being pressed ordered by the most specific ones first 107 | var matches = model.KeyBindings 108 | .Where((b) => b.KeyChord.IsBeingPressed()) 109 | .OrderByDescending((b) => b.KeyChord.Length) 110 | .ToList(); 111 | 112 | // nothing being pressed 113 | if (matches.Count == 0) 114 | { 115 | _activeBindings.Clear(); 116 | return; 117 | } 118 | 119 | // see if anything used to be pressed but is not anymore 120 | foreach(var noLongerPressed in _activeBindings.Where((b) => !matches.Contains(b))) 121 | { 122 | // axis commands really benefit from a final "reset to 0" command run -- so do that 123 | if(noLongerPressed.KeyChord.HasAxis) 124 | { 125 | model.EnqueueAction( 126 | new BindingEvent { 127 | EventName = BindingEvent.ON_BINDING_UP, 128 | Command = noLongerPressed.Command, 129 | Binding = noLongerPressed, 130 | Args = new CommandExecuteEventArgs 131 | { 132 | KeyBinding = noLongerPressed, 133 | Data = 0, 134 | IsRepeat = true 135 | } 136 | } 137 | ); 138 | } 139 | } 140 | // remove only the inactive bindings 141 | _activeBindings.RemoveWhere((b) => !matches.Contains(b)); 142 | 143 | foreach (var binding in matches) 144 | { 145 | var chord = binding.KeyChord; 146 | var action = binding.Action; 147 | 148 | // did another binding get triggered that is a superset of us? 149 | // (or are we a subset of an action that was just triggered) 150 | if(_activeBindings.Any((b) => chord.IsProperSubsetOf(b.KeyChord))) 151 | { 152 | // .. then consider us already tiggered 153 | continue; 154 | } 155 | 156 | if (chord.IsBeingRepeated()) 157 | { 158 | // Check repeat if still holding down last key 159 | if (_activeBindings.Any((b) => b.Equals(binding))) 160 | { 161 | // Still holding down the key... is it time for a repeat? 162 | if (Time.unscaledTime >= _chordRepeatTimestamp) 163 | { 164 | _chordRepeatTimestamp = Time.unscaledTime + binding.Command.RepeatSpeed; 165 | model.EnqueueAction( 166 | new BindingEvent { 167 | EventName = BindingEvent.ON_BINDING_REPEAT, 168 | Command = binding.Command, 169 | Binding = binding, 170 | Args = new CommandExecuteEventArgs 171 | { 172 | KeyBinding = binding, 173 | Data = binding.KeyChord.GetPressedValue(), 174 | IsRepeat = true 175 | } 176 | } 177 | ); 178 | } 179 | } 180 | else 181 | { 182 | _activeBindings.Add(binding); 183 | _chordRepeatTimestamp = Time.unscaledTime + binding.Command.RepeatDelay; 184 | } 185 | } 186 | else 187 | { 188 | // Handle the keypress and set the repeat delay timer 189 | model.EnqueueAction( 190 | new BindingEvent { 191 | EventName = BindingEvent.ON_BINDING_DOWN, 192 | Command = binding.Command, 193 | Binding = binding, 194 | Args = new CommandExecuteEventArgs 195 | { 196 | KeyBinding = binding, 197 | Data = binding.KeyChord.GetPressedValue(), 198 | IsRepeat = false 199 | } 200 | } 201 | ); 202 | 203 | _chordRepeatTimestamp = Time.unscaledTime + binding.Command.RepeatDelay; 204 | _activeBindings.Add(binding); 205 | } 206 | } 207 | 208 | // run any queue commands for this phase 209 | foreach(var e in model.DequeueActionForUpdate()) { 210 | var result = e.Execute(); 211 | } 212 | } 213 | 214 | public void FixedUpdate() { 215 | // run any queue commands for this phase 216 | foreach(var e in model.DequeueActionForFixedUpdate()) { 217 | var result = e.Execute(); 218 | } 219 | } 220 | 221 | void OnDestroy() 222 | { 223 | model?.Destroy(); 224 | } 225 | 226 | public SimpleJSON.JSONClass LoadBindingSettings() 227 | { 228 | try 229 | { 230 | var settings = SuperController.singleton.LoadJSON($"Saves\\lfe_keyboardshortcuts.json"); 231 | if(settings == null) { 232 | return null; 233 | } 234 | return settings.AsObject; 235 | } 236 | catch(Exception e) 237 | { 238 | // note: can't load "System.IO" without error so we will ignore a file 239 | // not found error by it's text (sadface) 240 | if(e.Message.Contains("Could not find file")) 241 | { 242 | // log nothing 243 | } 244 | else 245 | { 246 | SuperController.LogError(e.ToString(), false); 247 | } 248 | return null; 249 | } 250 | } 251 | 252 | public void SaveBindingSettings() 253 | { 254 | // merge 255 | var json = LoadBindingSettings() ?? new SimpleJSON.JSONClass(); 256 | 257 | // set any bindings that might have been updated during this session 258 | foreach(var binding in model.KeyBindings) 259 | { 260 | var value = new SimpleJSON.JSONClass(); 261 | value["enabled"] = binding.Enabled.ToString(); 262 | value["chord"] = binding.KeyChord.ToString(); 263 | json[binding.Name] = value; 264 | } 265 | // delete any bindings that this session knows about that were cleared this session 266 | foreach(var bindingName in model.UnusedKeyBindings) 267 | { 268 | json.Remove(bindingName); 269 | } 270 | try 271 | { 272 | SuperController.singleton.SaveJSON(json, $"Saves\\lfe_keyboardshortcuts.json"); 273 | } 274 | catch(Exception e) 275 | { 276 | SuperController.LogError(e.ToString(), false); 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/Models/BindingEvent.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Commands; 2 | 3 | namespace LFE.KeyboardShortcuts.Models 4 | { 5 | public class BindingEvent { 6 | 7 | public const string ON_BINDING_UP = "OnBindingUp"; 8 | public const string ON_BINDING_DOWN = "OnBindingDown"; 9 | public const string ON_BINDING_REPEAT = "OnBindingRepeat"; 10 | 11 | public string EventName { get; set; } 12 | public KeyBinding Binding { get; set; } 13 | public Command Command { get; set; } 14 | public CommandExecuteEventArgs Args { get; set; } 15 | 16 | public bool Execute() { 17 | switch(EventName) { 18 | case ON_BINDING_UP: 19 | Command.Execute(Args); 20 | return true; 21 | case ON_BINDING_REPEAT: 22 | if(Binding.Enabled) { 23 | return Command.Execute(Args); 24 | } 25 | return true; 26 | case ON_BINDING_DOWN: 27 | if(Binding.Enabled) { 28 | return Command.Execute(Args); 29 | } 30 | return false; 31 | } 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/KeyBinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LFE.KeyboardShortcuts.Commands; 3 | using LFE.KeyboardShortcuts.Main; 4 | 5 | namespace LFE.KeyboardShortcuts.Models 6 | { 7 | public class KeyBinding 8 | { 9 | public KeyChord KeyChord { get; private set; } 10 | public Func Action { get; private set; } 11 | public Command Command {get; private set; } 12 | public string Name { get; private set; } 13 | public bool Enabled { get; set; } 14 | 15 | private KeyBinding(string name, KeyChord chord, Command command) 16 | { 17 | KeyChord = chord; 18 | Action = (e) => command.Execute(e); 19 | Name = name; 20 | Enabled = true; 21 | Command = command; 22 | } 23 | 24 | public static KeyBinding Build(Plugin plugin, string name, KeyChord chord, Command command) 25 | { 26 | return new KeyBinding(name, chord, command); 27 | } 28 | 29 | public override string ToString() 30 | { 31 | return Name; 32 | } 33 | 34 | public override bool Equals(object obj) 35 | { 36 | KeyBinding b = obj as KeyBinding; 37 | if (b == null) { return false; } 38 | return b.ToString().Equals(this.ToString()); 39 | } 40 | 41 | public override int GetHashCode() 42 | { 43 | return this.ToString().GetHashCode(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/KeyChord.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace LFE.KeyboardShortcuts.Models 8 | { 9 | /// 10 | /// Represent multiple key combinations (chords). 11 | /// 12 | /// Also allows for supporting "virtual" keycodes like "Control" which will 13 | /// internally match both "LeftControl" and "RightControl" 14 | /// 15 | public class KeyChord 16 | { 17 | public static readonly HashSet IGNORED_KEYS = new HashSet() { KeyCode.Mouse0 }; 18 | private static readonly char[] DELIMITER = { '+', '-' }; 19 | private static readonly Dictionary> _virtualKeyNameToCodes = new Dictionary>(StringComparer.OrdinalIgnoreCase) 20 | { 21 | { "Shift", new List { KeyCode.LeftShift, KeyCode.RightShift } }, 22 | { "Control", new List { KeyCode.LeftControl, KeyCode.RightControl } }, 23 | { "Alt", new List { KeyCode.LeftAlt, KeyCode.RightAlt } } 24 | }; 25 | 26 | private string _originalChord; 27 | private IEnumerable> _keyPressDefinitions { get; } 28 | private string _axisDefinition; 29 | 30 | private IEnumerable> _namedDefinitions { get; } 31 | 32 | /// 33 | /// Number of keys in this key chord 34 | /// 35 | public int Length => _keyPressDefinitions.FirstOrDefault().Count + (_axisDefinition != null ? 1 : 0); 36 | 37 | public bool HasAxis => _axisDefinition != null; 38 | 39 | public KeyChord(string chord) 40 | { 41 | if (chord == null) { 42 | throw new ArgumentNullException(nameof(chord)); 43 | } 44 | var chordParts = chord.Split(DELIMITER).ToList(); 45 | var axisNamesInChord = chordParts.Where((n) => InputWrapper.AxisNames.Contains(n)); 46 | if (axisNamesInChord.Count() > 1) 47 | { 48 | throw new ArgumentException("only one axis allowed", nameof(chord)); 49 | } 50 | 51 | _axisDefinition = axisNamesInChord.FirstOrDefault(); // it's ok if this is null 52 | _originalChord = chord; 53 | _keyPressDefinitions = ConcreteChordsFromVirtual(chordParts); 54 | 55 | // create an internal copy of the keycode listings but by string/name (and include the axis) 56 | var namedDefinitions = new List>(); 57 | var axisEnumerable = _axisDefinition == null ? Enumerable.Empty() : new string[] { _axisDefinition }; 58 | if (_keyPressDefinitions.Count() > 0) 59 | { 60 | foreach (var definition in _keyPressDefinitions) 61 | { 62 | namedDefinitions.Add(new HashSet(definition.Select((code) => code.ToString()).Concat(axisEnumerable))); 63 | } 64 | } 65 | else 66 | { 67 | namedDefinitions.Add(new HashSet(axisEnumerable)); 68 | } 69 | _namedDefinitions = namedDefinitions; 70 | } 71 | 72 | public IEnumerable> GetChordSets() 73 | { 74 | return _namedDefinitions; 75 | } 76 | 77 | /// 78 | /// Have all of the keys been held down for a while? 79 | /// 80 | /// 81 | public bool IsBeingRepeated() 82 | { 83 | if (!IsBeingPressed()) 84 | { 85 | return false; 86 | } 87 | if(HasAxis) 88 | { 89 | // always run axis commands as fast as possible 90 | return false; 91 | } 92 | return _keyPressDefinitions.Any((definition) => definition.All((c) => !InputWrapper.GetKeyDown(c))); 93 | } 94 | 95 | /// 96 | /// Are all of the keys being held down? 97 | /// 98 | /// 99 | public bool IsBeingPressed() 100 | { 101 | var isButtonsPressed = _keyPressDefinitions.Any((definition) => definition.All(InputWrapper.GetKey)); 102 | var isAxisPressed = (_axisDefinition == null) ? true : InputWrapper.GetAxis(_axisDefinition) != 0; 103 | return isButtonsPressed && isAxisPressed; 104 | } 105 | 106 | /// 107 | /// If pressing a key you will get a 1f. If this includes an axis, then you will 108 | /// get -1f to 1f (but sometimes higher ranges depending on the axis) 109 | /// 110 | /// 111 | public float GetPressedValue() 112 | { 113 | if (!IsBeingPressed()) 114 | { 115 | return 0; 116 | } 117 | 118 | if (_axisDefinition != null) 119 | { 120 | return InputWrapper.GetAxis(_axisDefinition); 121 | } 122 | 123 | return 1; 124 | } 125 | 126 | public bool IsProperSubsetOf(KeyChord chord) 127 | { 128 | return GetChordSets().Any((mine) => chord.GetChordSets().Any((theirs) => mine.IsProperSubsetOf(theirs))); 129 | } 130 | 131 | public override string ToString() 132 | { 133 | return _originalChord; 134 | } 135 | 136 | public override bool Equals(object obj) 137 | { 138 | KeyChord kc = obj as KeyChord; 139 | if (kc == null) { return false; } 140 | return kc.ToString().Equals(this.ToString()); 141 | } 142 | 143 | public override int GetHashCode() 144 | { 145 | return this.ToString().GetHashCode(); 146 | } 147 | 148 | /// 149 | /// Take chord parts containing virtual key names and return a list 150 | /// of all concrete chords that would match 151 | /// 152 | /// Example: 153 | /// "Control-C" -> "LeftControl-C", "RightControl-C" 154 | /// 155 | /// 156 | /// 157 | private static IEnumerable> ConcreteChordsFromVirtual(List chordParts) 158 | { 159 | bool hasVirtualKeyCode = chordParts.Any((p) => _virtualKeyNameToCodes.ContainsKey(p)); 160 | if (hasVirtualKeyCode) 161 | { 162 | foreach (var virtualCode in _virtualKeyNameToCodes) 163 | { 164 | var modifierIndexInChord = chordParts.IndexOf(virtualCode.Key); 165 | if (modifierIndexInChord >= 0) 166 | { 167 | foreach (var realCode in virtualCode.Value) 168 | { 169 | var newChord = new List(chordParts); 170 | newChord[modifierIndexInChord] = realCode.ToString(); 171 | var concretes = ConcreteChordsFromVirtual(newChord); 172 | foreach (var concrete in concretes) 173 | { 174 | yield return concrete; 175 | } 176 | } 177 | } 178 | } 179 | } 180 | else 181 | { 182 | // if there is an axis name in here, thats ok.. just parse out things that 183 | // we know are button presses... later on we will pull our axis names 184 | var chordKeys = chordParts 185 | .Where((x) => !InputWrapper.AxisNames.Contains(x)) 186 | .Select((x) => (KeyCode)Enum.Parse(typeof(KeyCode), x)); 187 | var hash = new HashSet(chordKeys); 188 | if (hash.Intersect(IGNORED_KEYS).ToList().Count > 0) 189 | { 190 | throw new Exception($"These keys are not allowed {IGNORED_KEYS}"); 191 | } 192 | yield return hash; 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Models/KeyRecorder.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using UnityEngine; 7 | 8 | namespace LFE.KeyboardShortcuts.Models 9 | { 10 | public class KeyRecorder 11 | { 12 | public bool InProgress { get; private set; } = false; 13 | 14 | private Action _onFinish; 15 | private HashSet _recordedKeys = new HashSet(); 16 | private string _recordedAxis; 17 | 18 | public KeyRecorder(Action onFinish = null) 19 | { 20 | _onFinish = onFinish; 21 | } 22 | 23 | public void Record() 24 | { 25 | var noticedAxisThreshold = 0.5f; // don't consider an axis noticed for record for "blips" 26 | 27 | var noticedKeys = InputWrapper.KeyCodes 28 | .Where(InputWrapper.GetKey) 29 | .Where((k) => !KeyChord.IGNORED_KEYS.Contains(k)) 30 | .Where((k) => !Regex.IsMatch(k.ToString(), "^Joystick\\d")) // joy buttons register twice -- ignore most specific one 31 | .ToList(); 32 | var noticedAxis = InputWrapper.AxisNames 33 | .Where((n) => Mathf.Abs(InputWrapper.GetAxis(n)) >= noticedAxisThreshold) 34 | .FirstOrDefault(); 35 | 36 | if (InProgress && noticedKeys.Count == 0 && noticedAxis == null) 37 | { 38 | // looks like we just finished recording... don't 39 | // set the internal state anymore 40 | return; 41 | } 42 | 43 | if (noticedKeys.Count > 0) 44 | { 45 | InProgress = true; 46 | foreach (var k in noticedKeys) 47 | { 48 | _recordedKeys.Add(k); 49 | } 50 | } 51 | 52 | if(noticedAxis != null) 53 | { 54 | InProgress = true; 55 | _recordedAxis = noticedAxis; 56 | } 57 | 58 | return; 59 | } 60 | 61 | public KeyChord ToKeyChord() 62 | { 63 | var chordParts = _recordedKeys.Select((k) => k.ToString()).ToList(); 64 | if(_recordedAxis != null) 65 | { 66 | chordParts.Add(_recordedAxis); 67 | } 68 | var chord = string.Join("-", chordParts.ToArray()); 69 | return new KeyChord(chord); 70 | } 71 | 72 | public void OnFinish() 73 | { 74 | if (_onFinish != null) 75 | { 76 | _onFinish(ToKeyChord()); ; 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/Models/ViewModel.cs: -------------------------------------------------------------------------------- 1 | using LFE.KeyboardShortcuts.Commands; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEngine; 6 | using LFE.KeyboardShortcuts.Utils; 7 | using LFE.KeyboardShortcuts.Main; 8 | 9 | namespace LFE.KeyboardShortcuts.Models 10 | { 11 | public class ViewModel 12 | { 13 | public Plugin Plugin { get; private set; } 14 | private CommandFactory _actionController; 15 | private Dictionary _bindings; 16 | public KeyRecorder keyRecorder; 17 | 18 | private SuperController.OnAtomUIDsChanged _onAtomUIDsChanged; 19 | 20 | public ViewModel(Plugin plugin) 21 | { 22 | Plugin = plugin; 23 | 24 | _onAtomUIDsChanged = (uidList) => 25 | { 26 | if(!SuperController.singleton.isLoading) { 27 | Initialize(); 28 | } 29 | }; 30 | 31 | SuperController.singleton.onAtomUIDsChangedHandlers += _onAtomUIDsChanged; 32 | Initialize(); 33 | } 34 | 35 | public void Destroy() 36 | { 37 | if(_onAtomUIDsChanged != null) 38 | { 39 | SuperController.singleton.onAtomUIDsChangedHandlers -= _onAtomUIDsChanged; 40 | } 41 | _actionFilterStorable = null; 42 | _actionFilterUi = null; 43 | } 44 | 45 | public string ActionCategory { get; private set; } = CommandConst.CAT_GENERAL; 46 | 47 | public string ActionSubCategory { get; private set; } = CommandConst.SUBCAT_DEFAULT; 48 | 49 | public IEnumerable ActionNames => _bindings.Keys; 50 | public IEnumerable KeyBindings => _bindings.Values.Where((b) => b != null); 51 | public IEnumerable UnusedKeyBindings => _bindings.Where((kvp) => kvp.Value == null).Select((kvp) => kvp.Key); 52 | 53 | public void ClearKeyBinding(string name) 54 | { 55 | if(_bindings.ContainsKey(name)) 56 | { 57 | _bindings[name] = null; 58 | } 59 | } 60 | 61 | public KeyBinding GetKeyBinding(string name) 62 | { 63 | return _bindings.ContainsKey(name) ? _bindings[name] : null; 64 | } 65 | 66 | public void SetKeyBinding(string name, KeyBinding binding) 67 | { 68 | if (!_bindings.ContainsKey(name)) { throw new ArgumentException($"Invalid action name {name}", nameof(name)); } 69 | _bindings[name] = binding; 70 | } 71 | 72 | private Dictionary _lastAtomPluginInfo; 73 | private float _lastAtomPluginInfoTimer = 0.0f; 74 | public void CheckPluginsHaveChanged() 75 | { 76 | _lastAtomPluginInfoTimer += Time.deltaTime; 77 | 78 | if(SuperController.singleton.isLoading) { 79 | // Don't loop through scene information looking for changes if it is in the process of loading. 80 | return; 81 | } 82 | 83 | // only poll for plugin changes once every 3 seconds for performance 84 | if(_lastAtomPluginInfoTimer < 3.0f) { 85 | return; 86 | } 87 | else 88 | { 89 | _lastAtomPluginInfoTimer = 0.0f; 90 | } 91 | 92 | var atomPluginInfo = new Dictionary(); 93 | foreach (var atom in SuperController.singleton.GetAtoms()) 94 | { 95 | MVRPluginManager manager = atom.GetComponentInChildren(); 96 | if (manager != null) 97 | { 98 | var atomUid = atom.uid; 99 | var pluginInfo = manager.GetJSON(true, true).ToString(); 100 | atomPluginInfo[atomUid] = pluginInfo; 101 | } 102 | } 103 | 104 | if(_lastAtomPluginInfo != null) 105 | { 106 | // calculate which atom names have plugin changes 107 | var changedAtomUids = atomPluginInfo.Except(_lastAtomPluginInfo) 108 | .Concat(_lastAtomPluginInfo.Except(atomPluginInfo)) 109 | .Select((kvp) => kvp.Key) 110 | .Distinct(); 111 | if(changedAtomUids.Count() > 0) 112 | { 113 | Initialize(); 114 | } 115 | } 116 | 117 | _lastAtomPluginInfo = atomPluginInfo; 118 | } 119 | 120 | private Queue _actionQueueUpdatePhase = new Queue(); 121 | private Queue _actionQueueFixedUpdatePhase = new Queue(); 122 | public void EnqueueAction(BindingEvent e) { 123 | switch(e.Command.RunPhase) { 124 | case CommandConst.RUNPHASE_FIXED_UPDATE: 125 | _actionQueueFixedUpdatePhase.Enqueue(e); 126 | break; 127 | case CommandConst.RUNPHASE_UPDATE: 128 | default: 129 | _actionQueueUpdatePhase.Enqueue(e); 130 | break; 131 | } 132 | } 133 | 134 | public IEnumerable DequeueActionForUpdate() { 135 | for(var i = 0; i < _actionQueueUpdatePhase.Count; i++) { 136 | yield return _actionQueueUpdatePhase.Dequeue(); 137 | } 138 | } 139 | 140 | public IEnumerable DequeueActionForFixedUpdate() { 141 | for(var i = 0; i < _actionQueueFixedUpdatePhase.Count; i++) { 142 | yield return _actionQueueFixedUpdatePhase.Dequeue(); 143 | } 144 | } 145 | 146 | public void InitializeUI() 147 | { 148 | ClearUI(); 149 | InitUI(); 150 | } 151 | 152 | public void Initialize() 153 | { 154 | using(var timing = TimingLogger.Track("Initialize()")) 155 | { 156 | _actionController = new CommandFactory(Plugin); 157 | InitBindings(); 158 | InitializeUI(); 159 | } 160 | } 161 | 162 | public bool IsRecording => keyRecorder != null; 163 | public void RecordUpdate() 164 | { 165 | keyRecorder?.Record(); 166 | } 167 | public void RecordFinish() 168 | { 169 | if (keyRecorder != null && keyRecorder.InProgress) 170 | { 171 | try 172 | { 173 | keyRecorder.OnFinish(); 174 | } 175 | catch(Exception e) 176 | { 177 | SuperController.LogError(e.ToString()); 178 | } 179 | keyRecorder = null; 180 | } 181 | } 182 | 183 | private Dictionary _commandsByName = new Dictionary(); 184 | private void InitBindings() 185 | { 186 | _bindings = new Dictionary(); 187 | var settings = Plugin.LoadBindingSettings(); 188 | 189 | // build all of the commands/actions 190 | _commandsByName = new Dictionary(); 191 | var commands = _actionController.BuildCommands(); 192 | foreach(var command in commands) 193 | { 194 | var commandName = command.Name; 195 | 196 | _commandsByName[commandName] = command; 197 | // Consider exporting this action to the plugin so it shows up in GetActions elsewhere 198 | //_plugin.RegisterAction(new JSONStorableAction(commandName, () => command.Execute())); 199 | 200 | // fill in bindings from the saved settings 201 | if(settings != null) 202 | { 203 | if(settings[commandName] != null) 204 | { 205 | try 206 | { 207 | var chord = settings[commandName]["chord"].Value; 208 | var enabled = settings[commandName]["enabled"].AsBool; 209 | 210 | var binding = KeyBinding.Build(Plugin, commandName, new KeyChord(chord), command); 211 | binding.Enabled = enabled; 212 | _bindings[commandName] = binding; 213 | } 214 | catch 215 | { 216 | SuperController.LogError($"Failed loading action {commandName} from save file"); 217 | _bindings[commandName] = null; 218 | } 219 | } 220 | else 221 | { 222 | _bindings[commandName] = null; 223 | } 224 | } 225 | // ... or use defaults if there isn't anything there yet 226 | else 227 | { 228 | var defaultKeyChord = _actionController.GetDefaultKeyChordByActionName(commandName); 229 | if (defaultKeyChord != null) 230 | { 231 | _bindings[commandName] = KeyBinding.Build(Plugin, commandName, defaultKeyChord, command); 232 | } 233 | else 234 | { 235 | _bindings[commandName] = null; 236 | } 237 | } 238 | } 239 | } 240 | 241 | private void ClearUI() 242 | { 243 | foreach(var item in _uiBindings) 244 | { 245 | item.Destroy(); 246 | } 247 | _uiBindings = new List(); 248 | } 249 | 250 | private IEnumerable GetGroupNames() 251 | { 252 | return _commandsByName.Values 253 | .Select((c) => c.Group) 254 | .Distinct(); 255 | } 256 | 257 | private IEnumerable GetSubGroupNames(string category) 258 | { 259 | return _commandsByName.Values 260 | .Where((c) => c.Group.Equals(category)) 261 | .Select((c) => c.SubGroup) 262 | .Distinct(); 263 | } 264 | 265 | private JSONStorableStringChooser _actionFilterStorable; 266 | private UIDynamicPopup _actionFilterUi; 267 | private JSONStorableStringChooser _actionSubCatFilterStorable; 268 | private UIDynamicPopup _actionSubCatFilterUi; 269 | 270 | private List _uiBindings = new List(); 271 | 272 | private void InitUIHeader() 273 | { 274 | using(TimingLogger.Track("InitUIHeader()")) { 275 | // add the action filter 276 | 277 | using (TimingLogger.Track("... setup category dropdown")) 278 | { 279 | var groupNames = GetGroupNames().OrderBy((x) => x).ToList(); 280 | if (_actionFilterStorable == null) 281 | { 282 | _actionFilterStorable = new JSONStorableStringChooser("category", groupNames, ActionCategory, ""); 283 | _actionFilterUi = Plugin.CreateScrollablePopup(_actionFilterStorable); 284 | _actionFilterUi.labelWidth = 0f; 285 | _actionFilterUi.popup.onValueChangeHandlers += (newValue) => 286 | { 287 | var currentSubCategory = ActionSubCategory; 288 | ActionCategory = newValue; 289 | var subCategories = GetSubGroupNames(newValue).ToList(); 290 | if(subCategories.Count == 0) 291 | { 292 | ActionSubCategory = ""; 293 | } 294 | else if(!subCategories.Any((c) => c.Equals(currentSubCategory))) 295 | { 296 | ActionSubCategory = subCategories[0]; 297 | } 298 | InitializeUI(); 299 | 300 | // TODO: figure out how to actually get this UI element to 301 | // show up on top of the UI elements just below it instead 302 | // of behind 303 | _actionFilterUi.popup.Toggle(); 304 | _actionFilterUi.popup.Toggle(); 305 | }; 306 | } 307 | else 308 | { 309 | _actionFilterStorable.choices = groupNames; 310 | } 311 | } 312 | 313 | using (TimingLogger.Track("... setup subcategory dropdown")) 314 | { 315 | // add the subcategory filter 316 | var subCategories = GetSubGroupNames(ActionCategory).OrderBy((x) => x).ToList(); 317 | if (_actionSubCatFilterStorable == null) 318 | { 319 | _actionSubCatFilterStorable = new JSONStorableStringChooser("subcategory", subCategories, ActionSubCategory, ""); 320 | _actionSubCatFilterUi = Plugin.CreateScrollablePopup(_actionSubCatFilterStorable, rightSide: true); 321 | _actionSubCatFilterUi.height = _actionFilterUi.height; 322 | _actionSubCatFilterUi.labelWidth = 0f; 323 | _actionSubCatFilterUi.popup.onValueChangeHandlers += (newValue) => 324 | { 325 | ActionSubCategory = newValue; 326 | InitializeUI(); 327 | // TODO: figure out how to actually get this UI element to 328 | // show up on top of the UI elements just below it instead 329 | // of behind 330 | _actionSubCatFilterUi.popup.Toggle(); 331 | _actionSubCatFilterUi.popup.Toggle(); 332 | }; 333 | } 334 | else 335 | { 336 | _actionSubCatFilterStorable.choices = subCategories; 337 | _actionSubCatFilterUi.popup.currentValueNoCallback = ActionSubCategory; 338 | } 339 | } 340 | } 341 | } 342 | 343 | public List> GetShownBindings() 344 | { 345 | using (TimingLogger.Track("GetShowBindings()")) 346 | { 347 | 348 | var shown = new List>(); 349 | foreach (var item in _bindings) 350 | { 351 | var actionName = item.Key; 352 | Command command = _commandsByName.ContainsKey(actionName) ? _commandsByName[actionName] : null; 353 | if (command == null) 354 | { 355 | continue; 356 | } 357 | 358 | if(!ActionCategory.Equals(command.Group)) 359 | { 360 | continue; 361 | } 362 | 363 | if(!ActionSubCategory.Equals(command.SubGroup)) 364 | { 365 | continue; 366 | } 367 | 368 | shown.Add(item); 369 | } 370 | 371 | return shown; 372 | } 373 | } 374 | 375 | private void InitUIBindings() 376 | { 377 | var shownBindings = GetShownBindings(); 378 | using (TimingLogger.Track($"InitUIBindings() [count: {shownBindings.Count}]")) 379 | { 380 | // show ui elements based on filters 381 | foreach (var item in shownBindings) 382 | { 383 | var actionName = item.Key; 384 | var command = _commandsByName[actionName]; 385 | _uiBindings.Add(new UIBindingRow(this, command)); 386 | } 387 | } 388 | } 389 | 390 | private void InitUI() 391 | { 392 | using(TimingLogger.Track("InitUI()")) 393 | { 394 | InitUIHeader(); 395 | InitUIBindings(); 396 | } 397 | } 398 | } 399 | 400 | internal class UIBindingRow 401 | { 402 | 403 | public string ActionName { get; private set; } 404 | 405 | private ViewModel _model; 406 | private Command _command; 407 | private JSONStorableBool _checkboxStorable; 408 | private UIDynamicToggle _checkboxUi; 409 | private UIDynamicButton _shortcutButtonUi; 410 | 411 | private void OnCheckboxHandler(JSONStorableBool value) 412 | { 413 | var b = _model.GetKeyBinding(ActionName); 414 | if (b != null) 415 | { 416 | b.Enabled = value.val; 417 | _model.SetKeyBinding(ActionName, b); 418 | _model.Plugin.SaveBindingSettings(); 419 | } 420 | } 421 | 422 | private void OnShortcutHandler() 423 | { 424 | if (_model.IsRecording) 425 | { 426 | SuperController.LogError("Already recording", false); 427 | return; 428 | } 429 | 430 | if(_shortcutButtonUi == null) 431 | { 432 | SuperController.LogError("button ui has dissappeared"); 433 | return; 434 | } 435 | 436 | var origButtonText = _shortcutButtonUi.buttonText.text; 437 | 438 | _shortcutButtonUi.buttonText.text = "recording (press ESC to clear)"; 439 | _model.keyRecorder = new KeyRecorder((recordedChord) => 440 | { 441 | if(_shortcutButtonUi == null) 442 | { 443 | SuperController.LogError("button ui has dissappeared"); 444 | return; 445 | } 446 | var recordedChordText = recordedChord.ToString(); 447 | 448 | // if user recorded "esc" then exit out assuming clearning the 449 | // binding was what was wanted 450 | if (recordedChordText == "Escape") 451 | { 452 | _model.ClearKeyBinding(ActionName); 453 | _shortcutButtonUi.buttonText.text = ""; 454 | _checkboxStorable.SetVal(false); 455 | _model.Plugin.SaveBindingSettings(); 456 | return; 457 | } 458 | 459 | var keyChordInUse = _model.KeyBindings 460 | .Where((x) => !x.Name.Equals(ActionName)) // don't look at "us" 461 | .Count((x) => x.KeyChord.Equals(recordedChord)) + 1; // but see if the chord is used anywhere else 462 | var usageMax = recordedChord.HasAxis ? 2 : 1; 463 | 464 | if (keyChordInUse > usageMax) 465 | { 466 | _shortcutButtonUi.buttonText.text = origButtonText; 467 | SuperController.LogError("This key is already assigned to another action"); 468 | return; 469 | } 470 | 471 | _model.SetKeyBinding(ActionName, KeyBinding.Build(_model.Plugin, ActionName, recordedChord, _command)); 472 | _shortcutButtonUi.buttonText.text = recordedChordText; 473 | _model.Plugin.SaveBindingSettings(); 474 | }); 475 | 476 | 477 | _checkboxStorable.SetVal(true); 478 | } 479 | 480 | 481 | public UIBindingRow(ViewModel model, Command command) 482 | { 483 | using(var tracker = TimingLogger.Track("UIBindingRow.ctor()")) 484 | { 485 | _model = model; 486 | _command = command; 487 | ActionName = command.Name; 488 | 489 | var binding = _model.GetKeyBinding(ActionName); 490 | 491 | tracker.Log("... after binding"); 492 | 493 | // checkbox on the left 494 | _checkboxStorable = new JSONStorableBool(ActionName, binding == null ? false : binding.Enabled, OnCheckboxHandler); 495 | _checkboxUi = _model.Plugin.CreateToggle(_checkboxStorable); 496 | _checkboxUi.labelText.text = command.DisplayName; 497 | _checkboxUi.labelText.resizeTextMaxSize = _checkboxUi.labelText.fontSize; 498 | _checkboxUi.labelText.resizeTextForBestFit = true; 499 | _checkboxUi.backgroundColor = Color.clear; 500 | 501 | tracker.Log("... after checkbox create"); 502 | 503 | var shortcutText = binding == null ? "" : binding.KeyChord.ToString(); 504 | _shortcutButtonUi = _model.Plugin.CreateButton(shortcutText, rightSide: true); 505 | _shortcutButtonUi.height = _checkboxUi.height; 506 | _shortcutButtonUi.button.onClick.AddListener(OnShortcutHandler); 507 | 508 | tracker.Log("... after button create"); 509 | } 510 | } 511 | 512 | public void Destroy() 513 | { 514 | if(_checkboxUi != null) 515 | { 516 | _model.Plugin.RemoveToggle(_checkboxStorable); 517 | _model.Plugin.RemoveToggle(_checkboxUi); 518 | } 519 | if(_shortcutButtonUi != null) 520 | { 521 | _model.Plugin.RemoveButton(_shortcutButtonUi); 522 | } 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/Utils/MathUtilities.cs: -------------------------------------------------------------------------------- 1 | namespace LFE.KeyboardShortcuts 2 | { 3 | public static class MathUtilities 4 | { 5 | public static bool SameSign(float a, float b) 6 | { 7 | // I am considering 0 "neutral" and matches sign of the other param 8 | return a * b >= 0.0f; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Utils/TimingLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using LFE.KeyboardShortcuts.Extensions; 5 | 6 | namespace LFE.KeyboardShortcuts.Utils 7 | { 8 | public class TimingLogger : IDisposable 9 | { 10 | private static readonly UnityEngine.UI.Toggle PERF_MON = UserPreferences.singleton.transform 11 | .Find((p) => p.EndsWith("PerfMon Toggle")) 12 | .FirstOrDefault() 13 | ?.GetComponent(); 14 | private readonly string _identifier; 15 | private readonly Stopwatch _stopwatch; 16 | private bool _shouldLog; 17 | 18 | private TimingLogger(string identifier) 19 | { 20 | _identifier = identifier; 21 | _stopwatch = Stopwatch.StartNew(); 22 | _shouldLog = PERF_MON?.isOn ?? false; 23 | } 24 | 25 | public static TimingLogger Track(string identifier) 26 | { 27 | return new TimingLogger(identifier); 28 | } 29 | 30 | public void Log(string additional = null) 31 | { 32 | if(_shouldLog) { SuperController.LogMessage($"{_identifier} {additional} [{_stopwatch.ElapsedMilliseconds}ms]", false); } 33 | } 34 | 35 | public void Dispose() 36 | { 37 | _stopwatch.Stop(); 38 | Log(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vam/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "licenseType" : "CC BY-SA", 3 | "creatorName" : "LFE", 4 | "packageName" : "KeyboardShortcuts", 5 | "includeVersionsInReferences" : "true", 6 | "description" : "v0.0.0 Add custom keyboard shortcuts for common tasks", 7 | "credits" : "", 8 | "instructions" : "", 9 | "promotionalLink" : "https://github.com/lfe999/KeyboardShortcuts", 10 | "programVersion" : "1.19.2.2", 11 | "contentList" : [ 12 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\KeyboardShortcuts.cslist", 13 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AnimationPatternCommand.cs", 14 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AnimationSpeedChange.cs", 15 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomAdd.cs", 16 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomCommandBase.cs", 17 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomDelete.cs", 18 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomDump.cs", 19 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomHiddenToggle.cs", 20 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomPositionChange.cs", 21 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomPositionSetLerp.cs", 22 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomRotationChange.cs", 23 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomSelect.cs", 24 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomSelectNext.cs", 25 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomSelectPrev.cs", 26 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\AtomSelectTab.cs", 27 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\CameraPositionChange.cs", 28 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MouseRightClickDrag.cs", 29 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\CameraRotationChange.cs", 30 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\Command.cs", 31 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\CommandConst.cs", 32 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\CommandExecuteEventArgs.cs", 33 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ControllerRotationChange.cs", 34 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ControllerPositionChange.cs", 35 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ControllerCommandBase.cs", 36 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ControllerPositionSetLerp.cs", 37 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MouseWheelScroll.cs", 38 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ErrorLogToggle.cs", 39 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\FreezeAnimationSet.cs", 40 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\FreezeAnimationToggle.cs", 41 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\HardReset.cs", 42 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MessageLogToggle.cs", 43 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MirrorReflectionsToggle.cs", 44 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MonitorFieldOfViewChange.cs", 45 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\MsaaChange.cs", 46 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PerformanceMonitorToggle.cs", 47 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PixelLightCountChange.cs", 48 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PlayEditModeSet.cs", 49 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PlayEditModeToggle.cs", 50 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginActionCall.cs", 51 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginAdd.cs", 52 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginBoolSet.cs", 53 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginBoolToggle.cs", 54 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginFloatChange.cs", 55 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginShowUI.cs", 56 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\PluginStringChooserChange.cs", 57 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\RescanPackages.cs", 58 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\SceneLoad.cs", 59 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\SceneNew.cs", 60 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\SceneSave.cs", 61 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\ScreenShotModeOn.cs", 62 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\SoftBodyPhysicsToggle.cs", 63 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\TimeScaleChange.cs", 64 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\UIButtonTriggerCommand.cs", 65 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Commands\\WorldScaleChange.cs", 66 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\AtomExtensions.cs", 67 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\EnumerableExtensions.cs", 68 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\InputWrapper.cs", 69 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\JSONStorableExtensions.cs", 70 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\SuperControllerExtensions.cs", 71 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\TransformExtensions.cs", 72 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Extensions\\UITabSelectorExtensions.cs", 73 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\BindingEvent.cs", 74 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\CommandFactory.cs", 75 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\KeyBinding.cs", 76 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\KeyChord.cs", 77 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\KeyRecorder.cs", 78 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Models\\ViewModel.cs", 79 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Main\\Plugin.cs", 80 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Utils\\MathUtilities.cs", 81 | "Custom\\Scripts\\LFE\\KeyboardShortcuts\\src\\Utils\\TimingLogger.cs" 82 | ], 83 | "dependencies" : { 84 | } 85 | } --------------------------------------------------------------------------------