From f034a59b7b562d4f20ea9a82d23d734f94dd7180 Mon Sep 17 00:00:00 2001 From: Jarex985 <89909888+Jarex985@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:59:30 +0100 Subject: [PATCH 1/8] Change release type to Minor and fixed formatting --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f9977..29aa9e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Notes: --- -## v1.1.2.0 - Patch Release +## v1.1.2.0 - Minor Release ### Features / Improvements @@ -35,7 +35,7 @@ Notes: ### Internal / Refactor - Introduced a keybindings JSON schema version to force-refresh cached keybindings when the data format changes. -- + ## v1.0.2.0 - Patch Release > [!NOTE] > This update introduces a stable v2 identifier format for saved function bindings (v2||). From 737ac17539cfa6d8a89935f4653cd98ceb0aba6d Mon Sep 17 00:00:00 2001 From: Jarex985 Date: Sun, 8 Feb 2026 13:51:54 +0100 Subject: [PATCH 2/8] fix: modifier keys not executing properly with mouse buttons, "hold" activation modes triggered multiple times. imp: mouse wheel bindings now retrigger as long as key is held down. Signed-off-by: Jarex985 --- CHANGELOG.md | 11 +++ .../ActivationModeHandlers.cs | 33 ++++++- .../KeybindingInputExecutor.cs | 89 ++++++++++++------- .../Keybinding/KeybindingParserService.cs | 40 +++++++++ PluginCore/manifest.json | 2 +- .../ImmediatePressHandlerTests.cs | 64 ++++++++++++- .../KeybindingInputExecutorTests.cs | 76 ++++++++++++++++ .../KeybindingParserServiceTests.cs | 14 +++ 8 files changed, 289 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29aa9e8..dd1ae2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ Notes: --- +## v1.1.3.0 - Patch Release + +### Features / Improvements + +- Bindings that use mwheel_up/mwheel_down will be retriggered while key is held down. + +### Bug Fixes + +- Fixed an Issue where some modifier keys didn't properly execute when combined with a mouse button. +- Fixed an Issue where `Hold` ActivationModes triggered multiple times when a binding had a modifier key + ## v1.1.2.0 - Minor Release ### Features / Improvements diff --git a/PluginCore/Services/Keybinding/ActivationHandlers/ActivationModeHandlers.cs b/PluginCore/Services/Keybinding/ActivationHandlers/ActivationModeHandlers.cs index 721c134..1e10829 100644 --- a/PluginCore/Services/Keybinding/ActivationHandlers/ActivationModeHandlers.cs +++ b/PluginCore/Services/Keybinding/ActivationHandlers/ActivationModeHandlers.cs @@ -23,6 +23,12 @@ internal sealed class ImmediatePressHandler(Func? utcNow = null) : IAc /// private readonly ConcurrentDictionary _keyDownTimes = new(StringComparer.OrdinalIgnoreCase); + /// + /// Mouse wheel inputs are special: some Star Citizen actions are configured as ActivationMode.press but should + /// still repeat while the Stream Deck key is physically held. + /// + private readonly ConcurrentDictionary _mouseWheelHoldActions = new(StringComparer.OrdinalIgnoreCase); + public IReadOnlyCollection SupportedModes => [ ActivationMode.press, @@ -40,8 +46,9 @@ public bool Execute(ActivationExecutionContext context, IInputExecutor executor) private bool HandleKeyDown(ActivationExecutionContext context, IInputExecutor executor) { - // Clear any previous activation block for this action (new key press cycle) + // Clear any previous activation block for this action _activationBlocks.TryRemove(context.ActionName, out _); + _mouseWheelHoldActions.TryRemove(context.ActionName, out _); // Record KeyDown time for release-threshold logic (tap modes). _keyDownTimes[context.ActionName] = _utcNow(); @@ -51,9 +58,22 @@ private bool HandleKeyDown(ActivationExecutionContext context, IInputExecutor ex return true; // Ignore KeyDown, wait for KeyUp } - bool result = context.Metadata.Retriggerable - ? executor.ExecuteDown(context.Input, context.ActionName) - : executor.ExecutePressNoRepeat(context.Input); + bool shouldRepeatMouseWheel = + context.Input.Type == InputType.MouseWheel && + (context.Mode == ActivationMode.press || context.Mode == ActivationMode.press_quicker); + + bool result; + if (shouldRepeatMouseWheel) + { + _mouseWheelHoldActions[context.ActionName] = true; + result = executor.ExecuteDown(context.Input, context.ActionName); + } + else + { + result = context.Metadata.Retriggerable + ? executor.ExecuteDown(context.Input, context.ActionName) + : executor.ExecutePressNoRepeat(context.Input); + } // If MultiTapBlock is set, block subsequent OnRelease execution if (context.Metadata.MultiTapBlock > 0) @@ -71,6 +91,11 @@ private bool HandleKeyUp(ActivationExecutionContext context, IInputExecutor exec _keyDownTimes.TryRemove(context.ActionName, out DateTime keyDownTime); + if (_mouseWheelHoldActions.TryRemove(context.ActionName, out _)) + { + return executor.ExecuteUp(context.Input, context.ActionName); + } + // If OnRelease is true AND not blocked by MultiTapBlock, execute additional release press if (context.Metadata.OnRelease && !wasBlocked) { diff --git a/PluginCore/Services/Keybinding/ActivationHandlers/KeybindingInputExecutor.cs b/PluginCore/Services/Keybinding/ActivationHandlers/KeybindingInputExecutor.cs index 0a878ac..35a49ce 100644 --- a/PluginCore/Services/Keybinding/ActivationHandlers/KeybindingInputExecutor.cs +++ b/PluginCore/Services/Keybinding/ActivationHandlers/KeybindingInputExecutor.cs @@ -45,8 +45,7 @@ public bool ExecutePress(ParsedInput input) return true; case InputType.MouseButton: - VirtualKeyCode button = (VirtualKeyCode)input.Value; - ExecuteMouseButtonClick(button); + ExecuteMouseButtonClick(input.Value); return true; case InputType.MouseWheel: @@ -69,7 +68,7 @@ public bool ExecutePressNoRepeat(ParsedInput input) { if (modifiers.Length > 0) { - ExecuteModifiedKeyPress(key, modifiers); + _inputSimulator.Keyboard.DelayedModifiedKeyStroke(modifiers, key, 50); } else { @@ -80,8 +79,7 @@ public bool ExecutePressNoRepeat(ParsedInput input) return true; case InputType.MouseButton: - VirtualKeyCode button = (VirtualKeyCode)input.Value; - ExecuteMouseButtonClick(button); + ExecuteMouseButtonClick(input.Value); return true; case InputType.MouseWheel: @@ -106,8 +104,7 @@ public bool ExecuteDown(ParsedInput input, string actionKey) return ExecuteKeyboardDown(input.Value, actionKey); case InputType.MouseButton: - VirtualKeyCode button = (VirtualKeyCode)input.Value; - ExecuteMouseButtonDown(button); + ExecuteMouseButtonDown(input.Value); return true; case InputType.MouseWheel: @@ -137,8 +134,7 @@ public bool ExecuteUp(ParsedInput input, string actionKey) return ExecuteKeyboardUp(input.Value); case InputType.MouseButton: - VirtualKeyCode button = (VirtualKeyCode)input.Value; - ExecuteMouseButtonUp(button); + ExecuteMouseButtonUp(input.Value); return true; case InputType.MouseWheel: @@ -192,16 +188,6 @@ public void CancelDelayedHold(string actionKey) } } - private void ExecuteModifiedKeyPress(DirectInputKeyCode key, DirectInputKeyCode[] modifiers) - { - PressModifiers(modifiers); - Thread.Sleep(20); - _inputSimulator.Keyboard.KeyDown(key); - Thread.Sleep(30); - _inputSimulator.Keyboard.KeyUp(key); - ReleaseModifiers(modifiers); - } - private void PressModifiers(DirectInputKeyCode[] modifiers) { foreach (DirectInputKeyCode modifier in modifiers) @@ -244,14 +230,11 @@ private bool ExecuteKeyboardDown(object keyboardValue, string actionKey) if (modifiers.Length > 0) { PressModifiers(modifiers); - Timer repeatTimer = new(_ => + + foreach (DirectInputKeyCode key in keys) { - foreach (DirectInputKeyCode key in keys) - { - _inputSimulator.Keyboard.DelayedKeyPress(key, 50); - } - }, null, 0, 200); - _activationTimers[actionKey] = repeatTimer; + _inputSimulator.Keyboard.KeyDown(key); + } } else { @@ -287,14 +270,12 @@ private bool ExecuteKeyboardUp(object keyboardValue) (DirectInputKeyCode[] modifiers, DirectInputKeyCode[] keys) = ((DirectInputKeyCode[], DirectInputKeyCode[]))keyboardValue; - if (modifiers.Length == 0) + foreach (DirectInputKeyCode key in keys) { - foreach (DirectInputKeyCode key in keys) - { - _inputSimulator.Keyboard.KeyUp(key); - } + _inputSimulator.Keyboard.KeyUp(key); } - else + + if (modifiers.Length > 0) { ReleaseModifiers(modifiers); } @@ -323,6 +304,22 @@ private void CancelExistingTimer(string actionKey) #region Mouse Button Helpers + private void ExecuteMouseButtonClick(object mouseValue) + { + if (mouseValue is ValueTuple mouseWithModifiers) + { + (DirectInputKeyCode[] modifiers, VirtualKeyCode btn) = mouseWithModifiers; + PressModifiers(modifiers); + Thread.Sleep(20); + ExecuteMouseButtonClick(btn); + Thread.Sleep(50); + ReleaseModifiers(modifiers); + return; + } + + ExecuteMouseButtonClick((VirtualKeyCode)mouseValue); + } + private void ExecuteMouseButtonClick(VirtualKeyCode button) { switch (button) @@ -345,6 +342,20 @@ private void ExecuteMouseButtonClick(VirtualKeyCode button) } } + private void ExecuteMouseButtonDown(object mouseValue) + { + if (mouseValue is ValueTuple mouseWithModifiers) + { + (DirectInputKeyCode[] modifiers, VirtualKeyCode btn) = mouseWithModifiers; + PressModifiers(modifiers); + Thread.Sleep(20); + ExecuteMouseButtonDown(btn); + return; + } + + ExecuteMouseButtonDown((VirtualKeyCode)mouseValue); + } + private void ExecuteMouseButtonDown(VirtualKeyCode button) { switch (button) @@ -367,6 +378,20 @@ private void ExecuteMouseButtonDown(VirtualKeyCode button) } } + private void ExecuteMouseButtonUp(object mouseValue) + { + if (mouseValue is ValueTuple mouseWithModifiers) + { + (DirectInputKeyCode[] modifiers, VirtualKeyCode btn) = mouseWithModifiers; + ExecuteMouseButtonUp(btn); + Thread.Sleep(20); + ReleaseModifiers(modifiers); + return; + } + + ExecuteMouseButtonUp((VirtualKeyCode)mouseValue); + } + private void ExecuteMouseButtonUp(VirtualKeyCode button) { switch (button) diff --git a/PluginCore/Services/Keybinding/KeybindingParserService.cs b/PluginCore/Services/Keybinding/KeybindingParserService.cs index 1c542aa..4a693a7 100644 --- a/PluginCore/Services/Keybinding/KeybindingParserService.cs +++ b/PluginCore/Services/Keybinding/KeybindingParserService.cs @@ -68,6 +68,11 @@ public static class KeybindingParserService return mouseWheelResult; } + if (TryParseMouseButtonWithModifiers(normalized, out ParsedInputResult? mouseButtonWithModifiersResult)) + { + return mouseButtonWithModifiersResult; + } + if (TryParseMouseButton(normalized, out VirtualKeyCode mouseButton)) { return new ParsedInputResult(InputType.MouseButton, mouseButton); @@ -206,6 +211,41 @@ private static bool TryParseMouseButton(string normalized, out VirtualKeyCode bu return false; } + private static bool TryParseMouseButtonWithModifiers(string normalized, out ParsedInputResult? result) + { + result = null; + + if (!normalized.Contains('+', StringComparison.Ordinal)) + { + return false; + } + + List modifierList = []; + VirtualKeyCode? button = null; + + foreach (string token in SplitTokens(normalized)) + { + if (TryParseModifier(token, out DirectInputKeyCode modifier)) + { + modifierList.Add(modifier); + continue; + } + + if (TryParseMouseButton(token, out VirtualKeyCode mouseButton)) + { + button = mouseButton; + } + } + + if (modifierList.Count == 0 || button == null) + { + return false; + } + + result = new ParsedInputResult(InputType.MouseButton, (modifierList.ToArray(), button.Value)); + return true; + } + private static bool TryParseKeyboard(string scBinding, out DirectInputKeyCode[] modifiers, out DirectInputKeyCode[] keys) { modifiers = []; diff --git a/PluginCore/manifest.json b/PluginCore/manifest.json index 641689f..b2052fe 100644 --- a/PluginCore/manifest.json +++ b/PluginCore/manifest.json @@ -59,7 +59,7 @@ "Description": "Plugin that provides bindable actions for Star Citizen via Stream Deck.\nThis Plugin is not endorsed by or affiliated with the Cloud Imperium or Roberts Space Industries group of companies. All game content and materials are copyright Cloud Imperium Rights LLC and Cloud Imperium Rights Ltd.. Star Citizen®,Squadron 42®, Roberts Space Industries®, and Cloud Imperium® are registered trademarks of Cloud Imperium Rights LLC. All rights reserved.", "Icon": "Images/pluginIcon", "URL": "https://github.com/Jarex985/SCStreamDeck", - "Version": "1.1.2.0", + "Version": "1.1.3.0", "CodePath": "com.jarex985.scstreamdeck.exe", "Category": "Star Citizen Stream Deck", "CategoryIcon": "Images/categoryIcon", diff --git a/Tests/Unit/Services/Keybinding/ActivationHandlers/ImmediatePressHandlerTests.cs b/Tests/Unit/Services/Keybinding/ActivationHandlers/ImmediatePressHandlerTests.cs index aa7c766..b6d8cf7 100644 --- a/Tests/Unit/Services/Keybinding/ActivationHandlers/ImmediatePressHandlerTests.cs +++ b/Tests/Unit/Services/Keybinding/ActivationHandlers/ImmediatePressHandlerTests.cs @@ -6,6 +6,61 @@ namespace Tests.Unit.Services.Keybinding.ActivationHandlers; public sealed class ImmediatePressHandlerTests { + [Fact] + public void Execute_MouseWheel_Press_RepeatsWhileHeld_UsingDownUp() + { + ImmediatePressHandler handler = new(); + RecordingInputExecutor exec = new(); + + ActivationModeMetadata metadata = new() + { + OnPress = true, + OnRelease = false, + Retriggerable = false, + MultiTapBlock = 0, + ReleaseTriggerDelay = 0, + ReleaseTriggerThreshold = 0 + }; + + ActivationExecutionContext downCtx = Ctx(true, ActivationMode.press, InputType.MouseWheel, metadata); + ActivationExecutionContext upCtx = Ctx(false, ActivationMode.press, InputType.MouseWheel, metadata); + + handler.Execute(downCtx, exec).Should().BeTrue(); + handler.Execute(upCtx, exec).Should().BeTrue(); + + exec.DownCount.Should().Be(1); + exec.UpCount.Should().Be(1); + exec.PressNoRepeatCount.Should().Be(0); + } + + [Fact] + public void Execute_MouseWheel_PressQuicker_RepeatsWhileHeld_UsingDownUp_AndDoesNotPressOnRelease() + { + ImmediatePressHandler handler = new(); + RecordingInputExecutor exec = new(); + + ActivationModeMetadata metadata = new() + { + OnPress = true, + OnRelease = true, + Retriggerable = false, + MultiTapBlock = 0, + ReleaseTriggerDelay = 0, + ReleaseTriggerThreshold = 0.15f + }; + + ActivationExecutionContext downCtx = Ctx(true, ActivationMode.press_quicker, InputType.MouseWheel, metadata); + ActivationExecutionContext upCtx = Ctx(false, ActivationMode.press_quicker, InputType.MouseWheel, metadata); + + handler.Execute(downCtx, exec).Should().BeTrue(); + handler.Execute(upCtx, exec).Should().BeTrue(); + + exec.DownCount.Should().Be(1); + exec.UpCount.Should().Be(1); + exec.PressNoRepeatCount.Should().Be(0); + exec.ScheduledPresses.Should().BeEmpty(); + } + [Fact] public void Execute_OnRelease_IsBlocked_WhenMultiTapBlockWasSetOnPress() { @@ -153,12 +208,15 @@ public void Execute_OnRelease_WithReleaseTriggerThreshold_DoesNotExecute_WhenHel exec.ScheduledPresses.Should().BeEmpty(); } - private static ActivationExecutionContext Ctx(bool isKeyDown, ActivationModeMetadata metadata) => new() + private static ActivationExecutionContext Ctx(bool isKeyDown, ActivationModeMetadata metadata) => + Ctx(isKeyDown, ActivationMode.press, InputType.Keyboard, metadata); + + private static ActivationExecutionContext Ctx(bool isKeyDown, ActivationMode mode, InputType inputType, ActivationModeMetadata metadata) => new() { ActionName = "TestAction", - Input = new ParsedInput { Type = InputType.Keyboard, Value = new object() }, + Input = new ParsedInput { Type = inputType, Value = new object() }, IsKeyDown = isKeyDown, - Mode = ActivationMode.press, + Mode = mode, Metadata = metadata }; diff --git a/Tests/Unit/Services/Keybinding/ActivationHandlers/KeybindingInputExecutorTests.cs b/Tests/Unit/Services/Keybinding/ActivationHandlers/KeybindingInputExecutorTests.cs index e80af30..e7bcbb2 100644 --- a/Tests/Unit/Services/Keybinding/ActivationHandlers/KeybindingInputExecutorTests.cs +++ b/Tests/Unit/Services/Keybinding/ActivationHandlers/KeybindingInputExecutorTests.cs @@ -50,6 +50,29 @@ public void ExecutePress_Keyboard_WithModifiers_UsesDelayedModifiedKeyStroke() 50), Times.Once); } + [Fact] + public void ExecutePressNoRepeat_Keyboard_WithModifiers_UsesDelayedModifiedKeyStroke() + { + (KeybindingInputExecutor executor, _, Mock keyboard) = CreateExecutor(); + + DirectInputKeyCode[] modifiers = [DirectInputKeyCode.DikLalt]; + ParsedInput input = new() { Type = InputType.Keyboard, Value = (modifiers, new[] { DirectInputKeyCode.DikO }) }; + + keyboard + .Setup(k => k.DelayedModifiedKeyStroke( + It.Is>(m => m.SequenceEqual(modifiers)), + DirectInputKeyCode.DikO, + 50)) + .Returns(keyboard.Object); + + executor.ExecutePressNoRepeat(input).Should().BeTrue(); + + keyboard.Verify(k => k.DelayedModifiedKeyStroke( + It.Is>(m => m.SequenceEqual(modifiers)), + DirectInputKeyCode.DikO, + 50), Times.Once); + } + [Fact] public void ExecutePressNoRepeat_MouseButton_LeftButton_InvokesMouseClick() { @@ -78,6 +101,29 @@ public void ExecutePress_MouseButton_MiddleButton_InvokesMouseClick() mouse.Verify(m => m.MiddleButtonClick(), Times.Once); } + [Fact] + public void ExecutePressNoRepeat_MouseButton_WithModifiers_PressesAndReleasesModifiersAroundClick() + { + (KeybindingInputExecutor executor, Mock mouse, Mock keyboard) = CreateExecutor(); + + DirectInputKeyCode[] modifiers = [DirectInputKeyCode.DikLshift]; + ParsedInput input = new() + { + Type = InputType.MouseButton, + Value = (modifiers, VirtualKeyCode.MBUTTON) + }; + + keyboard.Setup(k => k.KeyDown(DirectInputKeyCode.DikLshift)).Returns(keyboard.Object); + keyboard.Setup(k => k.KeyUp(DirectInputKeyCode.DikLshift)).Returns(keyboard.Object); + mouse.Setup(m => m.MiddleButtonClick()).Returns(mouse.Object); + + executor.ExecutePressNoRepeat(input).Should().BeTrue(); + + keyboard.Verify(k => k.KeyDown(DirectInputKeyCode.DikLshift), Times.Once); + mouse.Verify(m => m.MiddleButtonClick(), Times.Once); + keyboard.Verify(k => k.KeyUp(DirectInputKeyCode.DikLshift), Times.Once); + } + [Fact] public void ExecuteDownUp_MouseButton_IsIdempotent_PerActionKey() { @@ -120,6 +166,36 @@ public void ExecuteDownUp_Keyboard_NoModifiers_CallsKeyDownAndUp_AndRemovesTimer keyboard.Verify(k => k.KeyUp(DirectInputKeyCode.DikF1), Times.Once); } + [Fact] + public void ExecuteDownUp_Keyboard_WithModifiers_HoldsKeyDown_NoRepeat_AndReleasesKeysThenModifiers() + { + (KeybindingInputExecutor executor, _, Mock keyboard) = CreateExecutor(); + + DirectInputKeyCode[] modifiers = [DirectInputKeyCode.DikLalt]; + DirectInputKeyCode[] keys = [DirectInputKeyCode.DikAdd]; + ParsedInput input = new() + { + Type = InputType.Keyboard, + Value = (modifiers, keys) + }; + + MockSequence seq = new(); + keyboard.InSequence(seq).Setup(k => k.KeyDown(DirectInputKeyCode.DikLalt)).Returns(keyboard.Object); + keyboard.InSequence(seq).Setup(k => k.KeyDown(DirectInputKeyCode.DikAdd)).Returns(keyboard.Object); + keyboard.InSequence(seq).Setup(k => k.KeyUp(DirectInputKeyCode.DikAdd)).Returns(keyboard.Object); + keyboard.InSequence(seq).Setup(k => k.KeyUp(DirectInputKeyCode.DikLalt)).Returns(keyboard.Object); + + executor.ExecuteDown(input, "Action").Should().BeTrue(); + executor.ExecuteUp(input, "Action").Should().BeTrue(); + + keyboard.Verify(k => k.DelayedKeyPress(It.IsAny(), It.IsAny()), Times.Never); + keyboard.Verify(k => k.KeyDown(DirectInputKeyCode.DikLalt), Times.Once); + keyboard.Verify(k => k.KeyDown(DirectInputKeyCode.DikAdd), Times.Once); + keyboard.Verify(k => k.KeyUp(DirectInputKeyCode.DikAdd), Times.Once); + keyboard.Verify(k => k.KeyUp(DirectInputKeyCode.DikLalt), Times.Once); + } + + private static (KeybindingInputExecutor Executor, Mock Mouse, Mock Keyboard) CreateExecutor() { diff --git a/Tests/Unit/Services/Keybinding/KeybindingParserServiceTests.cs b/Tests/Unit/Services/Keybinding/KeybindingParserServiceTests.cs index 296fc69..80dc274 100644 --- a/Tests/Unit/Services/Keybinding/KeybindingParserServiceTests.cs +++ b/Tests/Unit/Services/Keybinding/KeybindingParserServiceTests.cs @@ -60,6 +60,20 @@ public void ParseBinding_ParsesMouseButtons() right.Value.Should().Be(VirtualKeyCode.RBUTTON); } + [Fact] + public void ParseBinding_ParsesMouseButtonWithModifiers() + { + ParsedInputResult? result = KeybindingParserService.ParseBinding("lshift+mouse3"); + + result.Should().NotBeNull(); + result.Type.Should().Be(InputType.MouseButton); + result.Value.Should().BeOfType>(); + + (DirectInputKeyCode[] modifiers, VirtualKeyCode button) = ((DirectInputKeyCode[], VirtualKeyCode))result.Value; + modifiers.Should().ContainSingle().Which.Should().Be(DirectInputKeyCode.DikLshift); + button.Should().Be(VirtualKeyCode.MBUTTON); + } + [Fact] public void ParseBinding_ParsesKeyboardWithModifiers() { From ca19b3783b3e3d9748d2b0551d591053408d98b0 Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:07:22 +0100 Subject: [PATCH 3/8] feat: add Adaptive Dial action with shared PI function-picker helpers - Added new Stream Deck+ `Adaptive Dial` action with separate assignments for rotate-left, rotate-right, and push input. - Implemented dial action backend with dedicated dial base/action settings: - `AdaptiveDial` execution flow for rotation ticks and press/release handling - `SCDialActionBase` for shared dial initialization, PI updates, and setting migration - `DialSettings` schema for dial slot bindings and click sound path - Updated `manifest.json`: - Registered `Adaptive Dial` with `Encoder` controller metadata - Added explicit `Controllers` declarations for existing key actions - Bumped plugin version to `1.2.0.0` - Added Property Inspector support for dials: - New `Dials/AdaptiveDial.html` - New dial PI script (`pi-dial-action.js`) and source (`js/src/pi-dial-action.js`) - Moved shared function picker logic into `js/src/sc-common.js` and reused from key/dial PI scripts - Regenerated PI bundled assets (`sc-components.js`, pi-function-key.js, `pi-dial-action.js`, `base.css`) - Improved PI maintainability and UX behavior: - Centralized function flattening/details rendering helpers - Restored documentation comments/TODOs for unsupported axis filtering and details rendering semantics - Scoped dial details visibility rules to avoid cross-impact with key PIs - Expanded tests with `AdaptiveDialTests` for rotation resolution, executable binding selection, and settings serialization behavior. - Updated docs and release notes: - README.md, index.md, usage.md (including Adaptive Dial image) - CHANGELOG.md entry for `v1.2.0.0` including feature and internal refactor notes. --- CHANGELOG.md | 14 + PluginCore/ActionKeys/AdaptiveDial.cs | 176 ++++++++++ PluginCore/ActionKeys/ControlPanelKey.cs | 18 +- PluginCore/ActionKeys/SCActionBase.cs | 36 +- PluginCore/ActionKeys/SCDialActionBase.cs | 227 ++++++++++++ .../ActionKeys/Settings/DialSettings.cs | 21 ++ .../PropertyInspector/Dials/AdaptiveDial.html | 111 ++++++ PluginCore/PropertyInspector/css/base.css | 2 +- PluginCore/PropertyInspector/css/src/base.css | 23 ++ .../PropertyInspector/js/pi-dial-action.js | 2 + .../PropertyInspector/js/pi-function-key.js | 2 +- .../PropertyInspector/js/sc-components.js | 2 +- .../js/src/pi-dial-action.js | 183 ++++++++++ .../js/src/pi-function-key.js | 130 ++----- .../PropertyInspector/js/src/sc-common.js | 166 +++++++++ PluginCore/manifest.json | 36 +- README.md | 1 + Tests/Unit/ActionKeys/AdaptiveDialTests.cs | 328 ++++++++++++++++++ Tools/PiAssetsBuilder/Program.cs | 1 + docs/assets/images/adaptive-dial.png | Bin 0 -> 99653 bytes docs/index.md | 1 + docs/usage.md | 24 ++ 22 files changed, 1340 insertions(+), 164 deletions(-) create mode 100644 PluginCore/ActionKeys/AdaptiveDial.cs create mode 100644 PluginCore/ActionKeys/SCDialActionBase.cs create mode 100644 PluginCore/ActionKeys/Settings/DialSettings.cs create mode 100644 PluginCore/PropertyInspector/Dials/AdaptiveDial.html create mode 100644 PluginCore/PropertyInspector/js/pi-dial-action.js create mode 100644 PluginCore/PropertyInspector/js/src/pi-dial-action.js create mode 100644 Tests/Unit/ActionKeys/AdaptiveDialTests.cs create mode 100644 docs/assets/images/adaptive-dial.png diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1ae2b..72fd6a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ Notes: --- +## v1.2.0.0 - Minor Release + +### Features / Improvements + +- Added `Adaptive Dial` action for Stream Deck+ devices. Supports assigning separate Star Citizen functions to rotate left, rotate right, and dial push. Rotation executes the assigned function once per tick. Push respects the action's activation mode (e.g., Tap vs Hold). +- Click sound support for the `Adaptive Dial` (same `.wav`/`.mp3` configuration as `Adaptive Key`). + +### Internal / Refactor + +- `SCActionBase` and `ControlPanelKey` now inherit from `KeypadBase` instead of `KeyAndEncoderBase`, removing stub dial/touchpad overrides that were never used for key actions. +- Added explicit `Controllers` declarations (`Keypad` / `Encoder`) to all actions in `manifest.json`. +- Added unit tests for `AdaptiveDial` covering rotation resolution and executable binding resolution. +- Refactored Property Inspector function picker logic into shared helpers (`SCPI.functionPicker`) used by both key and dial scripts, reducing duplicated code. + ## v1.1.3.0 - Patch Release ### Features / Improvements diff --git a/PluginCore/ActionKeys/AdaptiveDial.cs b/PluginCore/ActionKeys/AdaptiveDial.cs new file mode 100644 index 0000000..a868c30 --- /dev/null +++ b/PluginCore/ActionKeys/AdaptiveDial.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using BarRaider.SdTools; +using BarRaider.SdTools.Payloads; +using SCStreamDeck.Common; +using SCStreamDeck.Logging; +using SCStreamDeck.Models; + +namespace SCStreamDeck.ActionKeys; + +/// +/// Adaptive Star Citizen dial. +/// Supports separate functions for rotate left, rotate right, and dial press. +/// +[SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Stream Deck action instantiated via SDK reflection")] +[PluginActionId("com.jarex985.scstreamdeck.adaptivedial")] +public sealed class AdaptiveDial(SDConnection connection, InitialPayload payload) : SCDialActionBase(connection, payload) +{ + public override async void DialRotate(DialRotatePayload payload) + { + try + { + await ProcessDialRotateAsync(payload.Ticks).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: {ex.Message}", ex); + } + } + + public override async void DialDown(DialPayload payload) + { + try + { + PlayClickSoundIfConfigured(); + await ProcessPressEventAsync(true).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: {ex.Message}", ex); + } + } + + public override async void DialUp(DialPayload payload) + { + try + { + await ProcessPressEventAsync(false).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: {ex.Message}", ex); + } + } + + private async Task ProcessDialRotateAsync(int ticks) + { + if (ticks == 0) + { + return; + } + + string? actionId = ResolveRotationFunction(Settings, ticks); + (KeybindingAction, string)? validationResult = ValidateAndResolve(actionId); + if (validationResult == null || string.IsNullOrWhiteSpace(actionId)) + { + return; + } + + int rotationSteps = Math.Abs(ticks); + string executableBinding = validationResult.Value.Item2; + + for (int i = 0; i < rotationSteps; i++) + { + bool success = await KeybindingService.ExecutePressNoRepeatAsync(actionId, executableBinding) + .ConfigureAwait(false); + if (success) + { + LogRotateExec(actionId, ticks, executableBinding); + } + } + } + + private async Task ProcessPressEventAsync(bool isKeyDown) + { + (KeybindingAction, string)? validationResult = ValidateAndResolve(Settings.PressFunction); + if (validationResult == null || string.IsNullOrWhiteSpace(Settings.PressFunction)) + { + return; + } + + (KeybindingAction action, string executableBinding) = validationResult.Value; + + KeybindingExecutionContext context = new() + { + ActionName = Settings.PressFunction, + Binding = executableBinding, + ActivationMode = action.ActivationMode, + IsKeyDown = isKeyDown + }; + + await ExecuteKeybindingAsync(context).ConfigureAwait(false); + } + + internal static string? ResolveRotationFunction(Settings.DialSettings settings, int ticks) + { + ArgumentNullException.ThrowIfNull(settings); + + return ticks switch + { + > 0 => settings.RotateRightFunction, + < 0 => settings.RotateLeftFunction, + _ => null + }; + } + + [ExcludeFromCodeCoverage] + private (KeybindingAction, string)? ValidateAndResolve(string? actionId) + { + if (string.IsNullOrWhiteSpace(actionId) || !CanExecuteBindings) + { + return null; + } + + if (!KeybindingService.TryGetAction(actionId, out KeybindingAction? action) || action == null) + { + return null; + } + + string? executableBinding = ResolveExecutableBinding(action); + return executableBinding == null ? null : (action, executableBinding); + } + + internal static string? ResolveExecutableBinding(KeybindingAction action) + { + ArgumentNullException.ThrowIfNull(action); + + if (!string.IsNullOrWhiteSpace(action.KeyboardBinding)) + { + return action.KeyboardBinding; + } + + if (string.IsNullOrWhiteSpace(action.MouseBinding)) + { + return null; + } + + InputType bindingType = action.MouseBinding.GetInputType(); + return bindingType is InputType.MouseButton or InputType.MouseWheel ? action.MouseBinding : null; + } + + private async Task ExecuteKeybindingAsync(KeybindingExecutionContext context) + { + try + { + bool success = await KeybindingService.ExecuteAsync(context).ConfigureAwait(false); + if (success) + { + LogPressExec(context); + } + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: '{context.ActionName}': {ex.Message}", ex); + } + } + + [Conditional("DEBUG")] + private void LogPressExec(KeybindingExecutionContext context) => + Log.Debug( + $"{GetType().Name}: {(context.IsKeyDown ? "pressed" : "released")} '{context.ActionName}' ({context.ActivationMode}) → '{context.Binding}'"); + + [Conditional("DEBUG")] + private static void LogRotateExec(string actionName, int ticks, string binding) => + Log.Debug($"{nameof(AdaptiveDial)}: rotated '{actionName}' ticks={ticks} → '{binding}'"); +} diff --git a/PluginCore/ActionKeys/ControlPanelKey.cs b/PluginCore/ActionKeys/ControlPanelKey.cs index a7084be..b919c87 100644 --- a/PluginCore/ActionKeys/ControlPanelKey.cs +++ b/PluginCore/ActionKeys/ControlPanelKey.cs @@ -18,7 +18,7 @@ namespace SCStreamDeck.ActionKeys; /// [SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Stream Deck action instantiated via SDK reflection")] [PluginActionId("com.jarex985.scstreamdeck.controlpanel")] -public sealed class ControlPanelKey : KeyAndEncoderBase +public sealed class ControlPanelKey : KeypadBase { private const string PiEventConnected = "propertyInspectorConnected"; private const string PiEventSetTheme = "setTheme"; @@ -67,22 +67,6 @@ public override void OnTick() { } - public override void DialRotate(DialRotatePayload payload) - { - } - - public override void DialDown(DialPayload payload) - { - } - - public override void DialUp(DialPayload payload) - { - } - - public override void TouchPress(TouchpadPressPayload payload) - { - } - public override void Dispose() { Connection.OnPropertyInspectorDidAppear -= OnPropertyInspectorDidAppear; diff --git a/PluginCore/ActionKeys/SCActionBase.cs b/PluginCore/ActionKeys/SCActionBase.cs index fdc7133..3a8ad8d 100644 --- a/PluginCore/ActionKeys/SCActionBase.cs +++ b/PluginCore/ActionKeys/SCActionBase.cs @@ -16,9 +16,9 @@ namespace SCStreamDeck.ActionKeys; /// -/// Base class for Star Citizen Stream Deck Keys and Dials. +/// Base class for Star Citizen Stream Deck key actions. /// -public abstract class SCActionBase : KeyAndEncoderBase +public abstract class SCActionBase : KeypadBase { #region Constructor and Initialization @@ -269,36 +269,4 @@ public override void OnTick() } #endregion - - #region Dial and Touchpad Methods - - /// - /// Called when the dial is rotated. Not used for keys. - /// - public override void DialRotate(DialRotatePayload payload) - { - } - - /// - /// Called when the dial is pressed down. Not used for keys. - /// - public override void DialDown(DialPayload payload) - { - } - - /// - /// Called when the dial is released. Not used for keys. - /// - public override void DialUp(DialPayload payload) - { - } - - /// - /// Called when the touchpad is pressed. Not used for keys. - /// - public override void TouchPress(TouchpadPressPayload payload) - { - } - - #endregion } diff --git a/PluginCore/ActionKeys/SCDialActionBase.cs b/PluginCore/ActionKeys/SCDialActionBase.cs new file mode 100644 index 0000000..a8e4d7a --- /dev/null +++ b/PluginCore/ActionKeys/SCDialActionBase.cs @@ -0,0 +1,227 @@ +using System.Diagnostics.CodeAnalysis; +using BarRaider.SdTools; +using BarRaider.SdTools.Events; +using BarRaider.SdTools.Payloads; +using BarRaider.SdTools.Wrappers; +using Newtonsoft.Json.Linq; +using SCStreamDeck.ActionKeys.Settings; +using SCStreamDeck.Common; +using SCStreamDeck.Infrastructure; +using SCStreamDeck.Logging; +using SCStreamDeck.Models; +using SCStreamDeck.Services.Audio; +using SCStreamDeck.Services.Core; +using SCStreamDeck.Services.Keybinding; + +namespace SCStreamDeck.ActionKeys; + +/// +/// Base class for Star Citizen Stream Deck Plus dial actions. +/// +public abstract class SCDialActionBase : EncoderBase +{ + [ExcludeFromCodeCoverage] + protected SCDialActionBase(SDConnection connection, InitialPayload payload) : base(connection, payload) + { + ArgumentNullException.ThrowIfNull(payload); + + Settings = payload.Settings is { Count: > 0 } + ? payload.Settings.ToObject() ?? new DialSettings() + : new DialSettings(); + + SCActionBaseDependencies deps = ActionDependencies.ForSCActionBase(); + InitializationService = deps.InitializationService; + KeybindingService = deps.KeybindingService; + AudioPlayerService = deps.AudioPlayerService; + + InitializationService.KeybindingsStateChanged += OnKeybindingsStateChanged; + Connection.OnPropertyInspectorDidAppear += OnPropertyInspectorDidAppear; + Connection.OnSendToPlugin += OnSendToPlugin; + + TryMigrateFunctionSettingsIfPossible(); + + if (CanExecuteBindings) + { + SendPropertyInspectorUpdate(); + } + } + + private InitializationService InitializationService { get; } + private AudioPlayerService AudioPlayerService { get; } + protected KeybindingService KeybindingService { get; } + + protected bool CanExecuteBindings => + InitializationService.IsInitialized && + KeybindingService.IsLoaded && + InitializationService.KeybindingsJsonExists(); + + protected DialSettings Settings { get; private set; } + + protected void PlayClickSoundIfConfigured() + { + if (string.IsNullOrWhiteSpace(Settings.ClickSoundPath)) + { + return; + } + + if (!File.Exists(Settings.ClickSoundPath)) + { + Settings.ClickSoundPath = null; + return; + } + + try + { + AudioPlayerService.Play(Settings.ClickSoundPath); + } + catch (Exception ex) + { + Log.Err($"[{GetType().Name}] Audio playback error: {ex.Message}", ex); + } + } + + public override void TouchPress(TouchpadPressPayload payload) + { + } + + public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) + { + } + + public override void OnTick() + { + } + + public override void Dispose() + { + Connection.OnPropertyInspectorDidAppear -= OnPropertyInspectorDidAppear; + Connection.OnSendToPlugin -= OnSendToPlugin; + InitializationService.KeybindingsStateChanged -= OnKeybindingsStateChanged; + GC.SuppressFinalize(this); + } + + [ExcludeFromCodeCoverage] + public override void ReceivedSettings(ReceivedSettingsPayload payload) + { + ArgumentNullException.ThrowIfNull(payload); + + if (payload.Settings is { Count: > 0 }) + { + Settings = payload.Settings.ToObject() ?? Settings; + } + + TryMigrateFunctionSettingsIfPossible(); + } + + private void SendPropertyInspectorUpdate() + { + try + { + if (!InitializationService.KeybindingsJsonExists() || !KeybindingService.IsLoaded) + { + Connection.SendToPropertyInspectorAsync(new JObject + { + ["functionsLoaded"] = false, + ["functions"] = new JArray() + }); + return; + } + + IReadOnlyList allActions = KeybindingService.GetAllActions(); + IntPtr hkl = KeyboardLayoutDetector.DetectCurrent().Hkl; + JArray groups = FunctionsPayloadBuilder.BuildGroupedFunctionsPayload(allActions, hkl); + + Connection.SendToPropertyInspectorAsync(new JObject { ["functionsLoaded"] = true, ["functions"] = groups }); + } + catch (Exception ex) + { + Log.Err($"[{GetType().Name}]: {ex.Message}", ex); + Connection.SendToPropertyInspectorAsync(new JObject { ["functionsLoaded"] = false, ["functions"] = new JArray() }); + } + } + + private void OnPropertyInspectorDidAppear(object? sender, SDEventReceivedEventArgs e) => + SendPropertyInspectorUpdate(); + + private void OnKeybindingsStateChanged() + { + TryMigrateFunctionSettingsIfPossible(); + SendPropertyInspectorUpdate(); + } + + private void TryMigrateFunctionSettingsIfPossible() + { + if (!CanExecuteBindings) + { + return; + } + + bool changed = false; + changed |= TryNormalizeFunctionSetting(Settings.RotateLeftFunction, value => Settings.RotateLeftFunction = value); + changed |= TryNormalizeFunctionSetting(Settings.RotateRightFunction, value => Settings.RotateRightFunction = value); + changed |= TryNormalizeFunctionSetting(Settings.PressFunction, value => Settings.PressFunction = value); + + if (!changed) + { + return; + } + + try + { + _ = Connection.SetSettingsAsync(JObject.FromObject(Settings)); + } + catch (Exception ex) + { + Log.Err($"[{GetType().Name}] Failed to persist migrated dial function ids: {ex.Message}", ex); + } + } + + private bool TryNormalizeFunctionSetting(string? currentValue, Action setValue) + { + if (string.IsNullOrWhiteSpace(currentValue)) + { + return false; + } + + if (!KeybindingService.TryNormalizeActionId(currentValue, out string normalizedId) || + string.IsNullOrWhiteSpace(normalizedId) || + string.Equals(currentValue, normalizedId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + setValue(normalizedId); + return true; + } + + [ExcludeFromCodeCoverage] + private void OnSendToPlugin(object? sender, SDEventReceivedEventArgs e) + { + try + { + if (e.Event?.Payload == null) + { + return; + } + + string? piEvent = null; + if (e.Event.Payload.TryGetValue("event", out JToken? eventToken)) + { + piEvent = eventToken.ToString(); + } + else if (e.Event.Payload.TryGetValue("property_inspector", out JToken? legacyToken)) + { + piEvent = legacyToken.ToString(); + } + + if (piEvent == "propertyInspectorConnected") + { + SendPropertyInspectorUpdate(); + } + } + catch (Exception ex) + { + Log.Err($"[{GetType().Name}] PI message handling error: {ex.Message}", ex); + } + } +} diff --git a/PluginCore/ActionKeys/Settings/DialSettings.cs b/PluginCore/ActionKeys/Settings/DialSettings.cs new file mode 100644 index 0000000..2bdc6e4 --- /dev/null +++ b/PluginCore/ActionKeys/Settings/DialSettings.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace SCStreamDeck.ActionKeys.Settings; + +/// +/// Settings schema for Stream Deck Plus dial actions. +/// +public sealed class DialSettings +{ + [JsonProperty(PropertyName = "rotateLeftFunction")] + public string? RotateLeftFunction { get; set; } + + [JsonProperty(PropertyName = "rotateRightFunction")] + public string? RotateRightFunction { get; set; } + + [JsonProperty(PropertyName = "pressFunction")] + public string? PressFunction { get; set; } + + [JsonProperty(PropertyName = "clickSoundPath")] + public string? ClickSoundPath { get; set; } +} diff --git a/PluginCore/PropertyInspector/Dials/AdaptiveDial.html b/PluginCore/PropertyInspector/Dials/AdaptiveDial.html new file mode 100644 index 0000000..624270b --- /dev/null +++ b/PluginCore/PropertyInspector/Dials/AdaptiveDial.html @@ -0,0 +1,111 @@ + + + + + + Star Citizen Stream Deck Plugin PI - Adaptive Dial + + + + + + + + +
+
+
+
+
Rotate Left
+
+

Executed once for each tick when the dial rotates left.

+
+
+
+ KEYBOARD + +
+
+ MOUSE + +
+
+ GAMEPAD + +
+
+ JOYSTICK + +
+
+
+
+ +
+
Rotate Right
+
+

Executed once for each tick when the dial rotates right.

+
+
+
+ KEYBOARD + +
+
+ MOUSE + +
+
+ GAMEPAD + +
+
+ JOYSTICK + +
+
+
+
+ +
+
Push
+
+

Uses the action activation mode while the dial is pressed and released.

+
+
+
+ KEYBOARD + +
+
+ MOUSE + +
+
+ GAMEPAD + +
+
+ JOYSTICK + +
+
+
+
+
+ +
+
+
Sound
+
+
+
+
+
+ + diff --git a/PluginCore/PropertyInspector/css/base.css b/PluginCore/PropertyInspector/css/base.css index d7656b9..299f6b3 100644 --- a/PluginCore/PropertyInspector/css/base.css +++ b/PluginCore/PropertyInspector/css/base.css @@ -1,2 +1,2 @@ /* generated: base.css */ -:root{--space-xs:4px;--space-sm:8px;--space-md:12px;--space-lg:16px;--space-xl:20px;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--frame-corner-size:12px;--frame-corner-size-sm:8px;--frame-corner-thickness:2px;--frame-corner-inset:2px;--dropdown-height:36px;--section-title-min-height:36px;--transition-fast:.15s;--transition-normal:.3s;--ease-out:cubic-bezier(.25,.46,.45,.94);--font-size-sm:12px;--font-size-md:14px;--theme-accent-rgb:94,195,202;--theme-accent-hi-rgb:154,230,236;--theme-surface-0-rgb:18,18,18;--theme-surface-1-rgb:15,15,15;--theme-surface-2-rgb:33,33,33;--theme-border-rgb:42,42,42;--color-bg-elgato:rgba(45,45,45,1);--color-bg-darker:rgba(var(--theme-surface-0-rgb),1);--color-bg-card:rgba(var(--theme-surface-1-rgb),1);--color-bg-elevated:rgba(var(--theme-surface-2-rgb),1);--color-border:rgba(var(--theme-border-rgb),1);--color-primary:rgba(var(--theme-accent-rgb),1);--color-primary-light:rgba(var(--theme-accent-hi-rgb),1);--color-primary-opacity-20:rgba(var(--theme-accent-hi-rgb),.2);--color-primary-opacity-30:rgba(var(--theme-accent-hi-rgb),.3);--color-primary-glow:rgba(var(--theme-accent-rgb),.3);--color-text-primary:rgba(255,255,255,1);--color-text-secondary:rgba(204,204,204,1);--color-text-tertiary:rgba(153,153,153,1);--color-error:rgba(244,67,54,1);--color-warning:rgba(255,193,7,1);--color-warning-glow:rgba(255,193,7,.35);--color-success:rgba(76,175,80,1);--color-success-glow:rgba(76,175,80,.55);--color-success-opacity-20:rgba(76,175,80,.2);--color-error-opacity-40:rgba(255,0,0,.4);--color-error-opacity-20:rgba(255,0,0,.2);--color-error-opacity-70:rgba(255,0,0,.7);--color-error-opacity-90:rgba(255,0,0,.9);--color-dark-opacity-40:rgba(0,0,0,.4);--color-dark-opacity-60:rgba(0,0,0,.6);--color-white-opacity-10:rgba(255,255,255,.1);--color-white-opacity-70:rgba(255,255,255,.7);--color-panel-glow:rgba(var(--theme-accent-rgb),.05);--color-inset-top:rgba(255,255,255,.02);--color-inset-bottom:rgba(0,0,0,.3);--color-glow-hover:rgba(var(--theme-accent-rgb),.4);--color-interactive-glow-hover:rgba(var(--theme-accent-hi-rgb),.85);--color-interactive-glow-active:rgba(var(--theme-accent-hi-rgb),.45)}@keyframes glow-pulse{0%,100%{opacity:1}50%{opacity:.7}}@keyframes focus-glow{0%{box-shadow:0 0 0 0 var(--color-glow-hover)}50%{box-shadow:0 0 8px 2px var(--color-glow-hover)}100%{box-shadow:0 0 4px 1px var(--color-primary-glow)}}@keyframes pi-spin{to{transform:rotate(360deg)}}@keyframes pi-loading-breathe{0%,100%{opacity:.55}50%{opacity:.95}}@keyframes pi-success-blink{0%{opacity:.15}35%{opacity:1}100%{opacity:.15}}@keyframes pi-dot{0%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-1px)}80%{opacity:.25;transform:translateY(0)}100%{opacity:.25;transform:translateY(0)}}*,*::before,*::after{box-sizing:border-box;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif}body{display:flex;justify-content:center;align-items:center;padding:var(--space-xs);margin:2px;background:var(--color-bg-elgato);color:var(--color-text-primary);overflow-x:hidden}.pi-container{width:100%;max-width:600px;position:relative;z-index:1}.pi-panel,.pi-section{position:relative;background-color:var(--color-bg-darker);background-image:linear-gradient(135deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(180deg,transparent 0%,var(--color-inset-bottom) 100%);background-repeat:no-repeat;background-size:var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),100% 100%;background-position:top left,top right,bottom left,bottom right,center center;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 0 6px var(--color-primary-glow),inset 0 0 0 1px var(--color-border),inset 0 2px 4px var(--color-dark-opacity-40)}.pi-panel::before,.pi-section::before,.pi-section__header::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--color-primary),transparent);opacity:.5}.pi-panel{padding:var(--space-xl) var(--space-sm);overflow:hidden;background-image:linear-gradient(135deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),radial-gradient(ellipse at 50% 0%,var(--color-panel-glow) 0%,transparent 60%),linear-gradient(180deg,var(--color-inset-top) 0%,transparent 20%,transparent 80%,var(--color-inset-bottom) 100%);background-size:var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),100% 100%,100% 100%;background-position:top left,top right,bottom left,bottom right,center top,center center;border-radius:var(--radius-lg);box-shadow:0 0 10px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-panel::before{height:2px}.pi-panel::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--color-primary),transparent);opacity:.5}.pi-section{padding:var(--space-lg);margin-top:var(--space-lg)}.pi-page-title{display:flex;font-weight:bold;text-transform:uppercase;letter-spacing:1px;align-content:center;justify-content:center;margin-bottom:var(--space-lg);padding-bottom:var(--space-md);color:var(--color-primary);border-bottom:1px solid var(--color-border)}.pi-dropdown__toggle,.file-picker-button,.file-picker-clear,.pi-button{cursor:pointer;position:relative;background:var(--color-bg-darker);border:1px solid var(--color-border);color:var(--color-primary);box-shadow:0 0 6px var(--color-primary-glow),inset 0 0 0 1px var(--color-border);transition:border-color var(--transition-normal) ease,box-shadow var(--transition-normal) ease,background-color var(--transition-normal) ease,transform var(--transition-fast) var(--ease-out)}.pi-dropdown__toggle::before,.file-picker-button::before,.file-picker-clear::before,.pi-button::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:1px solid var(--color-primary);border-radius:inherit;opacity:0;transition:opacity var(--transition-normal) ease}.pi-dropdown__toggle:hover,.pi-dropdown__toggle:focus-visible,.file-picker-button:hover,.file-picker-button:focus-visible,.file-picker-clear:hover,.file-picker-clear:focus-visible,.pi-button:hover,.pi-button:focus-visible{background:var(--color-bg-darker);border-color:var(--color-primary);color:var(--color-primary-light);box-shadow:0 0 10px var(--color-interactive-glow-hover)}.pi-dropdown__toggle:hover::before,.pi-dropdown__toggle:focus-visible::before,.file-picker-button:hover::before,.file-picker-button:focus-visible::before,.file-picker-clear:hover::before,.file-picker-clear:focus-visible::before,.pi-button:hover::before,.pi-button:focus-visible::before{opacity:1}.pi-dropdown__toggle:focus-visible,.file-picker-button:focus-visible,.file-picker-clear:focus-visible,.pi-button:focus-visible{outline:1px solid var(--color-primary-light);outline-offset:1px;animation:focus-glow .4s var(--ease-out) forwards}.pi-dropdown__toggle:active,.file-picker-button:active,.file-picker-clear:active,.pi-button:active{transform:scale(.98);box-shadow:0 0 4px var(--color-interactive-glow-active)}.pi-section__header,.pi-details__binding{position:relative;justify-content:center;font-weight:bold;background:linear-gradient(180deg,var(--color-bg-card) 0%,var(--color-bg-darker) 100%);border:1px solid var(--color-border);box-shadow:inset 0 1px 0 var(--color-white-opacity-10);max-height:32px}.pi-dropdown__input-wrapper::after,.pi-dropdown__toggle::after,.file-picker-button::after,.file-picker-clear::after,.pi-button::after{content:'';position:absolute;inset:var(--frame-corner-inset);pointer-events:none;border-radius:inherit;background-image:linear-gradient(135deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness));background-repeat:no-repeat;background-size:var(--frame-corner-size-sm) var(--frame-corner-size-sm);background-position:top left,top right,bottom left,bottom right;opacity:.6;transition:opacity var(--transition-normal) ease}.pi-dropdown__input-wrapper:hover::after,.pi-dropdown__input-wrapper:focus-within::after,.pi-dropdown__toggle:hover::after,.pi-dropdown__toggle:focus-visible::after,.file-picker-button:hover::after,.file-picker-button:focus-visible::after,.file-picker-clear:hover::after,.file-picker-clear:focus-visible::after,.pi-button:hover::after,.pi-button:focus-visible::after{opacity:1;animation:glow-pulse 1.5s ease-in-out infinite}.pi-button{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-sm);padding:var(--space-sm) var(--space-md);border-radius:var(--radius-sm);font-size:var(--font-size-sm);letter-spacing:.6px;text-transform:uppercase;user-select:none}.pi-button-row{display:flex;gap:var(--space-sm);margin-top:0}.pi-button-row>.pi-button{flex:1 1 0}.pi-inline-error{margin-top:var(--space-sm);font-size:var(--font-size-sm);color:var(--color-error);white-space:normal;word-break:break-word}.pi-inline-banner{margin-top:var(--space-sm);padding:var(--space-sm) var(--space-md);display:flex;align-items:center;justify-content:center;text-align:center;background:linear-gradient(180deg,var(--color-bg-card) 0%,var(--color-bg-darker) 100%);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 0 var(--color-white-opacity-10)}.pi-inline-banner .pi-inline-error{margin-top:0;text-align:center}.pi-button--danger{border-color:var(--color-error-opacity-40);color:var(--color-error-opacity-70)}.pi-button--danger::before{border-color:var(--color-error-opacity-90)}.pi-button--danger::after{background-image:linear-gradient(135deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness))}.pi-button--danger:hover,.pi-button--danger:focus-visible{border-color:var(--color-error-opacity-90);color:var(--color-error-opacity-90);box-shadow:0 0 10px var(--color-error-opacity-90)}.pi-picker-grid{display:flex;flex-direction:column;gap:var(--space-md);margin-top:var(--space-md)}.pi-picker-grid--top{gap:var(--space-xl)}.pi-section>.pi-picker-grid:first-child{margin-top:0}.pi-picker-row{display:flex;align-items:center;gap:var(--space-sm);width:100%;min-width:0;flex-wrap:nowrap}.pi-picker-label{min-width:60px;font-size:var(--font-size-sm);color:var(--color-text-tertiary);letter-spacing:.6px;font-weight:700;white-space:nowrap}.pi-picker-row .pi-dropdown{flex:1 1 auto;min-width:0;margin-bottom:0}.pi-picker-row .pi-file-picker{flex:1 1 auto;min-width:0}.pi-dropdown{position:relative;margin-bottom:var(--space-lg)}.pi-dropdown__input-row{display:flex;width:100%;gap:var(--space-xs);height:var(--dropdown-height)}.pi-dropdown__input-wrapper{flex:1;height:100%;padding:0;display:flex;align-items:center;justify-content:flex-start;position:relative;background-color:var(--color-bg-darker);border:1px solid var(--color-border);border-radius:var(--radius-md);transition:border-color var(--transition-normal) ease,box-shadow var(--transition-normal) ease,background-color var(--transition-normal) ease;box-shadow:0 0 8px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-spinner{width:12px;height:12px;display:inline-block;border-radius:50%;border:2px solid var(--color-primary-opacity-20);border-top-color:var(--color-primary);box-shadow:0 0 6px var(--color-primary-glow);animation:pi-spin .75s linear infinite}.pi-dropdown__loading{position:absolute;inset:0;display:none;align-items:center;gap:var(--space-sm);padding:0 var(--space-md);color:var(--color-text-tertiary);pointer-events:none}.pi-dropdown__loading-label{font-size:var(--font-size-md)}.pi-dropdown__loading-dots{display:inline-flex;align-items:center}.pi-dropdown__loading-dot{display:inline-block;min-width:.35em;opacity:.25;animation:pi-dot 1.2s ease-in-out infinite}.pi-dropdown__loading-dot:nth-child(2){animation-delay:.15s}.pi-dropdown__loading-dot:nth-child(3){animation-delay:.3s}.pi-dropdown--loading .pi-dropdown__loading{display:flex;animation:pi-loading-breathe 1.4s ease-in-out infinite}.pi-dropdown--error.pi-dropdown--loading .pi-dropdown__loading{color:var(--color-error);animation:none}.pi-dropdown--error.pi-dropdown--loading .pi-dropdown__loading-dots,.pi-dropdown--error.pi-dropdown--loading .pi-spinner{display:none}.pi-dropdown--success .pi-dropdown__loading{display:flex;color:var(--color-success);animation:pi-success-blink .22s ease-out 1}.pi-dropdown--loading .pi-dropdown__input-wrapper input,.pi-dropdown--success .pi-dropdown__input-wrapper input{opacity:0;pointer-events:none}.pi-dropdown--loading .pi-dropdown__toggle,.pi-dropdown--success .pi-dropdown__toggle{opacity:.65;pointer-events:none}.pi-dropdown__input-wrapper::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:1px solid var(--color-primary);border-radius:var(--radius-md);opacity:0;transition:opacity var(--transition-normal) ease}.pi-dropdown__input-wrapper:hover,.pi-dropdown__input-wrapper:focus-within{border-color:var(--color-primary);box-shadow:0 0 12px var(--color-glow-hover),inset 0 0 0 1px var(--color-primary-opacity-20)}.pi-dropdown__input-wrapper:focus-within{outline:1px solid var(--color-primary-light);outline-offset:1px;animation:focus-glow .4s var(--ease-out) forwards}.pi-dropdown__input-wrapper:hover::before,.pi-dropdown__input-wrapper:focus-within::before{opacity:1}.pi-dropdown__input-wrapper input{font-size:var(--font-size-md);font-family:inherit;width:100%;padding:0 var(--space-md);margin:0;outline:0;background:transparent;border:0;color:var(--color-text-primary)}.pi-dropdown__input-wrapper input::placeholder{color:var(--color-text-tertiary);background:transparent}.pi-dropdown__toggle{height:100%;padding:var(--space-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;border-radius:var(--radius-md);box-shadow:0 0 8px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-dropdown__arrow{transition:transform var(--transition-normal) var(--ease-out)}.pi-dropdown__arrow--open{transform:rotate(180deg)}.pi-dropdown__menu{position:absolute;top:100%;left:0;right:0;margin-top:var(--space-xs);max-height:300px;overflow-y:auto;overflow-x:hidden;z-index:1000;display:none;width:100%;background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:0 0 var(--radius-md) var(--radius-md);box-shadow:0 4px 12px var(--color-dark-opacity-60),0 0 8px var(--color-primary-glow)}.pi-dropdown__menu::-webkit-scrollbar{width:4px}.pi-dropdown__menu::-webkit-scrollbar-track{background:var(--color-border)}.pi-dropdown__menu::-webkit-scrollbar-thumb{background:var(--color-primary);border-radius:2px}.pi-dropdown__menu::-webkit-scrollbar-thumb:hover{background:var(--color-primary-light)}.pi-dropdown--open .pi-dropdown__menu{display:block}.pi-dropdown__empty-state{font-size:var(--font-size-sm);text-align:center;padding:var(--space-xs);color:var(--color-text-tertiary)}.pi-dropdown__group-header{padding:var(--space-sm) var(--space-lg);font-weight:bold;font-size:var(--font-size-md);background:repeating-linear-gradient(var(--color-bg-darker),var(--color-primary-glow) 2px);border-bottom:1px solid var(--color-border);color:var(--color-primary)}.pi-dropdown__option{padding:10px var(--space-lg);cursor:pointer;display:flex;align-items:center;width:100%;transition:background-color var(--transition-fast) ease,border-color var(--transition-fast) ease,box-shadow var(--transition-fast) ease;border-top:1px solid var(--color-primary-glow);border-left:var(--space-sm) solid transparent}.pi-dropdown__option:hover{background:var(--color-primary-glow);border-left-color:var(--color-primary);box-shadow:-5px 0 10px var(--color-primary-glow)}.pi-dropdown__option.disabled{cursor:not-allowed;opacity:.5}.pi-dropdown__option-label{flex:1}.pi-dropdown__option-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;font-size:12px;font-weight:800;margin-left:var(--space-sm);user-select:none}.pi-dropdown__option-badge--warn{color:rgba(0,0,0,.95);background:var(--color-warning);box-shadow:0 0 10px var(--color-warning-glow)}.pi-section__header{padding:var(--space-sm);margin-bottom:var(--space-lg);font-size:var(--font-size-md);display:flex;align-items:center;min-height:var(--section-title-min-height);border-radius:var(--radius-md);color:var(--color-text-primary);text-shadow:0 0 10px var(--color-primary-glow)}.pi-details__title{word-wrap:break-word;word-break:break-word;white-space:normal}.pi-details__title.pi-content-box{display:flex;align-items:center;justify-content:center;min-height:var(--section-title-min-height);font-size:var(--font-size-md);font-weight:bold;color:var(--color-primary);margin-bottom:var(--space-lg)}.pi-details__binding{display:grid;grid-template-columns:auto 1fr;align-items:center;font-size:var(--font-size-md);margin:var(--space-xs);padding:var(--space-xs);gap:var(--space-md);background:linear-gradient(180deg,var(--color-bg-darker) 0%,var(--color-bg-card) 100%);border-radius:var(--radius-sm);border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border)}.pi-details__binding-label{font-weight:bold;text-align:left;width:120px;padding:0 0 0 8px;color:var(--color-text-secondary);border-right:4px solid var(--color-border);border-left:4px solid var(--color-primary)}.pi-details__binding-value{font-weight:bold;text-align:left;color:var(--color-text-primary)}.pi-content-box{font-size:var(--font-size-sm);color:var(--color-text-primary);padding:var(--space-md);background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 0 var(--color-white-opacity-10);white-space:normal;word-break:break-word;overflow-x:hidden}.file-picker-container{display:flex;align-items:center;gap:var(--space-sm);flex:1 1 auto;min-width:0;flex-wrap:nowrap}.file-picker-button{display:flex;align-items:center;gap:var(--space-xs);padding:var(--space-sm) var(--space-md);white-space:nowrap;border-radius:var(--radius-sm)}.file-picker-button .button-icon{font-size:var(--font-size-sm);letter-spacing:.5px}.file-picker-display{flex:1;padding:var(--space-sm);min-width:0;background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:inset 0 1px 0 var(--color-white-opacity-10)}.file-picker-display .filename-text{display:block;font-size:var(--font-size-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--color-text-secondary)}.file-picker-clear{padding:var(--space-sm);font-size:var(--font-size-md);line-height:1;border-radius:var(--radius-sm);border-color:var(--color-error-opacity-40);color:var(--color-error-opacity-70)}.file-picker-clear::before{border-color:var(--color-error-opacity-90)}.file-picker-clear::after{background-image:linear-gradient(135deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness))}.file-picker-clear:hover,.file-picker-clear:focus-visible{border-color:var(--color-error-opacity-90);color:var(--color-error-opacity-90);box-shadow:0 0 10px var(--color-error-opacity-90)}.file-picker-clear:disabled,.file-picker-clear[disabled]{opacity:.45;cursor:not-allowed;pointer-events:none;transform:none;box-shadow:none} +:root{--space-xs:4px;--space-sm:8px;--space-md:12px;--space-lg:16px;--space-xl:20px;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--frame-corner-size:12px;--frame-corner-size-sm:8px;--frame-corner-thickness:2px;--frame-corner-inset:2px;--dropdown-height:36px;--section-title-min-height:36px;--transition-fast:.15s;--transition-normal:.3s;--ease-out:cubic-bezier(.25,.46,.45,.94);--font-size-sm:12px;--font-size-md:14px;--theme-accent-rgb:94,195,202;--theme-accent-hi-rgb:154,230,236;--theme-surface-0-rgb:18,18,18;--theme-surface-1-rgb:15,15,15;--theme-surface-2-rgb:33,33,33;--theme-border-rgb:42,42,42;--color-bg-elgato:rgba(45,45,45,1);--color-bg-darker:rgba(var(--theme-surface-0-rgb),1);--color-bg-card:rgba(var(--theme-surface-1-rgb),1);--color-bg-elevated:rgba(var(--theme-surface-2-rgb),1);--color-border:rgba(var(--theme-border-rgb),1);--color-primary:rgba(var(--theme-accent-rgb),1);--color-primary-light:rgba(var(--theme-accent-hi-rgb),1);--color-primary-opacity-20:rgba(var(--theme-accent-hi-rgb),.2);--color-primary-opacity-30:rgba(var(--theme-accent-hi-rgb),.3);--color-primary-glow:rgba(var(--theme-accent-rgb),.3);--color-text-primary:rgba(255,255,255,1);--color-text-secondary:rgba(204,204,204,1);--color-text-tertiary:rgba(153,153,153,1);--color-error:rgba(244,67,54,1);--color-warning:rgba(255,193,7,1);--color-warning-glow:rgba(255,193,7,.35);--color-success:rgba(76,175,80,1);--color-success-glow:rgba(76,175,80,.55);--color-success-opacity-20:rgba(76,175,80,.2);--color-error-opacity-40:rgba(255,0,0,.4);--color-error-opacity-20:rgba(255,0,0,.2);--color-error-opacity-70:rgba(255,0,0,.7);--color-error-opacity-90:rgba(255,0,0,.9);--color-dark-opacity-40:rgba(0,0,0,.4);--color-dark-opacity-60:rgba(0,0,0,.6);--color-white-opacity-10:rgba(255,255,255,.1);--color-white-opacity-70:rgba(255,255,255,.7);--color-panel-glow:rgba(var(--theme-accent-rgb),.05);--color-inset-top:rgba(255,255,255,.02);--color-inset-bottom:rgba(0,0,0,.3);--color-glow-hover:rgba(var(--theme-accent-rgb),.4);--color-interactive-glow-hover:rgba(var(--theme-accent-hi-rgb),.85);--color-interactive-glow-active:rgba(var(--theme-accent-hi-rgb),.45)}@keyframes glow-pulse{0%,100%{opacity:1}50%{opacity:.7}}@keyframes focus-glow{0%{box-shadow:0 0 0 0 var(--color-glow-hover)}50%{box-shadow:0 0 8px 2px var(--color-glow-hover)}100%{box-shadow:0 0 4px 1px var(--color-primary-glow)}}@keyframes pi-spin{to{transform:rotate(360deg)}}@keyframes pi-loading-breathe{0%,100%{opacity:.55}50%{opacity:.95}}@keyframes pi-success-blink{0%{opacity:.15}35%{opacity:1}100%{opacity:.15}}@keyframes pi-dot{0%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-1px)}80%{opacity:.25;transform:translateY(0)}100%{opacity:.25;transform:translateY(0)}}*,*::before,*::after{box-sizing:border-box;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif}body{display:flex;justify-content:center;align-items:center;padding:var(--space-xs);margin:2px;background:var(--color-bg-elgato);color:var(--color-text-primary);overflow-x:hidden}.pi-container{width:100%;max-width:600px;position:relative;z-index:1}.pi-panel,.pi-section{position:relative;background-color:var(--color-bg-darker);background-image:linear-gradient(135deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(180deg,transparent 0%,var(--color-inset-bottom) 100%);background-repeat:no-repeat;background-size:var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),100% 100%;background-position:top left,top right,bottom left,bottom right,center center;border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:0 0 6px var(--color-primary-glow),inset 0 0 0 1px var(--color-border),inset 0 2px 4px var(--color-dark-opacity-40)}.pi-panel::before,.pi-section::before,.pi-section__header::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--color-primary),transparent);opacity:.5}.pi-panel{padding:var(--space-xl) var(--space-sm);overflow:hidden;background-image:linear-gradient(135deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-30) 0,var(--color-primary-opacity-30) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),radial-gradient(ellipse at 50% 0%,var(--color-panel-glow) 0%,transparent 60%),linear-gradient(180deg,var(--color-inset-top) 0%,transparent 20%,transparent 80%,var(--color-inset-bottom) 100%);background-size:var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),var(--frame-corner-size) var(--frame-corner-size),100% 100%,100% 100%;background-position:top left,top right,bottom left,bottom right,center top,center center;border-radius:var(--radius-lg);box-shadow:0 0 10px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-panel::before{height:2px}.pi-panel::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--color-primary),transparent);opacity:.5}.pi-section{padding:var(--space-lg);margin-top:var(--space-lg)}.pi-page-title{display:flex;font-weight:bold;text-transform:uppercase;letter-spacing:1px;align-content:center;justify-content:center;margin-bottom:var(--space-lg);padding-bottom:var(--space-md);color:var(--color-primary);border-bottom:1px solid var(--color-border)}.pi-dropdown__toggle,.file-picker-button,.file-picker-clear,.pi-button{cursor:pointer;position:relative;background:var(--color-bg-darker);border:1px solid var(--color-border);color:var(--color-primary);box-shadow:0 0 6px var(--color-primary-glow),inset 0 0 0 1px var(--color-border);transition:border-color var(--transition-normal) ease,box-shadow var(--transition-normal) ease,background-color var(--transition-normal) ease,transform var(--transition-fast) var(--ease-out)}.pi-dropdown__toggle::before,.file-picker-button::before,.file-picker-clear::before,.pi-button::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:1px solid var(--color-primary);border-radius:inherit;opacity:0;transition:opacity var(--transition-normal) ease}.pi-dropdown__toggle:hover,.pi-dropdown__toggle:focus-visible,.file-picker-button:hover,.file-picker-button:focus-visible,.file-picker-clear:hover,.file-picker-clear:focus-visible,.pi-button:hover,.pi-button:focus-visible{background:var(--color-bg-darker);border-color:var(--color-primary);color:var(--color-primary-light);box-shadow:0 0 10px var(--color-interactive-glow-hover)}.pi-dropdown__toggle:hover::before,.pi-dropdown__toggle:focus-visible::before,.file-picker-button:hover::before,.file-picker-button:focus-visible::before,.file-picker-clear:hover::before,.file-picker-clear:focus-visible::before,.pi-button:hover::before,.pi-button:focus-visible::before{opacity:1}.pi-dropdown__toggle:focus-visible,.file-picker-button:focus-visible,.file-picker-clear:focus-visible,.pi-button:focus-visible{outline:1px solid var(--color-primary-light);outline-offset:1px;animation:focus-glow .4s var(--ease-out) forwards}.pi-dropdown__toggle:active,.file-picker-button:active,.file-picker-clear:active,.pi-button:active{transform:scale(.98);box-shadow:0 0 4px var(--color-interactive-glow-active)}.pi-section__header,.pi-details__binding{position:relative;justify-content:center;font-weight:bold;background:linear-gradient(180deg,var(--color-bg-card) 0%,var(--color-bg-darker) 100%);border:1px solid var(--color-border);box-shadow:inset 0 1px 0 var(--color-white-opacity-10);max-height:32px}.pi-dropdown__input-wrapper::after,.pi-dropdown__toggle::after,.file-picker-button::after,.file-picker-clear::after,.pi-button::after{content:'';position:absolute;inset:var(--frame-corner-inset);pointer-events:none;border-radius:inherit;background-image:linear-gradient(135deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-primary-opacity-20) 0,var(--color-primary-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness));background-repeat:no-repeat;background-size:var(--frame-corner-size-sm) var(--frame-corner-size-sm);background-position:top left,top right,bottom left,bottom right;opacity:.6;transition:opacity var(--transition-normal) ease}.pi-dropdown__input-wrapper:hover::after,.pi-dropdown__input-wrapper:focus-within::after,.pi-dropdown__toggle:hover::after,.pi-dropdown__toggle:focus-visible::after,.file-picker-button:hover::after,.file-picker-button:focus-visible::after,.file-picker-clear:hover::after,.file-picker-clear:focus-visible::after,.pi-button:hover::after,.pi-button:focus-visible::after{opacity:1;animation:glow-pulse 1.5s ease-in-out infinite}.pi-button{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-sm);padding:var(--space-sm) var(--space-md);border-radius:var(--radius-sm);font-size:var(--font-size-sm);letter-spacing:.6px;text-transform:uppercase;user-select:none}.pi-button-row{display:flex;gap:var(--space-sm);margin-top:0}.pi-button-row>.pi-button{flex:1 1 0}.pi-inline-error{margin-top:var(--space-sm);font-size:var(--font-size-sm);color:var(--color-error);white-space:normal;word-break:break-word}.pi-inline-banner{margin-top:var(--space-sm);padding:var(--space-sm) var(--space-md);display:flex;align-items:center;justify-content:center;text-align:center;background:linear-gradient(180deg,var(--color-bg-card) 0%,var(--color-bg-darker) 100%);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 0 var(--color-white-opacity-10)}.pi-inline-banner .pi-inline-error{margin-top:0;text-align:center}.pi-button--danger{border-color:var(--color-error-opacity-40);color:var(--color-error-opacity-70)}.pi-button--danger::before{border-color:var(--color-error-opacity-90)}.pi-button--danger::after{background-image:linear-gradient(135deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness))}.pi-button--danger:hover,.pi-button--danger:focus-visible{border-color:var(--color-error-opacity-90);color:var(--color-error-opacity-90);box-shadow:0 0 10px var(--color-error-opacity-90)}.pi-picker-grid{display:flex;flex-direction:column;gap:var(--space-md);margin-top:var(--space-md)}.pi-picker-grid--top{gap:var(--space-xl)}.pi-section>.pi-picker-grid:first-child{margin-top:0}.pi-picker-row{display:flex;align-items:center;gap:var(--space-sm);width:100%;min-width:0;flex-wrap:nowrap}.pi-picker-label{min-width:60px;font-size:var(--font-size-sm);color:var(--color-text-tertiary);letter-spacing:.6px;font-weight:700;white-space:nowrap}.pi-picker-row .pi-dropdown{flex:1 1 auto;min-width:0;margin-bottom:0}.pi-picker-row .pi-file-picker{flex:1 1 auto;min-width:0}.pi-dropdown{position:relative;margin-bottom:var(--space-lg)}.pi-dropdown__input-row{display:flex;width:100%;gap:var(--space-xs);height:var(--dropdown-height)}.pi-dropdown__input-wrapper{flex:1;height:100%;padding:0;display:flex;align-items:center;justify-content:flex-start;position:relative;background-color:var(--color-bg-darker);border:1px solid var(--color-border);border-radius:var(--radius-md);transition:border-color var(--transition-normal) ease,box-shadow var(--transition-normal) ease,background-color var(--transition-normal) ease;box-shadow:0 0 8px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-spinner{width:12px;height:12px;display:inline-block;border-radius:50%;border:2px solid var(--color-primary-opacity-20);border-top-color:var(--color-primary);box-shadow:0 0 6px var(--color-primary-glow);animation:pi-spin .75s linear infinite}.pi-dropdown__loading{position:absolute;inset:0;display:none;align-items:center;gap:var(--space-sm);padding:0 var(--space-md);color:var(--color-text-tertiary);pointer-events:none}.pi-dropdown__loading-label{font-size:var(--font-size-md)}.pi-dropdown__loading-dots{display:inline-flex;align-items:center}.pi-dropdown__loading-dot{display:inline-block;min-width:.35em;opacity:.25;animation:pi-dot 1.2s ease-in-out infinite}.pi-dropdown__loading-dot:nth-child(2){animation-delay:.15s}.pi-dropdown__loading-dot:nth-child(3){animation-delay:.3s}.pi-dropdown--loading .pi-dropdown__loading{display:flex;animation:pi-loading-breathe 1.4s ease-in-out infinite}.pi-dropdown--error.pi-dropdown--loading .pi-dropdown__loading{color:var(--color-error);animation:none}.pi-dropdown--error.pi-dropdown--loading .pi-dropdown__loading-dots,.pi-dropdown--error.pi-dropdown--loading .pi-spinner{display:none}.pi-dropdown--success .pi-dropdown__loading{display:flex;color:var(--color-success);animation:pi-success-blink .22s ease-out 1}.pi-dropdown--loading .pi-dropdown__input-wrapper input,.pi-dropdown--success .pi-dropdown__input-wrapper input{opacity:0;pointer-events:none}.pi-dropdown--loading .pi-dropdown__toggle,.pi-dropdown--success .pi-dropdown__toggle{opacity:.65;pointer-events:none}.pi-dropdown__input-wrapper::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;border:1px solid var(--color-primary);border-radius:var(--radius-md);opacity:0;transition:opacity var(--transition-normal) ease}.pi-dropdown__input-wrapper:hover,.pi-dropdown__input-wrapper:focus-within{border-color:var(--color-primary);box-shadow:0 0 12px var(--color-glow-hover),inset 0 0 0 1px var(--color-primary-opacity-20)}.pi-dropdown__input-wrapper:focus-within{outline:1px solid var(--color-primary-light);outline-offset:1px;animation:focus-glow .4s var(--ease-out) forwards}.pi-dropdown__input-wrapper:hover::before,.pi-dropdown__input-wrapper:focus-within::before{opacity:1}.pi-dropdown__input-wrapper input{font-size:var(--font-size-md);font-family:inherit;width:100%;padding:0 var(--space-md);margin:0;outline:0;background:transparent;border:0;color:var(--color-text-primary)}.pi-dropdown__input-wrapper input::placeholder{color:var(--color-text-tertiary);background:transparent}.pi-dropdown__toggle{height:100%;padding:var(--space-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;border-radius:var(--radius-md);box-shadow:0 0 8px var(--color-primary-glow),inset 0 0 0 1px var(--color-border)}.pi-dropdown__arrow{transition:transform var(--transition-normal) var(--ease-out)}.pi-dropdown__arrow--open{transform:rotate(180deg)}.pi-dropdown__menu{position:absolute;top:100%;left:0;right:0;margin-top:var(--space-xs);max-height:300px;overflow-y:auto;overflow-x:hidden;z-index:1000;display:none;width:100%;background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:0 0 var(--radius-md) var(--radius-md);box-shadow:0 4px 12px var(--color-dark-opacity-60),0 0 8px var(--color-primary-glow)}.pi-dropdown__menu::-webkit-scrollbar{width:4px}.pi-dropdown__menu::-webkit-scrollbar-track{background:var(--color-border)}.pi-dropdown__menu::-webkit-scrollbar-thumb{background:var(--color-primary);border-radius:2px}.pi-dropdown__menu::-webkit-scrollbar-thumb:hover{background:var(--color-primary-light)}.pi-dropdown--open .pi-dropdown__menu{display:block}.pi-dropdown__empty-state{font-size:var(--font-size-sm);text-align:center;padding:var(--space-xs);color:var(--color-text-tertiary)}.pi-dropdown__group-header{padding:var(--space-sm) var(--space-lg);font-weight:bold;font-size:var(--font-size-md);background:repeating-linear-gradient(var(--color-bg-darker),var(--color-primary-glow) 2px);border-bottom:1px solid var(--color-border);color:var(--color-primary)}.pi-dropdown__option{padding:10px var(--space-lg);cursor:pointer;display:flex;align-items:center;width:100%;transition:background-color var(--transition-fast) ease,border-color var(--transition-fast) ease,box-shadow var(--transition-fast) ease;border-top:1px solid var(--color-primary-glow);border-left:var(--space-sm) solid transparent}.pi-dropdown__option:hover{background:var(--color-primary-glow);border-left-color:var(--color-primary);box-shadow:-5px 0 10px var(--color-primary-glow)}.pi-dropdown__option.disabled{cursor:not-allowed;opacity:.5}.pi-dropdown__option-label{flex:1}.pi-dropdown__option-badge{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;font-size:12px;font-weight:800;margin-left:var(--space-sm);user-select:none}.pi-dropdown__option-badge--warn{color:rgba(0,0,0,.95);background:var(--color-warning);box-shadow:0 0 10px var(--color-warning-glow)}.pi-section__header{padding:var(--space-sm);margin-bottom:var(--space-lg);font-size:var(--font-size-md);display:flex;align-items:center;min-height:var(--section-title-min-height);border-radius:var(--radius-md);color:var(--color-text-primary);text-shadow:0 0 10px var(--color-primary-glow)}.pi-details__title{word-wrap:break-word;word-break:break-word;white-space:normal}.pi-details__title.pi-content-box{display:flex;align-items:center;justify-content:center;min-height:var(--section-title-min-height);font-size:var(--font-size-md);font-weight:bold;color:var(--color-primary);margin-bottom:var(--space-lg)}.pi-details__binding{display:grid;grid-template-columns:auto 1fr;align-items:center;font-size:var(--font-size-md);margin:var(--space-xs);padding:var(--space-xs);gap:var(--space-md);background:linear-gradient(180deg,var(--color-bg-darker) 0%,var(--color-bg-card) 100%);border-radius:var(--radius-sm);border-top:1px solid var(--color-border);border-bottom:1px solid var(--color-border)}.pi-details__binding-label{font-weight:bold;text-align:left;width:120px;padding:0 0 0 8px;color:var(--color-text-secondary);border-right:4px solid var(--color-border);border-left:4px solid var(--color-primary)}.pi-details__binding-value{font-weight:bold;text-align:left;color:var(--color-text-primary)}.pi-content-box{font-size:var(--font-size-sm);color:var(--color-text-primary);padding:var(--space-md);background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:inset 0 1px 0 var(--color-white-opacity-10);white-space:normal;word-break:break-word;overflow-x:hidden}.file-picker-container{display:flex;align-items:center;gap:var(--space-sm);flex:1 1 auto;min-width:0;flex-wrap:nowrap}.file-picker-button{display:flex;align-items:center;gap:var(--space-xs);padding:var(--space-sm) var(--space-md);white-space:nowrap;border-radius:var(--radius-sm)}.file-picker-button .button-icon{font-size:var(--font-size-sm);letter-spacing:.5px}.file-picker-display{flex:1;padding:var(--space-sm);min-width:0;background:linear-gradient(180deg,var(--color-bg-elevated) 0%,var(--color-bg-card) 100%);border:1px solid var(--color-border);border-radius:var(--radius-sm);box-shadow:inset 0 1px 0 var(--color-white-opacity-10)}.file-picker-display .filename-text{display:block;font-size:var(--font-size-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--color-text-secondary)}.file-picker-clear{padding:var(--space-sm);font-size:var(--font-size-md);line-height:1;border-radius:var(--radius-sm);border-color:var(--color-error-opacity-40);color:var(--color-error-opacity-70)}.file-picker-clear::before{border-color:var(--color-error-opacity-90)}.file-picker-clear::after{background-image:linear-gradient(135deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(225deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness)),linear-gradient(-45deg,var(--color-error-opacity-20) 0,var(--color-error-opacity-20) var(--frame-corner-thickness),transparent var(--frame-corner-thickness))}.file-picker-clear:hover,.file-picker-clear:focus-visible{border-color:var(--color-error-opacity-90);color:var(--color-error-opacity-90);box-shadow:0 0 10px var(--color-error-opacity-90)}.file-picker-clear:disabled,.file-picker-clear[disabled]{opacity:.45;cursor:not-allowed;pointer-events:none;transform:none;box-shadow:none}.pi-dial-inputs{display:grid;gap:12px}.pi-dial-input{display:grid;gap:6px}.pi-dial-input__hint{color:var(--sdpi-color-secondary);font-size:11px;line-height:1.4;margin:0}.pi-dial-input .pi-details{display:none} diff --git a/PluginCore/PropertyInspector/css/src/base.css b/PluginCore/PropertyInspector/css/src/base.css index f6c093c..fd8ee8b 100644 --- a/PluginCore/PropertyInspector/css/src/base.css +++ b/PluginCore/PropertyInspector/css/src/base.css @@ -902,3 +902,26 @@ body { transform: none; box-shadow: none; } + +/* === DIAL INPUTS === */ + +.pi-dial-inputs { + display: grid; + gap: 12px; +} + +.pi-dial-input { + display: grid; + gap: 6px; +} + +.pi-dial-input__hint { + color: var(--sdpi-color-secondary); + font-size: 11px; + line-height: 1.4; + margin: 0; +} + +.pi-dial-input .pi-details { + display: none; +} diff --git a/PluginCore/PropertyInspector/js/pi-dial-action.js b/PluginCore/PropertyInspector/js/pi-dial-action.js new file mode 100644 index 0000000..23a90cf --- /dev/null +++ b/PluginCore/PropertyInspector/js/pi-dial-action.js @@ -0,0 +1,2 @@ +/* generated: pi-dial-action.js */ +(function(){function o(t){const i=n?.functionPicker?.flattenFunctionsData;return typeof i!="function"?[]:i(t,{requireToggleCandidates:!1,excludeBindingTypes:["mouseaxis","joystick","gamepad"]})}function s(n){r=o(n);t.forEach(n=>{n.dropdown?.setItems?.(r),n.dropdown?.setSelectedValue?.(n.currentValue,{rerender:!1}),n.currentValue&&f(n.settingsKey,n.currentValue)})}function f(n,u){const o=t.find(t=>t.settingsKey===n);if(o){o.currentValue=u||"";const f=r.find(n=>n.value===u||n.legacyValue===u);if(!f){u||i(o,null);return}const s=f.legacyValue===u&&f.value!==u;e(n,f,{persist:s})}}function e(n,r,f={}){const e=t.find(t=>t.settingsKey===n);if(e){const o=f.persist!==!1;u=!0;e.currentValue=r.value;e.dropdown?.setSelectedValue?.(r.value,{rerender:!0});i(e,r);o&&e.setValue(r.value);setTimeout(()=>{u=!1},200)}}function i(t,i){if(t&&t.idPrefix){const u=n?.functionPicker?.updateFunctionDetails;if(typeof u=="function"){const r=t.idPrefix;u({containerEl:document.getElementById(`${r}-details-container`),titleEl:document.getElementById(`${r}-details-title`),descriptionEl:document.getElementById(`${r}-description`),bindingElements:{keyboard:document.getElementById(`${r}-binding-keyboard`),mouse:document.getElementById(`${r}-binding-mouse`),gamepad:document.getElementById(`${r}-binding-gamepad`),joystick:document.getElementById(`${r}-binding-joystick`)},hideContainerWhenEmpty:!0,showContainerWhenFilled:!0,slotLabel:t.slotLabel,selectedOption:i})}}}const n=globalThis.SCPI;n?.bus?.start?.();n?.theme?.initThemeDropdown?.();let r=[],u=!1;const t=[{rootId:"rotateLeftDropdown",settingsKey:"rotateLeftFunction",slotLabel:"Rotate Left",idPrefix:"rotateLeft"},{rootId:"rotateRightDropdown",settingsKey:"rotateRightFunction",slotLabel:"Rotate Right",idPrefix:"rotateRight"},{rootId:"pressDropdown",settingsKey:"pressFunction",slotLabel:"Push",idPrefix:"press"}].map(t=>{const i=n?.ui?.dropdown?.initDropdown?.({rootId:t.rootId,searchEnabled:!0,minLoadingMs:0,successFlashMs:100,emptyText:"No matching functions found",maxResults:50,getText:n=>String(n?.text??""),getValue:n=>String(n?.value??""),getGroup:n=>String(n?.group??""),isDisabled:n=>!!n?.disabled,onSelect:n=>e(t.settingsKey,n,{persist:!0})});i?.setLoading?.(!0,"Loading functions");const[r,o]=globalThis.SDPIComponents.useSettings(t.settingsKey,n=>{u||f(t.settingsKey,n)});return{...t,dropdown:i,getValue:r,setValue:o,currentValue:""}});n?.ui?.filePicker?.createFilePicker?.({rootId:"audioFilePicker",placeholderText:"No file selected",settingsKey:"clickSoundPath"});n?.bus?.on?.(n=>{const r=n?.functionsLoaded;r===!0&&(t.forEach(n=>{n.dropdown?.setLoading?.(!1),document.getElementById(n.rootId)?.classList.remove("pi-dropdown--error")}),s(n.functions||[]));r===!1&&t.forEach(n=>{document.getElementById(n.rootId)?.classList.add("pi-dropdown--error"),n.dropdown?.setLoading?.(!0,"No installation detected. Set custom path."),n.dropdown?.setItems?.([]),i(n,null)})});n?.util?.onDocumentReady?.(()=>{t.forEach(n=>{const t=n.getValue();n.currentValue=t||"";n.dropdown?.setSelectedValue?.(n.currentValue,{rerender:!1});i(n,null)}),n?.bus?.sendOnce?.("pi.connected","propertyInspectorConnected")})})() diff --git a/PluginCore/PropertyInspector/js/pi-function-key.js b/PluginCore/PropertyInspector/js/pi-function-key.js index ae15b67..11be196 100644 --- a/PluginCore/PropertyInspector/js/pi-function-key.js +++ b/PluginCore/PropertyInspector/js/pi-function-key.js @@ -1,2 +1,2 @@ /* generated: pi-function-key.js */ -(function(){function l(n){const t=[],i=globalThis.SCPI_REQUIRE_TOGGLE_CANDIDATES===!0;return Array.isArray(n)&&n.forEach(n=>{const r=n.label||"Other",u=n.options||[];u.forEach(n=>{const u=String(n.bindingType||"").toLowerCase();if(i){const t=n&&n.details&&n.details.isToggleCandidate===!0;if(!t)return}const e=u==="mouseaxis"||u==="joystick"||u==="gamepad";if(!e){const o=r,s=String(n.disabledReason||""),f=u==="unbound",h=!!n.disabled&&!f;t.push({value:n.value,legacyValue:n.legacyValue,text:n.text,group:o,details:n.details,bindingType:u,disabledReason:s,unbound:f,disabled:h})}})}),t}function a(n){u=l(n);t?.setItems?.(u);t?.setSelectedValue?.(i,{rerender:!1});i&&s(i)}function o(n,r={}){const u=r.persist!==!1;f=!0;i=n.value;t?.setSelectedValue?.(n.value,{rerender:!0});e(n);u&&c(n.value);setTimeout(()=>{f=!1},200)}function s(n){const t=u.find(t=>t.value===n||t.legacyValue===n);if(t){const i=t.legacyValue===n&&t.value!==n;o(t,{persist:i})}}function e(n){const i=document.querySelector(".pi-details");if(i){if(!n||!n.details){const n=i.querySelector(".pi-details__title");n&&(n.textContent="");const t=document.querySelector(".pi-description__content");t&&(t.textContent="");const r=document.getElementById("pi-details__binding-keyboard"),u=document.getElementById("pi-details__binding-mouse"),f=document.getElementById("pi-details__binding-gamepad"),e=document.getElementById("pi-details__binding-joystick");[r,u,f,e].forEach(n=>{n&&(n.textContent="")});return}const t=n.details,f=String(t.label||n.text||""),e=String(t.description||""),o=Array.isArray(t.devices)?t.devices:[],r=i.querySelector(".pi-details__title");r&&(r.textContent=f);["keyboard","mouse","gamepad","joystick"].forEach(n=>{const t=o.find(t=>t.device&&t.device.toLowerCase()===n.toLowerCase()),i=document.getElementById(`pi-details__binding-${n}`);let r="Unbound";if(t&&Array.isArray(t.bindings)&&t.bindings.length>0){const n=t.bindings.map(n=>String(n.display||n.raw||"")).filter(n=>n);n.length>0&&(r=n.join(", "))}i&&(i.textContent=r)});const u=document.querySelector(".pi-description__content");u&&(u.textContent=e||"No description available.")}}const r=globalThis.SCPI;r?.bus?.start?.();const t=r?.ui?.dropdown?.initDropdown?.({rootId:"functionDropdown",searchEnabled:!0,minLoadingMs:0,successFlashMs:100,emptyText:"No matching functions found",maxResults:50,getText:n=>String(n?.text??""),getValue:n=>String(n?.value??""),getGroup:n=>String(n?.group??""),isDisabled:n=>!!n?.disabled,onSelect:n=>o(n,{persist:!0})});t?.setLoading?.(!0,"Loading functions");let u=[],i="",f=!1;const[h,c]=globalThis.SDPIComponents.useSettings("function",n=>{f||(i=n,s(n))});r?.ui?.filePicker?.createFilePicker?.({rootId:"audioFilePicker",placeholderText:"No file selected",settingsKey:"clickSoundPath"});const n=document.getElementById("resetHoldSeconds");if(n){const i=1,e=.2,o=10,r=document.getElementById("resetHoldSecondsClear");function u(n){const t=typeof n=="number"?n:parseFloat(String(n));return Number.isFinite(t)?Math.min(o,Math.max(e,t)):i}function t(){if(r){const t=u(n.value);r.disabled=Math.abs(t-i)<.0001}}const[s,f]=globalThis.SDPIComponents.useSettings("resetHoldSeconds",i=>{const r=u(i);n.value=r.toFixed(1);t()});r&&r.addEventListener("click",()=>{n.value=i.toFixed(1),f(i),t()});n.addEventListener("input",()=>{t()});n.addEventListener("change",()=>{const i=u(n.value);n.value=i.toFixed(1);f(i);t()});n.value=u(s()).toFixed(1);t()}r?.bus?.on?.(n=>{const i=n?.functionsLoaded;i===!0&&(t?.setLoading?.(!1),document.getElementById("functionDropdown")?.classList.remove("pi-dropdown--error"),a(n.functions||[]));i===!1&&(document.getElementById("functionDropdown")?.classList.add("pi-dropdown--error"),t?.setLoading?.(!0,"No installation detected. Set custom path."),t?.setItems?.([]),e(null))});r?.util?.onDocumentReady?.(()=>{const n=h();n?i=n:e(null);t?.setSelectedValue?.(i,{rerender:!1});r?.bus?.sendOnce?.("pi.connected","propertyInspectorConnected")})})() +(function(){function l(n){const r=globalThis.SCPI_REQUIRE_TOGGLE_CANDIDATES===!0,i=t?.functionPicker?.flattenFunctionsData;return typeof i=="function"?i(n,{requireToggleCandidates:r,excludeBindingTypes:["mouseaxis","joystick","gamepad"]}):[]}function a(n){u=l(n);i?.setItems?.(u);i?.setSelectedValue?.(r,{rerender:!1});r&&s(r)}function o(n,t={}){const u=t.persist!==!1;f=!0;r=n.value;i?.setSelectedValue?.(n.value,{rerender:!0});e(n);u&&c(n.value);setTimeout(()=>{f=!1},200)}function s(n){const t=u.find(t=>t.value===n||t.legacyValue===n);if(t){const i=t.legacyValue===n&&t.value!==n;o(t,{persist:i})}}function e(n){const i=document.querySelector(".pi-details");if(i){const r=t?.functionPicker?.updateFunctionDetails;typeof r=="function"&&r({containerEl:i,titleEl:i.querySelector(".pi-details__title"),descriptionEl:document.querySelector(".pi-description__content"),bindingElements:{keyboard:document.getElementById("pi-details__binding-keyboard"),mouse:document.getElementById("pi-details__binding-mouse"),gamepad:document.getElementById("pi-details__binding-gamepad"),joystick:document.getElementById("pi-details__binding-joystick")},hideContainerWhenEmpty:!1,showContainerWhenFilled:!1,selectedOption:n})}}const t=globalThis.SCPI;t?.bus?.start?.();const i=t?.ui?.dropdown?.initDropdown?.({rootId:"functionDropdown",searchEnabled:!0,minLoadingMs:0,successFlashMs:100,emptyText:"No matching functions found",maxResults:50,getText:n=>String(n?.text??""),getValue:n=>String(n?.value??""),getGroup:n=>String(n?.group??""),isDisabled:n=>!!n?.disabled,onSelect:n=>o(n,{persist:!0})});i?.setLoading?.(!0,"Loading functions");let u=[],r="",f=!1;const[h,c]=globalThis.SDPIComponents.useSettings("function",n=>{f||(r=n,s(n))});t?.ui?.filePicker?.createFilePicker?.({rootId:"audioFilePicker",placeholderText:"No file selected",settingsKey:"clickSoundPath"});const n=document.getElementById("resetHoldSeconds");if(n){const i=1,e=.2,o=10,r=document.getElementById("resetHoldSecondsClear");function u(n){const t=typeof n=="number"?n:parseFloat(String(n));return Number.isFinite(t)?Math.min(o,Math.max(e,t)):i}function t(){if(r){const t=u(n.value);r.disabled=Math.abs(t-i)<.0001}}const[s,f]=globalThis.SDPIComponents.useSettings("resetHoldSeconds",i=>{const r=u(i);n.value=r.toFixed(1);t()});r&&r.addEventListener("click",()=>{n.value=i.toFixed(1),f(i),t()});n.addEventListener("input",()=>{t()});n.addEventListener("change",()=>{const i=u(n.value);n.value=i.toFixed(1);f(i);t()});n.value=u(s()).toFixed(1);t()}t?.bus?.on?.(n=>{const t=n?.functionsLoaded;t===!0&&(i?.setLoading?.(!1),document.getElementById("functionDropdown")?.classList.remove("pi-dropdown--error"),a(n.functions||[]));t===!1&&(document.getElementById("functionDropdown")?.classList.add("pi-dropdown--error"),i?.setLoading?.(!0,"No installation detected. Set custom path."),i?.setItems?.([]),e(null))});t?.util?.onDocumentReady?.(()=>{const n=h();n?r=n:e(null);i?.setSelectedValue?.(r,{rerender:!1});t?.bus?.sendOnce?.("pi.connected","propertyInspectorConnected")})})() diff --git a/PluginCore/PropertyInspector/js/sc-components.js b/PluginCore/PropertyInspector/js/sc-components.js index a33b554..f141e1d 100644 --- a/PluginCore/PropertyInspector/js/sc-components.js +++ b/PluginCore/PropertyInspector/js/sc-components.js @@ -1,2 +1,2 @@ /* generated: sc-components.js */ -(function(){function r(n,t){let i;return function(...r){const u=()=>{clearTimeout(i),n(...r)};clearTimeout(i);i=setTimeout(u,t)}}function u(n){document.readyState==="loading"?document.addEventListener("DOMContentLoaded",n):n()}function f(t,i={}){try{if(!n.SDPIComponents?.streamDeckClient?.send){console.warn("[sc-common] SDPIComponents.streamDeckClient.send not available");return}n.SDPIComponents.streamDeckClient.send("sendToPlugin",{event:t,...i})}catch(r){console.error("[sc-common] sendToPlugin failed",r)}}const n=globalThis,i=n.SCPI=n.SCPI||{},t=i.util=i.util||{};t.debounce=r;t.onDocumentReady=u;t.sendToPlugin=f;typeof n.debounce!="function"&&(n.debounce=r);typeof n.onDocumentReady!="function"&&(n.onDocumentReady=u);typeof n.sendToPlugin!="function"&&(n.sendToPlugin=f)})(),function(){function e(){return n.SDPIComponents?.streamDeckClient||null}function i(n,t={}){const i=e();if(!i?.send){console.warn("[sc-bus] SDPI streamDeckClient.send not available");return}i.send("sendToPlugin",{event:n,...t})}function s(n,t,u={}){const f=String(n||"").trim();if(!f){i(t,u);return}r.has(f)||(r.add(f),i(t,u))}function h(n){return typeof n!="function"?()=>{}:(t.add(n),()=>t.delete(n))}function c(){if(!u){u=!0;const n=()=>{if(!f){const i=e(),r=i?.sendToPropertyInspector?.subscribe;if(typeof r!="function"){setTimeout(n,0);return}f=!0;i.sendToPropertyInspector.subscribe(n=>{if(n?.event==="sendToPropertyInspector"&&n.payload)for(const i of Array.from(t))try{i(n.payload,n)}catch(r){console.error("[sc-bus] listener failed",r)}})}};n()}}const n=globalThis,o=n.SCPI=n.SCPI||{},t=new Set,r=new Set;let u=!1,f=!1;o.bus={start:c,on:h,send:i,sendOnce:s}}(),function(){function i(){const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("class","pi-dropdown__arrow");n.setAttribute("fill","none");n.setAttribute("height","20");n.setAttribute("width","20");n.setAttribute("xmlns","http://www.w3.org/2000/svg");const t=document.createElementNS("http://www.w3.org/2000/svg","path");return t.setAttribute("d","m5 7.5 5 5 5-5"),t.setAttribute("stroke","var(--color-primary)"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("stroke-width","2"),n.appendChild(t),n}function r(n,t={}){const s=!!n.querySelector(".pi-dropdown__search"),h=!!n.querySelector(".pi-dropdown__toggle"),c=!!n.querySelector(".pi-dropdown__menu");if(!s||!h||!c){const l=typeof t.placeholder=="string"?t.placeholder:n.getAttribute("data-placeholder")||"",r=document.createElement("div");r.className="pi-dropdown__input-row";const f=document.createElement("div");f.className="pi-dropdown__input-wrapper";const u=document.createElement("input");u.className="pi-dropdown__search";u.type="text";u.placeholder=l;f.appendChild(u);const e=document.createElement("div");e.className="pi-dropdown__toggle";e.appendChild(i());r.appendChild(f);r.appendChild(e);const o=document.createElement("div");o.className="pi-dropdown__menu";n.replaceChildren(r,o)}}function u(t={}){function l(){return u.classList.contains("pi-dropdown--open")}function lt(n){d&&(n?d.classList.add("pi-dropdown__arrow--open"):d.classList.remove("pi-dropdown__arrow--open"))}function it(n){if(h.textContent="",!Array.isArray(n)||n.length===0){const n=document.createElement("div");n.className="pi-dropdown__empty-state";n.textContent=bt;h.appendChild(n);return}if(ot){const t=new Map;for(const i of n){const n=String(ot(i)??"");t.has(n)||t.set(n,[]);t.get(n).push(i)}for(const[n,i]of t.entries()){if(n){const t=document.createElement("div");t.className="pi-dropdown__group-header";t.textContent=n;h.appendChild(t)}for(const n of i)h.appendChild(at(n))}}else for(const t of n)h.appendChild(at(t))}function at(n){const t=document.createElement("div");t.className="pi-dropdown__option";const u=!!n?.unbound,r=wt(n);r&&t.classList.add("disabled");const i=document.createElement("span");if(i.className="pi-dropdown__option-label",i.textContent=p(n),t.appendChild(i),u){const n=document.createElement("span");n.className="pi-dropdown__option-badge pi-dropdown__option-badge--warn";n.textContent="!";n.title="Unbound";t.appendChild(n)}return t.addEventListener("mousedown",()=>{tt=!0}),t.addEventListener("click",()=>{r||ti(n)}),t}function vt(n){const i=(n||"").toLowerCase().trim();let t=a.filter(n=>p(n).toLowerCase().includes(i));return t.length>ft&&(t=t.slice(0,ft)),t}function k(){if(!e)if(u.classList.add("pi-dropdown--open"),lt(!0),f){const n=(i.value||"").trim();it(n?vt(n):a)}else it(a)}function v(){u.classList.remove("pi-dropdown--open");lt(!1)}function yt(){e||(l()?v():k())}function gt(n){a=Array.isArray(n)?n:[];l()&&k()}function ni(n,t={}){if(nt=typeof n=="string"?n:"",ht){const n=a.find(n=>et(n)===nt);i.value=n?p(n):""}t.rerender&&l()&&k()}function ti(n){e||(nt=et(n),ht&&(i.value=p(n)),f&&(i.value=""),v(),st&&st(n),setTimeout(()=>{tt=!1},200))}function pt(n){if(f&&!e){const t=n?.target?.value??"";it(vt(t));!l()&&String(t).trim()&&k()}}function ii(){f&&(tt||(l()&&v(),i.value=""))}function ri(n){f||e||(n?.stopPropagation?.(),yt())}function ei(n,t="Loading"){const r=!!n;if(r===e){e&&o&&(o.textContent=String(t||"Loading"));return}if(w+=1,c&&(clearTimeout(c),c=null),r){e=!0;ct=Date.now();v();u.classList.remove("pi-dropdown--success");u.classList.add("pi-dropdown--loading");i.readOnly=!0;i.setAttribute("readonly","");i.blur?.();y.setAttribute("aria-disabled","true");o&&(o.textContent=String(t||"Loading"));return}const f=w,s=Date.now()-ct,h=Math.max(0,kt-s);c=setTimeout(()=>{f===w&&(u.classList.remove("pi-dropdown--loading"),u.classList.add("pi-dropdown--success"),o&&typeof g=="string"&&g.trim().length>0&&(o.textContent=g),c=setTimeout(()=>{f===w&&(u.classList.remove("pi-dropdown--success"),y.removeAttribute("aria-disabled"),i.readOnly=ui,fi?i.setAttribute("readonly",""):i.removeAttribute("readonly"),e=!1,c=null)},dt))},h)}const rt=t.rootId;if(!rt)return null;const u=document.getElementById(rt);if(!u)return null;r(u,{placeholder:t.placeholder});const i=u.querySelector(".pi-dropdown__search"),y=u.querySelector(".pi-dropdown__toggle"),d=u.querySelector(".pi-dropdown__arrow"),h=u.querySelector(".pi-dropdown__menu"),ut=u.querySelector(".pi-dropdown__input-wrapper");if(!i||!y||!h)return null;const f=t.searchEnabled!==!1,ft=typeof t.maxResults=="number"?t.maxResults:50,p=typeof t.getText=="function"?t.getText:n=>String(n?.text??""),et=typeof t.getValue=="function"?t.getValue:n=>String(n?.value??""),ot=typeof t.getGroup=="function"?t.getGroup:null,wt=typeof t.isDisabled=="function"?t.isDisabled:n=>!!n?.disabled,st=typeof t.onSelect=="function"?t.onSelect:null,bt=typeof t.emptyText=="string"?t.emptyText:"No items found",ht=t.displaySelectedInInput!==undefined?!!t.displaySelectedInInput:!f,kt=typeof t.minLoadingMs=="number"?t.minLoadingMs:500,dt=typeof t.successFlashMs=="number"?t.successFlashMs:220,g=typeof t.successText=="string"?t.successText:"";let a=[],nt="",tt=!1,e=!1,ct=0,w=0,c=null,s=null,o=null,b=null;if(ut){s=document.createElement("div");s.className="pi-dropdown__loading";s.setAttribute("aria-hidden","true");const n=document.createElement("span");n.className="pi-spinner";n.setAttribute("aria-hidden","true");o=document.createElement("span");o.className="pi-dropdown__loading-label";b=document.createElement("span");b.className="pi-dropdown__loading-dots";for(let n=0;n<3;n+=1){const n=document.createElement("span");n.className="pi-dropdown__loading-dot";n.textContent=".";b.appendChild(n)}s.appendChild(o);s.appendChild(b);s.appendChild(n);ut.appendChild(s)}f||(i.readOnly=!0,i.setAttribute("readonly",""));const ui=i.readOnly,fi=i.hasAttribute("readonly");if(f){const t=n.SCPI?.util?.debounce||n.debounce,r=typeof t=="function"?t(pt,150):pt;i.addEventListener("input",r);i.addEventListener("blur",ii)}else i.addEventListener("click",ri);return y.addEventListener("click",n=>{n.stopPropagation(),yt()}),document.addEventListener("click",n=>{u.contains(n.target)||(l()&&v(),f&&(i.value=""))},!0),{setItems:gt,setSelectedValue:ni,setLoading:ei}}const n=globalThis;const t=n.SCPI=n.SCPI||{};t.ui=t.ui||{};t.ui.dropdown={initDropdown:u};n.SCDropdown=n.SCDropdown||t.ui.dropdown}(),function(){function e(n){try{return decodeURIComponent(n)}catch(t){return n}}function i(n){return typeof n!="string"?"":e(String(n).replace(/^C:\\fakepath\\/i,""))}function r(n){return typeof n!="string"||n.length===0?"":n.split("\\").pop().split("/").pop()}function o(n,t={}){const c=n.querySelector(".file-picker-container");if(!c){const h=typeof t.accept=="string"?t.accept:n.getAttribute("data-accept")||"",l=typeof t.placeholderText=="string"?t.placeholderText:n.getAttribute("data-placeholder")||"No file selected",a=typeof t.buttonText=="string"?t.buttonText:n.getAttribute("data-button-text")||"FILE",v=typeof t.selectTitle=="string"?t.selectTitle:n.getAttribute("data-select-title")||"Select file",y=typeof t.clearTitle=="string"?t.clearTitle:n.getAttribute("data-clear-title")||"Clear",i=document.createElement("div");i.className="file-picker-container";const f=document.createElement("input");f.type="file";f.style.display="none";h&&f.setAttribute("accept",h);const e=document.createElement("div");e.className="file-picker-display";const o=document.createElement("span");o.className="filename-text";o.textContent=l;e.appendChild(o);const u=document.createElement("button");u.className="file-picker-button";u.type="button";u.title=v;const s=document.createElement("span");s.className="button-icon";s.textContent=a;u.appendChild(s);const r=document.createElement("button");r.className="file-picker-clear";r.type="button";r.title=y;r.disabled=!0;r.textContent="X";i.appendChild(f);i.appendChild(e);i.appendChild(u);i.appendChild(r);n.replaceChildren(i)}}function u(t={}){function p(n){const t=typeof n=="string"&&n.length>0,i=t?rt==="full"?n:r(n):it;a.textContent=i;a.title=t?n:"";c.disabled=!t}function f(n,t={}){const r=t.persist!==!1,u=!!t.silent,i=typeof n=="string"?n:"";if(s=i,p(s),!u&&b)try{b(s)}catch(f){}if(r&&y){v=!0;try{y(i.length>0?i:null)}finally{setTimeout(()=>{v=!1},50)}}}function nt(){e.value="";f("",{persist:!0})}const w=t.rootId,u=w?document.getElementById(w):null;if(!u)return null;o(u,t);const tt=t.filenameSelector||".filename-text",it=t.placeholderText||"No file selected",h=t.settingsKey,rt=t.displayMode||"basename",b=typeof t.onValueChanged=="function"?t.onValueChanged:null,k=typeof t.initialValue=="string"?t.initialValue:"",e=u.querySelector('input[type="file"]'),d=u.querySelector(".file-picker-button"),c=u.querySelector(".file-picker-clear"),l=u.querySelector(".file-picker-display"),a=l?l.querySelector(tt):null;if(!e||!d||!c||!l||!a)return null;let s="",v=!1,y=null,g=null;return d.addEventListener("click",()=>{e.click()}),e.addEventListener("change",n=>{const t=n?.target?.files?.[0],u=n?.target?.value||"",e=i(u),r=t?.path||e;if(r){f(r,{persist:!0});return}t?.name&&p(t.name)}),c.addEventListener("click",()=>{nt()}),typeof h=="string"&&h.length>0&&n.SDPIComponents?.useSettings&&([g,y]=n.SDPIComponents.useSettings(h,n=>{v||f(typeof n=="string"?n:"",{persist:!1})}),Promise.resolve(g()).then(n=>{f(typeof n=="string"?n:"",{persist:!1})}).catch(()=>{})),p(""),k&&f(k,{persist:!1,silent:!0}),{setValue:f,clear:nt,getValue:()=>s}}function f(t={}){function y(n){const t=typeof n=="string"&&n.length>0,i=t?et==="full"?n:r(n):ft;l.textContent=i;l.title=t?n:"";c.disabled=!t}function f(n,t={}){const r=t.persist!==!1,u=!!t.silent,i=typeof n=="string"?n:"";if(s=i,y(s),!u&&p)try{p(s)}catch(f){}if(r&&v){a=!0;try{v(i.length>0?i:null)}finally{setTimeout(()=>{a=!1},50)}}}function g(){e.value="";f("",{persist:!0})}if(typeof t.rootId=="string"&&t.rootId.length>0)return u(t);const nt=t.inputId,tt=t.buttonId,it=t.clearId,rt=t.displayId,ut=t.filenameSelector||".filename-text",ft=t.placeholderText||"No file selected",h=t.settingsKey,et=t.displayMode||"basename",p=typeof t.onValueChanged=="function"?t.onValueChanged:null,w=typeof t.initialValue=="string"?t.initialValue:"",e=document.getElementById(nt),b=document.getElementById(tt),c=document.getElementById(it),o=document.getElementById(rt),l=o?o.querySelector(ut):null;if(!e||!b||!c||!o||!l)return null;const ot=document.createElement("div"),k=o.closest(".file-picker-container");k&&ot.appendChild(k.cloneNode(!0));let s="",a=!1,v=null,d=null;return b.addEventListener("click",()=>{e.click()}),e.addEventListener("change",n=>{const t=n?.target?.files?.[0],u=n?.target?.value||"",e=i(u),r=t?.path||e;if(r){f(r,{persist:!0});return}t?.name&&y(t.name)}),c.addEventListener("click",()=>{g()}),typeof h=="string"&&h.length>0&&n.SDPIComponents?.useSettings&&([d,v]=n.SDPIComponents.useSettings(h,n=>{a||f(typeof n=="string"?n:"",{persist:!1})}),Promise.resolve(d()).then(n=>{f(typeof n=="string"?n:"",{persist:!1})}).catch(()=>{})),y(""),w&&f(w,{persist:!1,silent:!0}),{setValue:f,clear:g,getValue:()=>s}}const n=globalThis;const t=n.SCPI=n.SCPI||{};t.ui=t.ui||{};t.ui.filePicker={createFilePicker:u,initFilePicker:f};n.SCFilePicker=n.SCFilePicker||{initFilePicker:f}}(),function(){function i(n,t){if(n&&typeof n=="string"){t&&(t.href=`../css/themes/${n}`);try{localStorage.setItem(r,n)}catch(i){}}}function u(r={}){const o=r.rootId||"themeDropdown",s=r.linkId||"pi-theme-styles",u=document.getElementById(s);if(u){let e=[];n.bus?.start?.();const f=n.ui?.dropdown?.initDropdown?.({rootId:o,searchEnabled:!1,displaySelectedInInput:!0,minLoadingMs:500,successFlashMs:220,emptyText:"No themes found",getText:n=>String(n?.name??""),getValue:n=>String(n?.file??""),onSelect:r=>{const f=String(r?.file??"");f&&(i(f,u),(n.bus?.send||n.util?.sendToPlugin||t.sendToPlugin)?.("setTheme",{themeFile:f}))}});f?.setLoading?.(!0,"Loading themes");n.bus?.on?.(n=>{if(n&&(n.themesLoaded&&(e=n.themes||[],f?.setItems?.(e),f?.setLoading?.(!1)),typeof n.selectedTheme=="string"&&n.selectedTheme.length>0)){const t=n.selectedTheme;i(t,u);f?.setSelectedValue?.(t,{rerender:!0})}});try{const t=u.getAttribute("href")||"",n=t.split("/").pop().split("?")[0];n&&(i(n,u),f?.setSelectedValue?.(n,{rerender:!0}))}catch(h){}}}const t=globalThis,n=t.SCPI=t.SCPI||{},r="scsd.selectedTheme";n.theme={initThemeDropdown:u};t.SCTheme=t.SCTheme||n.theme}() +(function(){function r(n,t){let i;return function(...r){const u=()=>{clearTimeout(i),n(...r)};clearTimeout(i);i=setTimeout(u,t)}}function u(n){document.readyState==="loading"?document.addEventListener("DOMContentLoaded",n):n()}function f(t,i={}){try{if(!n.SDPIComponents?.streamDeckClient?.send){console.warn("[sc-common] SDPIComponents.streamDeckClient.send not available");return}n.SDPIComponents.streamDeckClient.send("sendToPlugin",{event:t,...i})}catch(r){console.error("[sc-common] sendToPlugin failed",r)}}const n=globalThis,i=n.SCPI=n.SCPI||{},t=i.util=i.util||{};t.debounce=r;t.onDocumentReady=u;t.sendToPlugin=f;typeof n.debounce!="function"&&(n.debounce=r);typeof n.onDocumentReady!="function"&&(n.onDocumentReady=u);typeof n.sendToPlugin!="function"&&(n.sendToPlugin=f)})(),function(){function r(n,t={}){const i=[],r=t.requireToggleCandidates===!0,u=new Set((t.excludeBindingTypes||[]).map(n=>String(n||"").toLowerCase()));return Array.isArray(n)?(n.forEach(n=>{const t=n?.label||"Other",f=Array.isArray(n?.options)?n.options:[];f.forEach(n=>{const f=String(n?.bindingType||"").toLowerCase();if(r){const t=n&&n.details&&n.details.isToggleCandidate===!0;if(!t)return}if(!u.has(f)){const o=String(n?.disabledReason||""),e=f==="unbound",s=!!n?.disabled&&!e;i.push({value:n?.value,legacyValue:n?.legacyValue,text:n?.text,group:t,details:n?.details,bindingType:f,disabledReason:o,unbound:e,disabled:s})}})}),i):i}function u(n={}){const t=n.containerEl||null,i=n.titleEl||null,r=n.descriptionEl||null,e=n.bindingElements||{},c=n.hideContainerWhenEmpty===!0,l=n.showContainerWhenFilled===!0,u=n.selectedOption||null,o=String(n.slotLabel||"").trim(),s=["keyboard","mouse","gamepad","joystick"];if(!u||!u.details){c&&t&&(t.style.display="none");i&&(i.textContent="");r&&(r.textContent="");s.forEach(n=>{const t=e[n]||null;t&&(t.textContent="")});return}l&&t&&(t.style.display="block");const f=u.details,h=String(f.label||u.text||""),a=String(f.description||""),v=Array.isArray(f.devices)?f.devices:[];i&&(i.textContent=o.length>0?`${o}: ${h}`:h);s.forEach(n=>{const t=v.find(t=>t.device&&t.device.toLowerCase()===n),i=e[n]||null;let r="Unbound";if(t&&Array.isArray(t.bindings)&&t.bindings.length>0){const n=t.bindings.map(n=>String(n.display||n.raw||"")).filter(n=>n);n.length>0&&(r=n.join(", "))}i&&(i.textContent=r)});r&&(r.textContent=a||"No description available.")}const n=globalThis,t=n.SCPI=n.SCPI||{},i=t.functionPicker=t.functionPicker||{};i.flattenFunctionsData=r;i.updateFunctionDetails=u}(),function(){function e(){return n.SDPIComponents?.streamDeckClient||null}function i(n,t={}){const i=e();if(!i?.send){console.warn("[sc-bus] SDPI streamDeckClient.send not available");return}i.send("sendToPlugin",{event:n,...t})}function s(n,t,u={}){const f=String(n||"").trim();if(!f){i(t,u);return}r.has(f)||(r.add(f),i(t,u))}function h(n){return typeof n!="function"?()=>{}:(t.add(n),()=>t.delete(n))}function c(){if(!u){u=!0;const n=()=>{if(!f){const i=e(),r=i?.sendToPropertyInspector?.subscribe;if(typeof r!="function"){setTimeout(n,0);return}f=!0;i.sendToPropertyInspector.subscribe(n=>{if(n?.event==="sendToPropertyInspector"&&n.payload)for(const i of Array.from(t))try{i(n.payload,n)}catch(r){console.error("[sc-bus] listener failed",r)}})}};n()}}const n=globalThis,o=n.SCPI=n.SCPI||{},t=new Set,r=new Set;let u=!1,f=!1;o.bus={start:c,on:h,send:i,sendOnce:s}}(),function(){function i(){const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("class","pi-dropdown__arrow");n.setAttribute("fill","none");n.setAttribute("height","20");n.setAttribute("width","20");n.setAttribute("xmlns","http://www.w3.org/2000/svg");const t=document.createElementNS("http://www.w3.org/2000/svg","path");return t.setAttribute("d","m5 7.5 5 5 5-5"),t.setAttribute("stroke","var(--color-primary)"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("stroke-width","2"),n.appendChild(t),n}function r(n,t={}){const s=!!n.querySelector(".pi-dropdown__search"),h=!!n.querySelector(".pi-dropdown__toggle"),c=!!n.querySelector(".pi-dropdown__menu");if(!s||!h||!c){const l=typeof t.placeholder=="string"?t.placeholder:n.getAttribute("data-placeholder")||"",r=document.createElement("div");r.className="pi-dropdown__input-row";const f=document.createElement("div");f.className="pi-dropdown__input-wrapper";const u=document.createElement("input");u.className="pi-dropdown__search";u.type="text";u.placeholder=l;f.appendChild(u);const e=document.createElement("div");e.className="pi-dropdown__toggle";e.appendChild(i());r.appendChild(f);r.appendChild(e);const o=document.createElement("div");o.className="pi-dropdown__menu";n.replaceChildren(r,o)}}function u(t={}){function l(){return u.classList.contains("pi-dropdown--open")}function lt(n){d&&(n?d.classList.add("pi-dropdown__arrow--open"):d.classList.remove("pi-dropdown__arrow--open"))}function it(n){if(h.textContent="",!Array.isArray(n)||n.length===0){const n=document.createElement("div");n.className="pi-dropdown__empty-state";n.textContent=bt;h.appendChild(n);return}if(ot){const t=new Map;for(const i of n){const n=String(ot(i)??"");t.has(n)||t.set(n,[]);t.get(n).push(i)}for(const[n,i]of t.entries()){if(n){const t=document.createElement("div");t.className="pi-dropdown__group-header";t.textContent=n;h.appendChild(t)}for(const n of i)h.appendChild(at(n))}}else for(const t of n)h.appendChild(at(t))}function at(n){const t=document.createElement("div");t.className="pi-dropdown__option";const u=!!n?.unbound,r=wt(n);r&&t.classList.add("disabled");const i=document.createElement("span");if(i.className="pi-dropdown__option-label",i.textContent=p(n),t.appendChild(i),u){const n=document.createElement("span");n.className="pi-dropdown__option-badge pi-dropdown__option-badge--warn";n.textContent="!";n.title="Unbound";t.appendChild(n)}return t.addEventListener("mousedown",()=>{tt=!0}),t.addEventListener("click",()=>{r||ti(n)}),t}function vt(n){const i=(n||"").toLowerCase().trim();let t=a.filter(n=>p(n).toLowerCase().includes(i));return t.length>ft&&(t=t.slice(0,ft)),t}function k(){if(!e)if(u.classList.add("pi-dropdown--open"),lt(!0),f){const n=(i.value||"").trim();it(n?vt(n):a)}else it(a)}function v(){u.classList.remove("pi-dropdown--open");lt(!1)}function yt(){e||(l()?v():k())}function gt(n){a=Array.isArray(n)?n:[];l()&&k()}function ni(n,t={}){if(nt=typeof n=="string"?n:"",ht){const n=a.find(n=>et(n)===nt);i.value=n?p(n):""}t.rerender&&l()&&k()}function ti(n){e||(nt=et(n),ht&&(i.value=p(n)),f&&(i.value=""),v(),st&&st(n),setTimeout(()=>{tt=!1},200))}function pt(n){if(f&&!e){const t=n?.target?.value??"";it(vt(t));!l()&&String(t).trim()&&k()}}function ii(){f&&(tt||(l()&&v(),i.value=""))}function ri(n){f||e||(n?.stopPropagation?.(),yt())}function ei(n,t="Loading"){const r=!!n;if(r===e){e&&o&&(o.textContent=String(t||"Loading"));return}if(w+=1,c&&(clearTimeout(c),c=null),r){e=!0;ct=Date.now();v();u.classList.remove("pi-dropdown--success");u.classList.add("pi-dropdown--loading");i.readOnly=!0;i.setAttribute("readonly","");i.blur?.();y.setAttribute("aria-disabled","true");o&&(o.textContent=String(t||"Loading"));return}const f=w,s=Date.now()-ct,h=Math.max(0,kt-s);c=setTimeout(()=>{f===w&&(u.classList.remove("pi-dropdown--loading"),u.classList.add("pi-dropdown--success"),o&&typeof g=="string"&&g.trim().length>0&&(o.textContent=g),c=setTimeout(()=>{f===w&&(u.classList.remove("pi-dropdown--success"),y.removeAttribute("aria-disabled"),i.readOnly=ui,fi?i.setAttribute("readonly",""):i.removeAttribute("readonly"),e=!1,c=null)},dt))},h)}const rt=t.rootId;if(!rt)return null;const u=document.getElementById(rt);if(!u)return null;r(u,{placeholder:t.placeholder});const i=u.querySelector(".pi-dropdown__search"),y=u.querySelector(".pi-dropdown__toggle"),d=u.querySelector(".pi-dropdown__arrow"),h=u.querySelector(".pi-dropdown__menu"),ut=u.querySelector(".pi-dropdown__input-wrapper");if(!i||!y||!h)return null;const f=t.searchEnabled!==!1,ft=typeof t.maxResults=="number"?t.maxResults:50,p=typeof t.getText=="function"?t.getText:n=>String(n?.text??""),et=typeof t.getValue=="function"?t.getValue:n=>String(n?.value??""),ot=typeof t.getGroup=="function"?t.getGroup:null,wt=typeof t.isDisabled=="function"?t.isDisabled:n=>!!n?.disabled,st=typeof t.onSelect=="function"?t.onSelect:null,bt=typeof t.emptyText=="string"?t.emptyText:"No items found",ht=t.displaySelectedInInput!==undefined?!!t.displaySelectedInInput:!f,kt=typeof t.minLoadingMs=="number"?t.minLoadingMs:500,dt=typeof t.successFlashMs=="number"?t.successFlashMs:220,g=typeof t.successText=="string"?t.successText:"";let a=[],nt="",tt=!1,e=!1,ct=0,w=0,c=null,s=null,o=null,b=null;if(ut){s=document.createElement("div");s.className="pi-dropdown__loading";s.setAttribute("aria-hidden","true");const n=document.createElement("span");n.className="pi-spinner";n.setAttribute("aria-hidden","true");o=document.createElement("span");o.className="pi-dropdown__loading-label";b=document.createElement("span");b.className="pi-dropdown__loading-dots";for(let n=0;n<3;n+=1){const n=document.createElement("span");n.className="pi-dropdown__loading-dot";n.textContent=".";b.appendChild(n)}s.appendChild(o);s.appendChild(b);s.appendChild(n);ut.appendChild(s)}f||(i.readOnly=!0,i.setAttribute("readonly",""));const ui=i.readOnly,fi=i.hasAttribute("readonly");if(f){const t=n.SCPI?.util?.debounce||n.debounce,r=typeof t=="function"?t(pt,150):pt;i.addEventListener("input",r);i.addEventListener("blur",ii)}else i.addEventListener("click",ri);return y.addEventListener("click",n=>{n.stopPropagation(),yt()}),document.addEventListener("click",n=>{u.contains(n.target)||(l()&&v(),f&&(i.value=""))},!0),{setItems:gt,setSelectedValue:ni,setLoading:ei}}const n=globalThis;const t=n.SCPI=n.SCPI||{};t.ui=t.ui||{};t.ui.dropdown={initDropdown:u};n.SCDropdown=n.SCDropdown||t.ui.dropdown}(),function(){function e(n){try{return decodeURIComponent(n)}catch(t){return n}}function i(n){return typeof n!="string"?"":e(String(n).replace(/^C:\\fakepath\\/i,""))}function r(n){return typeof n!="string"||n.length===0?"":n.split("\\").pop().split("/").pop()}function o(n,t={}){const c=n.querySelector(".file-picker-container");if(!c){const h=typeof t.accept=="string"?t.accept:n.getAttribute("data-accept")||"",l=typeof t.placeholderText=="string"?t.placeholderText:n.getAttribute("data-placeholder")||"No file selected",a=typeof t.buttonText=="string"?t.buttonText:n.getAttribute("data-button-text")||"FILE",v=typeof t.selectTitle=="string"?t.selectTitle:n.getAttribute("data-select-title")||"Select file",y=typeof t.clearTitle=="string"?t.clearTitle:n.getAttribute("data-clear-title")||"Clear",i=document.createElement("div");i.className="file-picker-container";const f=document.createElement("input");f.type="file";f.style.display="none";h&&f.setAttribute("accept",h);const e=document.createElement("div");e.className="file-picker-display";const o=document.createElement("span");o.className="filename-text";o.textContent=l;e.appendChild(o);const u=document.createElement("button");u.className="file-picker-button";u.type="button";u.title=v;const s=document.createElement("span");s.className="button-icon";s.textContent=a;u.appendChild(s);const r=document.createElement("button");r.className="file-picker-clear";r.type="button";r.title=y;r.disabled=!0;r.textContent="X";i.appendChild(f);i.appendChild(e);i.appendChild(u);i.appendChild(r);n.replaceChildren(i)}}function u(t={}){function p(n){const t=typeof n=="string"&&n.length>0,i=t?rt==="full"?n:r(n):it;a.textContent=i;a.title=t?n:"";c.disabled=!t}function f(n,t={}){const r=t.persist!==!1,u=!!t.silent,i=typeof n=="string"?n:"";if(s=i,p(s),!u&&b)try{b(s)}catch(f){}if(r&&y){v=!0;try{y(i.length>0?i:null)}finally{setTimeout(()=>{v=!1},50)}}}function nt(){e.value="";f("",{persist:!0})}const w=t.rootId,u=w?document.getElementById(w):null;if(!u)return null;o(u,t);const tt=t.filenameSelector||".filename-text",it=t.placeholderText||"No file selected",h=t.settingsKey,rt=t.displayMode||"basename",b=typeof t.onValueChanged=="function"?t.onValueChanged:null,k=typeof t.initialValue=="string"?t.initialValue:"",e=u.querySelector('input[type="file"]'),d=u.querySelector(".file-picker-button"),c=u.querySelector(".file-picker-clear"),l=u.querySelector(".file-picker-display"),a=l?l.querySelector(tt):null;if(!e||!d||!c||!l||!a)return null;let s="",v=!1,y=null,g=null;return d.addEventListener("click",()=>{e.click()}),e.addEventListener("change",n=>{const t=n?.target?.files?.[0],u=n?.target?.value||"",e=i(u),r=t?.path||e;if(r){f(r,{persist:!0});return}t?.name&&p(t.name)}),c.addEventListener("click",()=>{nt()}),typeof h=="string"&&h.length>0&&n.SDPIComponents?.useSettings&&([g,y]=n.SDPIComponents.useSettings(h,n=>{v||f(typeof n=="string"?n:"",{persist:!1})}),Promise.resolve(g()).then(n=>{f(typeof n=="string"?n:"",{persist:!1})}).catch(()=>{})),p(""),k&&f(k,{persist:!1,silent:!0}),{setValue:f,clear:nt,getValue:()=>s}}function f(t={}){function y(n){const t=typeof n=="string"&&n.length>0,i=t?et==="full"?n:r(n):ft;l.textContent=i;l.title=t?n:"";c.disabled=!t}function f(n,t={}){const r=t.persist!==!1,u=!!t.silent,i=typeof n=="string"?n:"";if(s=i,y(s),!u&&p)try{p(s)}catch(f){}if(r&&v){a=!0;try{v(i.length>0?i:null)}finally{setTimeout(()=>{a=!1},50)}}}function g(){e.value="";f("",{persist:!0})}if(typeof t.rootId=="string"&&t.rootId.length>0)return u(t);const nt=t.inputId,tt=t.buttonId,it=t.clearId,rt=t.displayId,ut=t.filenameSelector||".filename-text",ft=t.placeholderText||"No file selected",h=t.settingsKey,et=t.displayMode||"basename",p=typeof t.onValueChanged=="function"?t.onValueChanged:null,w=typeof t.initialValue=="string"?t.initialValue:"",e=document.getElementById(nt),b=document.getElementById(tt),c=document.getElementById(it),o=document.getElementById(rt),l=o?o.querySelector(ut):null;if(!e||!b||!c||!o||!l)return null;const ot=document.createElement("div"),k=o.closest(".file-picker-container");k&&ot.appendChild(k.cloneNode(!0));let s="",a=!1,v=null,d=null;return b.addEventListener("click",()=>{e.click()}),e.addEventListener("change",n=>{const t=n?.target?.files?.[0],u=n?.target?.value||"",e=i(u),r=t?.path||e;if(r){f(r,{persist:!0});return}t?.name&&y(t.name)}),c.addEventListener("click",()=>{g()}),typeof h=="string"&&h.length>0&&n.SDPIComponents?.useSettings&&([d,v]=n.SDPIComponents.useSettings(h,n=>{a||f(typeof n=="string"?n:"",{persist:!1})}),Promise.resolve(d()).then(n=>{f(typeof n=="string"?n:"",{persist:!1})}).catch(()=>{})),y(""),w&&f(w,{persist:!1,silent:!0}),{setValue:f,clear:g,getValue:()=>s}}const n=globalThis;const t=n.SCPI=n.SCPI||{};t.ui=t.ui||{};t.ui.filePicker={createFilePicker:u,initFilePicker:f};n.SCFilePicker=n.SCFilePicker||{initFilePicker:f}}(),function(){function i(n,t){if(n&&typeof n=="string"){t&&(t.href=`../css/themes/${n}`);try{localStorage.setItem(r,n)}catch(i){}}}function u(r={}){const o=r.rootId||"themeDropdown",s=r.linkId||"pi-theme-styles",u=document.getElementById(s);if(u){let e=[];n.bus?.start?.();const f=n.ui?.dropdown?.initDropdown?.({rootId:o,searchEnabled:!1,displaySelectedInInput:!0,minLoadingMs:500,successFlashMs:220,emptyText:"No themes found",getText:n=>String(n?.name??""),getValue:n=>String(n?.file??""),onSelect:r=>{const f=String(r?.file??"");f&&(i(f,u),(n.bus?.send||n.util?.sendToPlugin||t.sendToPlugin)?.("setTheme",{themeFile:f}))}});f?.setLoading?.(!0,"Loading themes");n.bus?.on?.(n=>{if(n&&(n.themesLoaded&&(e=n.themes||[],f?.setItems?.(e),f?.setLoading?.(!1)),typeof n.selectedTheme=="string"&&n.selectedTheme.length>0)){const t=n.selectedTheme;i(t,u);f?.setSelectedValue?.(t,{rerender:!0})}});try{const t=u.getAttribute("href")||"",n=t.split("/").pop().split("?")[0];n&&(i(n,u),f?.setSelectedValue?.(n,{rerender:!0}))}catch(h){}}}const t=globalThis,n=t.SCPI=t.SCPI||{},r="scsd.selectedTheme";n.theme={initThemeDropdown:u};t.SCTheme=t.SCTheme||n.theme}() diff --git a/PluginCore/PropertyInspector/js/src/pi-dial-action.js b/PluginCore/PropertyInspector/js/src/pi-dial-action.js new file mode 100644 index 0000000..654cbfb --- /dev/null +++ b/PluginCore/PropertyInspector/js/src/pi-dial-action.js @@ -0,0 +1,183 @@ +(function () { + const SCPI = globalThis.SCPI; + SCPI?.bus?.start?.(); + SCPI?.theme?.initThemeDropdown?.(); + + const slotConfigs = [ + {rootId: 'rotateLeftDropdown', settingsKey: 'rotateLeftFunction', slotLabel: 'Rotate Left', idPrefix: 'rotateLeft'}, + {rootId: 'rotateRightDropdown', settingsKey: 'rotateRightFunction', slotLabel: 'Rotate Right', idPrefix: 'rotateRight'}, + {rootId: 'pressDropdown', settingsKey: 'pressFunction', slotLabel: 'Push', idPrefix: 'press'} + ]; + + let allOptions = []; + let isSelectingOption = false; + + const slotStates = slotConfigs.map((config) => { + const dropdown = SCPI?.ui?.dropdown?.initDropdown?.({ + rootId: config.rootId, + searchEnabled: true, + minLoadingMs: 0, + successFlashMs: 100, + emptyText: 'No matching functions found', + maxResults: 50, + getText: (opt) => String(opt?.text ?? ''), + getValue: (opt) => String(opt?.value ?? ''), + getGroup: (opt) => String(opt?.group ?? ''), + isDisabled: (opt) => !!opt?.disabled, + onSelect: (opt) => selectOption(config.settingsKey, opt, {persist: true}) + }); + + dropdown?.setLoading?.(true, 'Loading functions'); + + const [getValue, setValue] = globalThis.SDPIComponents.useSettings( + config.settingsKey, + (value) => { + if (!isSelectingOption) { + syncSelection(config.settingsKey, value); + } + } + ); + + return { + ...config, + dropdown, + getValue, + setValue, + currentValue: '' + }; + }); + + SCPI?.ui?.filePicker?.createFilePicker?.({ + rootId: 'audioFilePicker', + placeholderText: 'No file selected', + settingsKey: 'clickSoundPath' + }); + + function flattenFunctionsData(groups) { + const flatten = SCPI?.functionPicker?.flattenFunctionsData; + if (typeof flatten !== 'function') { + return []; + } + + return flatten(groups, { + requireToggleCandidates: false, + excludeBindingTypes: ['mouseaxis', 'joystick', 'gamepad'] + }); + } + + function populateFunctionsDropdowns(functionsData) { + allOptions = flattenFunctionsData(functionsData); + + slotStates.forEach((slot) => { + slot.dropdown?.setItems?.(allOptions); + slot.dropdown?.setSelectedValue?.(slot.currentValue, {rerender: false}); + + if (slot.currentValue) { + syncSelection(slot.settingsKey, slot.currentValue); + } + }); + } + + function syncSelection(settingsKey, value) { + const slot = slotStates.find((entry) => entry.settingsKey === settingsKey); + if (!slot) { + return; + } + + slot.currentValue = value || ''; + + const opt = allOptions.find((entry) => entry.value === value || entry.legacyValue === value); + if (!opt) { + if (!value) { + updateFunctionDetails(slot, null); + } + return; + } + + const isLegacyMatch = opt.legacyValue === value && opt.value !== value; + selectOption(settingsKey, opt, {persist: isLegacyMatch}); + } + + function selectOption(settingsKey, opt, opts = {}) { + const slot = slotStates.find((entry) => entry.settingsKey === settingsKey); + if (!slot) { + return; + } + + const persist = opts.persist !== false; + + isSelectingOption = true; + slot.currentValue = opt.value; + slot.dropdown?.setSelectedValue?.(opt.value, {rerender: true}); + updateFunctionDetails(slot, opt); + + if (persist) { + slot.setValue(opt.value); + } + + setTimeout(() => { + isSelectingOption = false; + }, 200); + } + + function updateFunctionDetails(slot, opt) { + if (!slot || !slot.idPrefix) { + return; + } + + const updateDetails = SCPI?.functionPicker?.updateFunctionDetails; + if (typeof updateDetails !== 'function') { + return; + } + + const prefix = slot.idPrefix; + updateDetails({ + containerEl: document.getElementById(`${prefix}-details-container`), + titleEl: document.getElementById(`${prefix}-details-title`), + descriptionEl: document.getElementById(`${prefix}-description`), + bindingElements: { + keyboard: document.getElementById(`${prefix}-binding-keyboard`), + mouse: document.getElementById(`${prefix}-binding-mouse`), + gamepad: document.getElementById(`${prefix}-binding-gamepad`), + joystick: document.getElementById(`${prefix}-binding-joystick`) + }, + hideContainerWhenEmpty: true, + showContainerWhenFilled: true, + slotLabel: slot.slotLabel, + selectedOption: opt + }); + } + + SCPI?.bus?.on?.((payload) => { + const loaded = payload?.functionsLoaded; + + if (loaded === true) { + slotStates.forEach((slot) => { + slot.dropdown?.setLoading?.(false); + document.getElementById(slot.rootId)?.classList.remove('pi-dropdown--error'); + }); + + populateFunctionsDropdowns(payload.functions || []); + } + + if (loaded === false) { + slotStates.forEach((slot) => { + document.getElementById(slot.rootId)?.classList.add('pi-dropdown--error'); + slot.dropdown?.setLoading?.(true, 'No installation detected. Set custom path.'); + slot.dropdown?.setItems?.([]); + updateFunctionDetails(slot, null); + }); + } + }); + + SCPI?.util?.onDocumentReady?.(() => { + slotStates.forEach((slot) => { + const savedValue = slot.getValue(); + slot.currentValue = savedValue || ''; + slot.dropdown?.setSelectedValue?.(slot.currentValue, {rerender: false}); + updateFunctionDetails(slot, null); + }); + + SCPI?.bus?.sendOnce?.('pi.connected', 'propertyInspectorConnected'); + }); +})(); diff --git a/PluginCore/PropertyInspector/js/src/pi-function-key.js b/PluginCore/PropertyInspector/js/src/pi-function-key.js index e7b4bdd..02badd6 100644 --- a/PluginCore/PropertyInspector/js/src/pi-function-key.js +++ b/PluginCore/PropertyInspector/js/src/pi-function-key.js @@ -134,58 +134,17 @@ * @returns {Array} - Flattened array of options */ function flattenFunctionsData(groups) { - const flat = []; - const requireToggleCandidates = globalThis.SCPI_REQUIRE_TOGGLE_CANDIDATES === true; - if (Array.isArray(groups)) { - groups.forEach(group => { - const groupName = group.label || 'Other'; - const options = group.options || []; - - options.forEach(opt => { - const bindingType = String(opt.bindingType || '').toLowerCase(); - - if (requireToggleCandidates) { - const isToggleCandidate = opt && opt.details && opt.details.isToggleCandidate === true; - if (!isToggleCandidate) { - return; - } - } - - // Filter out unsupported options - // TODO: When implementing full axis support, stop hiding axis-only options in the PI. - const isUnsupported = bindingType === 'mouseaxis' || bindingType === 'joystick' || bindingType === 'gamepad'; - if (isUnsupported) { - return; - } - - // Keep the original category; unbound actions are shown with a warning indicator. - const category = groupName; - - const disabledReason = String(opt.disabledReason || ''); - const isUnbound = bindingType === 'unbound'; - - // Unbound actions are selectable (so users can bind them later), but still visually flagged. - const isDisabled = !!opt.disabled && !isUnbound; - - flat.push( - { - value: opt.value, - legacyValue: opt.legacyValue, - text: opt.text, - group: category, - details: opt.details, - bindingType, - disabledReason, - unbound: isUnbound, - disabled: isDisabled - }); - }); + const flatten = SCPI?.functionPicker?.flattenFunctionsData; + if (typeof flatten === 'function') { + return flatten(groups, { + requireToggleCandidates, + excludeBindingTypes: ['mouseaxis', 'joystick', 'gamepad'] }); } - return flat; + return []; } /** @@ -260,72 +219,25 @@ return; } - // Show empty state if no option selected - if (!opt || !opt.details) { - const titleEl = detailsEl.querySelector('.pi-details__title'); - if (titleEl) { - titleEl.textContent = ''; - } - - const scContentEl = document.querySelector('.pi-description__content'); - if (scContentEl) { - scContentEl.textContent = ''; - } - - // Clear all binding values - const kbEl = document.getElementById('pi-details__binding-keyboard'); - const mouseEl = document.getElementById('pi-details__binding-mouse'); - const gpEl = document.getElementById('pi-details__binding-gamepad'); - const jsEl = document.getElementById('pi-details__binding-joystick'); - - [kbEl, mouseEl, gpEl, jsEl].forEach((el) => { - if (!el) { - return; - } - el.textContent = ''; - }); + const updateDetails = SCPI?.functionPicker?.updateFunctionDetails; + if (typeof updateDetails !== 'function') { return; } - const details = opt.details; - const title = String(details.label || opt.text || ''); - const desc = String(details.description || ''); - const devices = Array.isArray(details.devices) ? details.devices : []; - - const titleEl = detailsEl.querySelector('.pi-details__title'); - if (titleEl) { - titleEl.textContent = title; - } - - // Update binding values for all four device types - const allDeviceTypes = ['keyboard', 'mouse', 'gamepad', 'joystick']; - - allDeviceTypes.forEach(deviceType => { - const deviceData = devices.find(d => d.device && d.device.toLowerCase() === deviceType.toLowerCase()); - const bindingEl = document.getElementById(`pi-details__binding-${deviceType}`); - - let bindingValue = 'Unbound'; - - if (deviceData && Array.isArray(deviceData.bindings) && deviceData.bindings.length > 0) { - const bindingLines = deviceData.bindings - .map(b => String(b.display || b.raw || '')) - .filter(x => x); - - if (bindingLines.length > 0) { - bindingValue = bindingLines.join(', '); - } - } - - if (bindingEl) { - bindingEl.textContent = bindingValue; - } + updateDetails({ + containerEl: detailsEl, + titleEl: detailsEl.querySelector('.pi-details__title'), + descriptionEl: document.querySelector('.pi-description__content'), + bindingElements: { + keyboard: document.getElementById('pi-details__binding-keyboard'), + mouse: document.getElementById('pi-details__binding-mouse'), + gamepad: document.getElementById('pi-details__binding-gamepad'), + joystick: document.getElementById('pi-details__binding-joystick') + }, + hideContainerWhenEmpty: false, + showContainerWhenFilled: false, + selectedOption: opt }); - - // Update Description content - const scContentEl = document.querySelector('.pi-description__content'); - if (scContentEl) { - scContentEl.textContent = desc || 'No description available.'; - } } // #endregion diff --git a/PluginCore/PropertyInspector/js/src/sc-common.js b/PluginCore/PropertyInspector/js/src/sc-common.js index a587bc3..c630c64 100644 --- a/PluginCore/PropertyInspector/js/src/sc-common.js +++ b/PluginCore/PropertyInspector/js/src/sc-common.js @@ -55,3 +55,169 @@ root.sendToPlugin = sendToPlugin; } })(); + +(function () { + const root = globalThis; + const SCPI = root.SCPI = root.SCPI || {}; + const functionPicker = SCPI.functionPicker = SCPI.functionPicker || {}; + + /** + * Flatten grouped function payload data into a dropdown-friendly list. + * + * Supports two key filters: + * - `requireToggleCandidates`: keep only functions that can be used by Toggle Key. + * - `excludeBindingTypes`: hide binding types that a PI context cannot execute. + * + * @param {Array} groups + * @param {{ requireToggleCandidates?: boolean, excludeBindingTypes?: string[] }} options + * @returns {Array} + */ + function flattenFunctionsData(groups, options = {}) { + const flat = []; + const requireToggleCandidates = options.requireToggleCandidates === true; + const excluded = new Set((options.excludeBindingTypes || []).map((t) => String(t || '').toLowerCase())); + + if (!Array.isArray(groups)) { + return flat; + } + + groups.forEach((group) => { + const groupName = group?.label || 'Other'; + const entries = Array.isArray(group?.options) ? group.options : []; + + entries.forEach((opt) => { + const bindingType = String(opt?.bindingType || '').toLowerCase(); + + if (requireToggleCandidates) { + const isToggleCandidate = opt && opt.details && opt.details.isToggleCandidate === true; + if (!isToggleCandidate) { + return; + } + } + + // Filter out unsupported options. + // TODO: When implementing full axis support, stop hiding axis-only options in the PI. + if (excluded.has(bindingType)) { + return; + } + + const disabledReason = String(opt?.disabledReason || ''); + const isUnbound = bindingType === 'unbound'; + + // Keep the original category; unbound actions are shown with a warning indicator. + // Unbound actions are selectable (so users can bind them later), but still visually flagged. + const isDisabled = !!opt?.disabled && !isUnbound; + + flat.push({ + value: opt?.value, + legacyValue: opt?.legacyValue, + text: opt?.text, + group: groupName, + details: opt?.details, + bindingType, + disabledReason, + unbound: isUnbound, + disabled: isDisabled + }); + }); + }); + + return flat; + } + + /** + * Render function metadata and per-device bindings into a details panel. + * + * The caller controls whether the details container is hidden for empty + * state and shown again when an option is selected. + * + * @param {{ + * containerEl?: HTMLElement | null, + * titleEl?: HTMLElement | null, + * descriptionEl?: HTMLElement | null, + * bindingElements?: Object, + * hideContainerWhenEmpty?: boolean, + * showContainerWhenFilled?: boolean, + * selectedOption?: Object | null, + * slotLabel?: string + * }} options + */ + function updateFunctionDetails(options = {}) { + const containerEl = options.containerEl || null; + const titleEl = options.titleEl || null; + const descriptionEl = options.descriptionEl || null; + const bindingElements = options.bindingElements || {}; + const hideContainerWhenEmpty = options.hideContainerWhenEmpty === true; + const showContainerWhenFilled = options.showContainerWhenFilled === true; + const selectedOption = options.selectedOption || null; + const slotLabel = String(options.slotLabel || '').trim(); + + const devices = ['keyboard', 'mouse', 'gamepad', 'joystick']; + + // Empty state: clear all displayed values to avoid stale details. + if (!selectedOption || !selectedOption.details) { + if (hideContainerWhenEmpty && containerEl) { + containerEl.style.display = 'none'; + } + + if (titleEl) { + titleEl.textContent = ''; + } + + if (descriptionEl) { + descriptionEl.textContent = ''; + } + + devices.forEach((device) => { + const el = bindingElements[device] || null; + if (el) { + el.textContent = ''; + } + }); + + return; + } + + if (showContainerWhenFilled && containerEl) { + containerEl.style.display = 'block'; + } + + const details = selectedOption.details; + const title = String(details.label || selectedOption.text || ''); + const description = String(details.description || ''); + const detailDevices = Array.isArray(details.devices) ? details.devices : []; + + if (titleEl) { + titleEl.textContent = slotLabel.length > 0 ? `${slotLabel}: ${title}` : title; + } + + // Update binding values for all four device types. + devices.forEach((deviceType) => { + const deviceData = detailDevices.find((d) => d.device && d.device.toLowerCase() === deviceType); + const bindingEl = bindingElements[deviceType] || null; + + let bindingValue = 'Unbound'; + if (deviceData && Array.isArray(deviceData.bindings) && deviceData.bindings.length > 0) { + const bindingLines = deviceData.bindings + .map((binding) => String(binding.display || binding.raw || '')) + .filter((text) => text); + + if (bindingLines.length > 0) { + bindingValue = bindingLines.join(', '); + } + } + + if (bindingEl) { + bindingEl.textContent = bindingValue; + } + }); + + // Update Description content. + if (descriptionEl) { + descriptionEl.textContent = description || 'No description available.'; + } + } + + functionPicker.flattenFunctionsData = flattenFunctionsData; + functionPicker.updateFunctionDetails = updateFunctionDetails; +})(); diff --git a/PluginCore/manifest.json b/PluginCore/manifest.json index b2052fe..8ea0a73 100644 --- a/PluginCore/manifest.json +++ b/PluginCore/manifest.json @@ -3,6 +3,9 @@ { "Icon": "Images/icon", "Name": "Adaptive Key", + "Controllers": [ + "Keypad" + ], "States": [ { "Image": "PropertyInspector/svg/key", @@ -15,9 +18,37 @@ "UUID": "com.jarex985.scstreamdeck.adaptivekey", "PropertyInspectorPath": "PropertyInspector/Keys/AdaptiveKey.html" }, + { + "Icon": "Images/icon", + "Name": "Adaptive Dial", + "Controllers": [ + "Encoder" + ], + "Encoder": { + "layout": "$B1", + "TriggerDescription": { + "Rotate": "Execute left or right function", + "Push": "Execute push function" + } + }, + "States": [ + { + "Image": "PropertyInspector/svg/key", + "TitleAlignment": "middle", + "FontSize": 12 + } + ], + "SupportedInMultiActions": false, + "Tooltip": "Star Citizen Adaptive Dial", + "UUID": "com.jarex985.scstreamdeck.adaptivedial", + "PropertyInspectorPath": "PropertyInspector/Dials/AdaptiveDial.html" + }, { "Icon": "Images/icon", "Name": "Toggle Key", + "Controllers": [ + "Keypad" + ], "States": [ { "Image": "PropertyInspector/svg/key", @@ -39,6 +70,9 @@ { "Icon": "Images/icon", "Name": "Control Panel", + "Controllers": [ + "Keypad" + ], "States": [ { "Image": "PropertyInspector/svg/key", @@ -59,7 +93,7 @@ "Description": "Plugin that provides bindable actions for Star Citizen via Stream Deck.\nThis Plugin is not endorsed by or affiliated with the Cloud Imperium or Roberts Space Industries group of companies. All game content and materials are copyright Cloud Imperium Rights LLC and Cloud Imperium Rights Ltd.. Star Citizen®,Squadron 42®, Roberts Space Industries®, and Cloud Imperium® are registered trademarks of Cloud Imperium Rights LLC. All rights reserved.", "Icon": "Images/pluginIcon", "URL": "https://github.com/Jarex985/SCStreamDeck", - "Version": "1.1.3.0", + "Version": "1.2.0.0", "CodePath": "com.jarex985.scstreamdeck.exe", "Category": "Star Citizen Stream Deck", "CategoryIcon": "Images/categoryIcon", diff --git a/README.md b/README.md index 63e7a18..5ffb38c 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Report bugs / feature requests: https://github.com/Jarex985/SCStreamDeck/issues - **Adaptive Key**: A key that executes keybindings based on the activation mode for a given binding. - *Example:* Two in-game bindings (Tap vs Hold) on `Num-` executes only the Tap function when this is the assigned function. +- **Adaptive Dial** *(Stream Deck+)*: A dial that maps separate Star Citizen functions to rotate left, rotate right, and dial push. Rotation fires the assigned function once per tick; push respects the action's activation mode. - **Toggle Key**: A key that toggles between two states (e.g., landing gear up/down). Can be reset to match the current in-game state on de-sync. - **Control Panel Key**: A dedicated key for managing global plugin settings such as themes, channel selection, and installation paths. - **Auto-Detection of Star Citizen Installation Path**: Automatically detects the installation path of Star Citizen. diff --git a/Tests/Unit/ActionKeys/AdaptiveDialTests.cs b/Tests/Unit/ActionKeys/AdaptiveDialTests.cs new file mode 100644 index 0000000..5afc88e --- /dev/null +++ b/Tests/Unit/ActionKeys/AdaptiveDialTests.cs @@ -0,0 +1,328 @@ +using FluentAssertions; +using Newtonsoft.Json.Linq; +using SCStreamDeck.ActionKeys; +using SCStreamDeck.ActionKeys.Settings; +using SCStreamDeck.Models; + +namespace Tests.Unit.ActionKeys; + +public sealed class AdaptiveDialTests +{ + private static KeybindingAction CreateAction( + string? keyboardBinding = null, + string? mouseBinding = null) => + new() + { + ActionName = "test_action", + MapName = "Gameplay", + MapLabel = "Gameplay", + UiLabel = "Test Action", + UiDescription = "Test Description", + UiCategory = "Flight", + KeyboardBinding = keyboardBinding ?? string.Empty, + MouseBinding = mouseBinding ?? string.Empty, + JoystickBinding = string.Empty, + GamepadBinding = string.Empty, + ActivationMode = ActivationMode.press + }; + + #region ResolveRotationFunction + + [Fact] + public void ResolveRotationFunction_ReturnsRotateRight_ForPositiveTicks() + { + DialSettings settings = new() + { + RotateLeftFunction = "left_function", + RotateRightFunction = "right_function" + }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, 3); + + result.Should().Be("right_function"); + } + + [Fact] + public void ResolveRotationFunction_ReturnsRotateLeft_ForNegativeTicks() + { + DialSettings settings = new() + { + RotateLeftFunction = "left_function", + RotateRightFunction = "right_function" + }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, -2); + + result.Should().Be("left_function"); + } + + [Fact] + public void ResolveRotationFunction_ReturnsNull_ForZeroTicks() + { + DialSettings settings = new() + { + RotateLeftFunction = "left_function", + RotateRightFunction = "right_function" + }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, 0); + + result.Should().BeNull(); + } + + [Theory] + [InlineData(1)] + [InlineData(10)] + [InlineData(int.MaxValue)] + public void ResolveRotationFunction_ReturnsRightFunction_ForAnyPositiveTicks(int ticks) + { + DialSettings settings = new() { RotateRightFunction = "right" }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, ticks); + + result.Should().Be("right"); + } + + [Theory] + [InlineData(-1)] + [InlineData(-10)] + [InlineData(int.MinValue)] + public void ResolveRotationFunction_ReturnsLeftFunction_ForAnyNegativeTicks(int ticks) + { + DialSettings settings = new() { RotateLeftFunction = "left" }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, ticks); + + result.Should().Be("left"); + } + + [Fact] + public void ResolveRotationFunction_ReturnsNull_WhenRightFunctionIsNull_AndPositiveTicks() + { + DialSettings settings = new() { RotateLeftFunction = "left" }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, 1); + + result.Should().BeNull(); + } + + [Fact] + public void ResolveRotationFunction_ReturnsNull_WhenLeftFunctionIsNull_AndNegativeTicks() + { + DialSettings settings = new() { RotateRightFunction = "right" }; + + string? result = AdaptiveDial.ResolveRotationFunction(settings, -1); + + result.Should().BeNull(); + } + + [Fact] + public void ResolveRotationFunction_ThrowsArgumentNullException_ForNullSettings() + { + Action act = () => AdaptiveDial.ResolveRotationFunction(null!, 1); + + act.Should().Throw(); + } + + #endregion + + #region ResolveExecutableBinding + + [Fact] + public void ResolveExecutableBinding_PrefersKeyboardBinding_WhenPresent() + { + KeybindingAction action = CreateAction(keyboardBinding: "f1", mouseBinding: "mouse1"); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be("f1"); + } + + [Fact] + public void ResolveExecutableBinding_ReturnsMouseButton_WhenNoKeyboardBindingExists() + { + KeybindingAction action = CreateAction(mouseBinding: "mouse1"); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be("mouse1"); + } + + [Fact] + public void ResolveExecutableBinding_ReturnsMouseWheel_WhenNoKeyboardBindingExists() + { + KeybindingAction action = CreateAction(mouseBinding: "mwheel_up"); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be("mwheel_up"); + } + + [Fact] + public void ResolveExecutableBinding_ReturnsNull_ForUnsupportedAxisBinding() + { + KeybindingAction action = CreateAction(mouseBinding: "maxis_x"); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().BeNull(); + } + + [Fact] + public void ResolveExecutableBinding_ReturnsNull_WhenNoSupportedBindingExists() + { + KeybindingAction action = CreateAction(); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().BeNull(); + } + + [Theory] + [InlineData("mouse1")] + [InlineData("mouse2")] + [InlineData("mouse3")] + public void ResolveExecutableBinding_ReturnsMouseButton_ForAllMouseButtonVariants(string mouseBinding) + { + KeybindingAction action = CreateAction(mouseBinding: mouseBinding); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be(mouseBinding); + } + + [Theory] + [InlineData("mwheel_up")] + [InlineData("mwheel_down")] + public void ResolveExecutableBinding_ReturnsMouseWheel_ForAllWheelVariants(string mouseBinding) + { + KeybindingAction action = CreateAction(mouseBinding: mouseBinding); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be(mouseBinding); + } + + [Theory] + [InlineData("maxis_x")] + [InlineData("maxis_y")] + public void ResolveExecutableBinding_ReturnsNull_ForAllMouseAxisVariants(string mouseBinding) + { + KeybindingAction action = CreateAction(mouseBinding: mouseBinding); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().BeNull(); + } + + [Theory] + [InlineData("f1")] + [InlineData("a")] + [InlineData("lshift+space")] + [InlineData("lctrl+alt+f4")] + public void ResolveExecutableBinding_ReturnsKeyboardBinding_ForVariousKeyboardCombos(string keyboardBinding) + { + KeybindingAction action = CreateAction(keyboardBinding: keyboardBinding); + + string? result = AdaptiveDial.ResolveExecutableBinding(action); + + result.Should().Be(keyboardBinding); + } + + [Fact] + public void ResolveExecutableBinding_ThrowsArgumentNullException_ForNullAction() + { + Action act = () => AdaptiveDial.ResolveExecutableBinding(null!); + + act.Should().Throw(); + } + + #endregion + + #region DialSettings + + [Fact] + public void DialSettings_DefaultValues_AreAllNull() + { + DialSettings settings = new(); + + settings.RotateLeftFunction.Should().BeNull(); + settings.RotateRightFunction.Should().BeNull(); + settings.PressFunction.Should().BeNull(); + settings.ClickSoundPath.Should().BeNull(); + } + + [Fact] + public void DialSettings_SerializesAndDeserializes_UsingExpectedPropertyNames() + { + DialSettings settings = new() + { + RotateLeftFunction = "left_function", + RotateRightFunction = "right_function", + PressFunction = "press_function", + ClickSoundPath = @"C:\\Audio\\click.wav" + }; + + JObject json = JObject.FromObject(settings); + DialSettings? roundTripped = json.ToObject(); + + json.Value("rotateLeftFunction").Should().Be("left_function"); + json.Value("rotateRightFunction").Should().Be("right_function"); + json.Value("pressFunction").Should().Be("press_function"); + json.Value("clickSoundPath").Should().Be(@"C:\\Audio\\click.wav"); + roundTripped.Should().NotBeNull(); + roundTripped!.RotateLeftFunction.Should().Be("left_function"); + roundTripped.RotateRightFunction.Should().Be("right_function"); + roundTripped.PressFunction.Should().Be("press_function"); + roundTripped.ClickSoundPath.Should().Be(@"C:\\Audio\\click.wav"); + } + + [Fact] + public void DialSettings_DeserializesPartialJson_LeavingMissingPropertiesNull() + { + JObject json = new() { ["rotateLeftFunction"] = "only_left" }; + + DialSettings? settings = json.ToObject(); + + settings.Should().NotBeNull(); + settings!.RotateLeftFunction.Should().Be("only_left"); + settings.RotateRightFunction.Should().BeNull(); + settings.PressFunction.Should().BeNull(); + settings.ClickSoundPath.Should().BeNull(); + } + + [Fact] + public void DialSettings_SerializesNullProperties_AsJsonNulls() + { + DialSettings settings = new() { RotateLeftFunction = "left" }; + + JObject json = JObject.FromObject(settings); + + json["rotateLeftFunction"]!.Value().Should().Be("left"); + json["rotateRightFunction"]!.Type.Should().Be(JTokenType.Null); + json["pressFunction"]!.Type.Should().Be(JTokenType.Null); + json["clickSoundPath"]!.Type.Should().Be(JTokenType.Null); + } + + [Fact] + public void DialSettings_RoundTrip_EmptyStringValues_ArePreserved() + { + DialSettings settings = new() + { + RotateLeftFunction = "", + RotateRightFunction = "", + PressFunction = "", + ClickSoundPath = "" + }; + + JObject json = JObject.FromObject(settings); + DialSettings? roundTripped = json.ToObject(); + + roundTripped!.RotateLeftFunction.Should().Be(""); + roundTripped.RotateRightFunction.Should().Be(""); + roundTripped.PressFunction.Should().Be(""); + roundTripped.ClickSoundPath.Should().Be(""); + } + + #endregion +} diff --git a/Tools/PiAssetsBuilder/Program.cs b/Tools/PiAssetsBuilder/Program.cs index 5a14cfb..5d51f84 100644 --- a/Tools/PiAssetsBuilder/Program.cs +++ b/Tools/PiAssetsBuilder/Program.cs @@ -49,6 +49,7 @@ private static void BuildAll(string piRoot) BuildPageScript(jsSrc, jsOut, "sc-preload.js"); BuildPageScript(jsSrc, jsOut, "pi-control-panel.js"); BuildPageScript(jsSrc, jsOut, "pi-function-key.js"); + BuildPageScript(jsSrc, jsOut, "pi-dial-action.js"); BuildCss(cssSrc, cssOut); } diff --git a/docs/assets/images/adaptive-dial.png b/docs/assets/images/adaptive-dial.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f0c0e9ef72387b60021c10d463070fc7837902 GIT binary patch literal 99653 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 I8V_Rr3oONw;s5{u literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index ef5ea7e..7fa5c64 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,6 +26,7 @@ This plugin lets you control various aspects of Star Citizen from your Elgato St - **Adaptive Key**: A key that executes keybindings based on the activation mode for a given binding. - *Example:* Two in-game bindings (Tap vs Hold) on `Num-` executes only the Tap function when this is the assigned function. +- **Adaptive Dial** *(Stream Deck+)*: A dial that maps separate Star Citizen functions to rotate left, rotate right, and dial push. Rotation fires the assigned function once per tick; push respects the action's activation mode. - **Toggle Key**: A key that toggles between two states (e.g., landing gear up/down). Can be reset to match the current in-game state on de-sync. - **Control Panel Key**: A dedicated key for managing global plugin settings such as themes, channel selection, and installation paths. - **Auto-Detection of Star Citizen Installation Path**: Automatically detects the installation path of Star Citizen. diff --git a/docs/usage.md b/docs/usage.md index 9c0dea9..0ba117f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -14,6 +14,30 @@ Basic flow: ![Adaptive Key](assets/images/adaptive-key.png){ style="width:50%; height:auto;" } +## Adaptive Dial *(Stream Deck+)* + +Use `Adaptive Dial` on a **Stream Deck+** device to map Star Citizen functions to the physical dial. + +Each dial slot is independent: + +| Slot | Trigger | Behaviour | +|---|---|---| +| **Rotate Left** | Turn dial counter-clockwise | Executes the assigned function once per tick | +| **Rotate Right** | Turn dial clockwise | Executes the assigned function once per tick | +| **Push** | Press the dial down / release | Executes the assigned function using its activation mode (e.g., Tap vs Hold) | + +Basic flow: + +1. Drag `Adaptive Dial` onto a dial slot on your Stream Deck+. +2. Click on it to open the Property Inspector. +3. Select a Star Citizen function for each slot you want to use (slots left empty are ignored). +4. (Optional) Select a sound file (.wav/.mp3) to play on dial press. + +![Adaptive Dial](assets/images/adaptive-dial.png){ style="width:50%; height:auto;" } + +!!! tip "Rotation step size" + Each physical tick of the dial fires the function exactly once, so fast spins will queue multiple executions automatically. + ## Toggle Key Use `Toggle Key` for bindings that have two states (e.g., landing gear up/down). From 3e5a749606c23538b0fd58d67525b41568f43eba Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:19:31 +0100 Subject: [PATCH 4/8] fix: align dial touch press behavior with dial press --- PluginCore/ActionKeys/AdaptiveDial.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/PluginCore/ActionKeys/AdaptiveDial.cs b/PluginCore/ActionKeys/AdaptiveDial.cs index a868c30..4e0c2c9 100644 --- a/PluginCore/ActionKeys/AdaptiveDial.cs +++ b/PluginCore/ActionKeys/AdaptiveDial.cs @@ -53,6 +53,31 @@ public override async void DialUp(DialPayload payload) } } + public override async void TouchPress(TouchpadPressPayload payload) + { + try + { + // SDK versions may differ on TouchpadPressPayload shape; read pressed-state defensively. + bool isKeyDown = true; + object? pressedValue = payload.GetType().GetProperty("Pressed")?.GetValue(payload); + if (pressedValue is bool pressed) + { + isKeyDown = pressed; + } + + if (isKeyDown) + { + PlayClickSoundIfConfigured(); + } + + await ProcessPressEventAsync(isKeyDown).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: {ex.Message}", ex); + } + } + private async Task ProcessDialRotateAsync(int ticks) { if (ticks == 0) From 9fe9efa4b2e41ac9022ef2c330e56e01a1dda543 Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:45:04 +0100 Subject: [PATCH 5/8] fix: color-text-secondary --- PluginCore/PropertyInspector/css/src/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginCore/PropertyInspector/css/src/base.css b/PluginCore/PropertyInspector/css/src/base.css index fd8ee8b..bc82b08 100644 --- a/PluginCore/PropertyInspector/css/src/base.css +++ b/PluginCore/PropertyInspector/css/src/base.css @@ -916,7 +916,7 @@ body { } .pi-dial-input__hint { - color: var(--sdpi-color-secondary); + color: var(--color-text-secondary); font-size: 11px; line-height: 1.4; margin: 0; From bd06bd6d294e39404bb549a123ba101a1b71f3a5 Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:07:22 +0100 Subject: [PATCH 6/8] feat: add Adaptive Dial action with shared PI function-picker helpers - Added new Stream Deck+ `Adaptive Dial` action with separate assignments for rotate-left, rotate-right, and push input. - Implemented dial action backend with dedicated dial base/action settings: - `AdaptiveDial` execution flow for rotation ticks and press/release handling - `SCDialActionBase` for shared dial initialization, PI updates, and setting migration - `DialSettings` schema for dial slot bindings and click sound path - Updated `manifest.json`: - Registered `Adaptive Dial` with `Encoder` controller metadata - Added explicit `Controllers` declarations for existing key actions - Bumped plugin version to `1.2.0.0` - Added Property Inspector support for dials: - New `Dials/AdaptiveDial.html` - New dial PI script (`pi-dial-action.js`) and source (`js/src/pi-dial-action.js`) - Moved shared function picker logic into `js/src/sc-common.js` and reused from key/dial PI scripts - Regenerated PI bundled assets (`sc-components.js`, pi-function-key.js, `pi-dial-action.js`, `base.css`) - Improved PI maintainability and UX behavior: - Centralized function flattening/details rendering helpers - Restored documentation comments/TODOs for unsupported axis filtering and details rendering semantics - Scoped dial details visibility rules to avoid cross-impact with key PIs - Expanded tests with `AdaptiveDialTests` for rotation resolution, executable binding selection, and settings serialization behavior. - Updated docs and release notes: - README.md, index.md, usage.md (including Adaptive Dial image) - CHANGELOG.md entry for `v1.2.0.0` including feature and internal refactor notes. --- PluginCore/ActionKeys/AdaptiveDial.cs | 25 ------------------- PluginCore/PropertyInspector/css/src/base.css | 2 +- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/PluginCore/ActionKeys/AdaptiveDial.cs b/PluginCore/ActionKeys/AdaptiveDial.cs index 4e0c2c9..a868c30 100644 --- a/PluginCore/ActionKeys/AdaptiveDial.cs +++ b/PluginCore/ActionKeys/AdaptiveDial.cs @@ -53,31 +53,6 @@ public override async void DialUp(DialPayload payload) } } - public override async void TouchPress(TouchpadPressPayload payload) - { - try - { - // SDK versions may differ on TouchpadPressPayload shape; read pressed-state defensively. - bool isKeyDown = true; - object? pressedValue = payload.GetType().GetProperty("Pressed")?.GetValue(payload); - if (pressedValue is bool pressed) - { - isKeyDown = pressed; - } - - if (isKeyDown) - { - PlayClickSoundIfConfigured(); - } - - await ProcessPressEventAsync(isKeyDown).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Err($"{GetType().Name}: {ex.Message}", ex); - } - } - private async Task ProcessDialRotateAsync(int ticks) { if (ticks == 0) diff --git a/PluginCore/PropertyInspector/css/src/base.css b/PluginCore/PropertyInspector/css/src/base.css index bc82b08..fd8ee8b 100644 --- a/PluginCore/PropertyInspector/css/src/base.css +++ b/PluginCore/PropertyInspector/css/src/base.css @@ -916,7 +916,7 @@ body { } .pi-dial-input__hint { - color: var(--color-text-secondary); + color: var(--sdpi-color-secondary); font-size: 11px; line-height: 1.4; margin: 0; From e78a9b85620bf7701c08058090c1072c3327cb84 Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:19:31 +0100 Subject: [PATCH 7/8] fix: align dial touch press behavior with dial press --- PluginCore/ActionKeys/AdaptiveDial.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/PluginCore/ActionKeys/AdaptiveDial.cs b/PluginCore/ActionKeys/AdaptiveDial.cs index a868c30..4e0c2c9 100644 --- a/PluginCore/ActionKeys/AdaptiveDial.cs +++ b/PluginCore/ActionKeys/AdaptiveDial.cs @@ -53,6 +53,31 @@ public override async void DialUp(DialPayload payload) } } + public override async void TouchPress(TouchpadPressPayload payload) + { + try + { + // SDK versions may differ on TouchpadPressPayload shape; read pressed-state defensively. + bool isKeyDown = true; + object? pressedValue = payload.GetType().GetProperty("Pressed")?.GetValue(payload); + if (pressedValue is bool pressed) + { + isKeyDown = pressed; + } + + if (isKeyDown) + { + PlayClickSoundIfConfigured(); + } + + await ProcessPressEventAsync(isKeyDown).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Err($"{GetType().Name}: {ex.Message}", ex); + } + } + private async Task ProcessDialRotateAsync(int ticks) { if (ticks == 0) From d1020dff0637247a17045f39510eb6e28b59df86 Mon Sep 17 00:00:00 2001 From: ROBdk97 Date: Mon, 16 Mar 2026 13:45:04 +0100 Subject: [PATCH 8/8] fix: color-text-secondary --- PluginCore/PropertyInspector/css/src/base.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginCore/PropertyInspector/css/src/base.css b/PluginCore/PropertyInspector/css/src/base.css index fd8ee8b..bc82b08 100644 --- a/PluginCore/PropertyInspector/css/src/base.css +++ b/PluginCore/PropertyInspector/css/src/base.css @@ -916,7 +916,7 @@ body { } .pi-dial-input__hint { - color: var(--sdpi-color-secondary); + color: var(--color-text-secondary); font-size: 11px; line-height: 1.4; margin: 0;