diff --git a/UI/SettingsForm.cs b/UI/SettingsForm.cs index 5f75d26..fa8c56c 100644 --- a/UI/SettingsForm.cs +++ b/UI/SettingsForm.cs @@ -46,6 +46,10 @@ public class SettingsForm : EqSwitchForm private NumericUpDown _nudLogTrimThreshold = null!; // ─── Tray Click controls (Left) + // 2026-06-04: non-selectable group divider inserted into the click-action dropdowns — a + // U+2500 box-drawing line. WireTraySeparatorBounce keeps it unpickable so it can never be + // committed/saved as an action (which AppConfig.Validate would reject → silent reset to None). + private const string TrayActionSeparator = "──────────────"; private ComboBox _cboSingleClick = null!; private ComboBox _cboDoubleClick = null!; private ComboBox _cboTripleClick = null!; @@ -630,7 +634,20 @@ private TabPage BuildGeneralTab() // Display strings. "Launch One"/"Launch Two" are display-only (stored values stay // "LaunchOne"/"LaunchAll" via the tray action maps). AutoLoginTeam5/6 included so a // hand-edited config binding them isn't shown a blank dropdown (round-trip-safe). - var clickActions = new[] { "None", "Auto-Login1", "AutoLoginTeam1", "TogglePiP", "Launch One", "Launch Two", "FixWindows", "SwapWindows", "Settings", "ShowHelp", "Auto-Login2", "Auto-Login3", "Auto-Login4", "AutoLoginTeam2", "AutoLoginTeam3", "AutoLoginTeam4", "AutoLoginTeam5", "AutoLoginTeam6" }; + // 2026-06-04: TrayActionSeparator rows partition the list into four logical groups — + // primary (None / Auto-Login1 / Team1) | window + utility | extra Auto-Logins | extra Teams. + // Dividers are made non-selectable below (WireTraySeparatorBounce). All 5 click combos + // share this one list. + var clickActions = new[] + { + "None", "Auto-Login1", "AutoLoginTeam1", + TrayActionSeparator, + "TogglePiP", "Launch One", "Launch Two", "FixWindows", "SwapWindows", "Settings", "ShowHelp", + TrayActionSeparator, + "Auto-Login2", "Auto-Login3", "Auto-Login4", + TrayActionSeparator, + "AutoLoginTeam2", "AutoLoginTeam3", "AutoLoginTeam4", "AutoLoginTeam5", "AutoLoginTeam6" + }; var cardTray = stack.NewCard("🖱", "Tray Click Actions", DarkTheme.CardBlue); _cboSingleClick = Fields.Combo(140, clickActions); @@ -642,6 +659,10 @@ private TabPage BuildGeneralTab() "Left Click", new (string, Control)[] { ("Single", _cboSingleClick), ("Double", _cboDoubleClick), ("Triple", _cboTripleClick) }, "Middle Click", new (string, Control)[] { ("Single", _cboMiddleClick), ("Triple", _cboMiddleDoubleClick) })); + // 2026-06-04: make the group dividers in clickActions non-selectable on every click combo. + foreach (var cb in new[] { _cboSingleClick, _cboDoubleClick, _cboTripleClick, _cboMiddleClick, _cboMiddleDoubleClick }) + WireTraySeparatorBounce(cb); + // v3.23.0: live readout of what each "AutoLogin 1-4" dropdown entry currently fires. _lblQuickLoginReadout = new Label { Text = "", AutoSize = true, ForeColor = DarkTheme.FgDimGray, Font = DarkTheme.FontUI75 }; cardTray.Full(_lblQuickLoginReadout); @@ -4063,6 +4084,39 @@ private static string TrayActionToDisplay(string action) => /// Convert dropdown display name back to config action name. private static string TrayDisplayToAction(string display) => _trayDisplayActionMap.TryGetValue(display, out var action) ? action : display; + + /// + /// Make the divider rows in the tray-click dropdowns + /// non-selectable: when a divider is landed on, skip PAST it in the direction of travel so + /// keyboard arrows can cross group boundaries (a mouse-click on a divider lands on the adjacent + /// real row); fall back to the last real selection only if the list edge is reached. A divider + /// can therefore never be committed and saved as a tray action — TrayDisplayToAction would pass + /// the divider string through verbatim and AppConfig.Validate would then reject it, silently + /// resetting the click to "None". Mirrors the separator skip in QuickLoginSlotsDialog. + /// Re-entrancy is safe: setting SelectedIndex to a real row re-fires the handler, which takes + /// the non-divider branch (updating lastIndex) and terminates. + /// + private static void WireTraySeparatorBounce(ComboBox cb) + { + int lastIndex = cb.SelectedIndex; + cb.SelectedIndexChanged += (s, _) => + { + var box = (ComboBox)s!; + if ((box.SelectedItem as string) != TrayActionSeparator) + { + lastIndex = box.SelectedIndex; + return; + } + // Skip past the divider in the direction of travel (inferred from lastIndex, since the + // event doesn't say which arrow was pressed); while-loop tolerates adjacent dividers. + // Edge guard: if no real row lies that way, revert to the last real index. + int dir = box.SelectedIndex > lastIndex ? 1 : -1; + int i = box.SelectedIndex + dir; + while (i >= 0 && i < box.Items.Count && (box.Items[i] as string) == TrayActionSeparator) + i += dir; + box.SelectedIndex = (i >= 0 && i < box.Items.Count) ? i : lastIndex; + }; + } } ///