// ActionDialog.cs // Modaler Dialog zum Bearbeiten einer einzelnen Button-/Encoder-Aktion. // // Layout je nach Typ: // HID Tastatur → Capture-Button (Klicken + Taste drücken) + Modifier-Checkboxen // HID Consumer → Dropdown mit benannten Medien-/Lautstärke-Aktionen // Host Command → Zahlen-Eingabe (Command-ID) // Makro → acht HID-Key-Steps // Profilwechsel → Zielprofil oder zyklisch nächstes Profil // Keine Aktion → nichts // // Optional (nur MX-Buttons): LED-Basisfarbe + vier auswählbare Animationen + // Geschwindigkeit. Das Binärmodell akzeptiert zusätzlich drei Animationen. namespace VersaGUI; public class ActionDialog : Form { // ── Ergebnis ────────────────────────────────────────────────────────────── public DeviceAction ResultAction { get; private set; } public Color ResultColor { get; private set; } public LedAnimType ResultAnim { get; private set; } public ushort ResultPeriod { get; private set; } // ── Controls ────────────────────────────────────────────────────────────── private readonly ComboBox _typeCombo; // HID Tastatur private readonly Panel _hidKeyPanel; private readonly Button _captureBtn; private readonly CheckBox _chkCtrl, _chkShift, _chkAlt, _chkWin; // HID Consumer private readonly Panel _consumerPanel; private readonly ComboBox _consumerCombo; // Host Command private readonly Panel _cmdPanel; private readonly TextBox _cmdBox; // Profil wechseln private readonly Panel _profilePanel; private readonly ComboBox _profileCombo; // LED-Farbe private readonly Panel _colorPanel; private readonly Button _colorBtn; private Color _color; // LED-Animation private readonly Panel _animPanel; private readonly ComboBox _animCombo; private readonly ComboBox _periodCombo; // Makro private readonly Panel _macroPanel; private readonly MacroTable _macros; private readonly int _macroSlot; // Je Step: [CaptureBtn, CheckCtrl, CheckShift, CheckAlt] private readonly Button[] _stepBtns = new Button[8]; private readonly CheckBox[] _stepCtrl = new CheckBox[8]; private readonly CheckBox[] _stepShift = new CheckBox[8]; private readonly CheckBox[] _stepAlt = new CheckBox[8]; private readonly byte[] _stepKeycodes = new byte[8]; private int _captureStep = -1; // -1 = nicht im Capture-Modus // Capture-Zustand private bool _capturing; private byte _hidKeycode; // 0 = nicht belegt // Layout private readonly bool _showColor; private Button _okBtn = null!; private Button _cancelBtn = null!; // ── Lookup-Tabellen ─────────────────────────────────────────────────────── // P/Invoke: Windows-API für layout-unabhängige Scan-Code-Konvertierung [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern uint MapVirtualKey(uint uCode, uint uMapType); [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] private static extern int GetKeyNameText(int lParam, System.Text.StringBuilder lpString, int nSize); // Scan-Code (PS/2 Set 1) → HID Usage (physische Tastenposition, Layout-unabhängig). // Korrekt für QWERTZ, AZERTY und andere Layouts: Ö = Scan 0x27 → HID 0x33 (;-Position) private static readonly Dictionary s_scanToHid = new() { [0x01]=0x29, // Escape [0x02]=0x1E,[0x03]=0x1F,[0x04]=0x20,[0x05]=0x21,[0x06]=0x22, [0x07]=0x23,[0x08]=0x24,[0x09]=0x25,[0x0A]=0x26,[0x0B]=0x27, // 1–0 [0x0C]=0x2D,[0x0D]=0x2E, // -/ß =/´ [0x0E]=0x2A, // Backspace [0x0F]=0x2B, // Tab [0x10]=0x14,[0x11]=0x1A,[0x12]=0x08,[0x13]=0x15,[0x14]=0x17, // Q W E R T [0x15]=0x1C,[0x16]=0x18,[0x17]=0x0C,[0x18]=0x12,[0x19]=0x13, // Z/Y U I O P [0x1A]=0x2F,[0x1B]=0x30, // Ü + [0x1C]=0x28, // Enter [0x1E]=0x04,[0x1F]=0x16,[0x20]=0x07,[0x21]=0x09,[0x22]=0x0A, // A S D F G [0x23]=0x0B,[0x24]=0x0D,[0x25]=0x0E,[0x26]=0x0F, // H J K L [0x27]=0x33,[0x28]=0x34,[0x29]=0x35, // Ö Ä ^ [0x2B]=0x31, // # [0x2C]=0x1D,[0x2D]=0x1B,[0x2E]=0x06,[0x2F]=0x19,[0x30]=0x05, // Y/Z X C V B [0x31]=0x11,[0x32]=0x10,[0x33]=0x36,[0x34]=0x37,[0x35]=0x38, // N M , . - [0x37]=0x55, // Numpad * [0x39]=0x2C, // Space [0x3A]=0x39, // CapsLock [0x3B]=0x3A,[0x3C]=0x3B,[0x3D]=0x3C,[0x3E]=0x3D,[0x3F]=0x3E, // F1–F5 [0x40]=0x3F,[0x41]=0x40,[0x42]=0x41,[0x43]=0x42,[0x44]=0x43, // F6–F10 [0x45]=0x53,[0x46]=0x47, // NumLock ScrollLock [0x47]=0x5F,[0x48]=0x60,[0x49]=0x61,[0x4A]=0x56, // Num7 8 9 - [0x4B]=0x5C,[0x4C]=0x5D,[0x4D]=0x5E,[0x4E]=0x57, // Num4 5 6 + [0x4F]=0x59,[0x50]=0x5A,[0x51]=0x5B,[0x52]=0x62,[0x53]=0x63, // Num1 2 3 0 . [0x56]=0x64, // Non-US \ (< > auf QWERTZ) [0x57]=0x44,[0x58]=0x45, // F11 F12 }; // HID → Scan-Code (umgekehrte Tabelle, für Anzeigenamen) private static readonly Dictionary s_hidToScan = s_scanToHid.ToDictionary(kv => kv.Value, kv => kv.Key); // Direkte VK→HID-Zuordnung nur für erweiterte Navigationstasten. // MapVirtualKey gibt für diese den Numpad-Scan-Code zurück (falsch). private static readonly Dictionary s_extVkToHid = new() { [Keys.Up]=0x52, [Keys.Down]=0x51, [Keys.Left]=0x50, [Keys.Right]=0x4F, [Keys.Home]=0x4A, [Keys.End]=0x4D, [Keys.PageUp]=0x4B, [Keys.PageDown]=0x4E, [Keys.Insert]=0x49, [Keys.Delete]=0x4C, [Keys.Pause]=0x48, [Keys.Scroll]=0x47, [Keys.PrintScreen]=0x46, }; // Windows VK → HID via Scan-Code (layout-unabhängig). // Ö auf QWERTZ → Scan 0x27 → HID 0x33 (;-Position auf US) → Board sendet HID 0x33 // → Zielrechner mit QWERTZ-Layout erzeugt korrekt Ö. private static byte VkToHid(Keys vk) { if (s_extVkToHid.TryGetValue(vk, out byte hExt)) return hExt; byte sc = (byte)MapVirtualKey((uint)(vk & Keys.KeyCode), 0u /* VK→SC */); return s_scanToHid.TryGetValue(sc, out byte hid) ? hid : (byte)0; } // HID-Code → lesbarer Tastenname laut aktivem Windows-Tastaturlayout. // Gibt auf QWERTZ "Ö" für HID 0x33, auf US-Layout ";" zurück. private static string HidKeyName(byte hid) { // Sondertasten ohne Scan-Code-Eintrag var special = (Dictionary)new Dictionary { [0x28]="Enter", [0x29]="Escape", [0x2A]="Backspace", [0x2B]="Tab", [0x2C]="Leer", [0x39]="Caps", [0x46]="Druck", [0x47]="Rollen", [0x48]="Pause", [0x49]="Einfg", [0x4A]="Pos1", [0x4B]="Bild↑", [0x4C]="Entf", [0x4D]="Ende", [0x4E]="Bild↓", [0x4F]="→", [0x50]="←", [0x51]="↓", [0x52]="↑", [0x53]="NumLock",[0x54]="Num/", [0x55]="Num*", [0x56]="Num-", [0x57]="Num+", [0x58]="NumEnter", [0x59]="Num1", [0x5A]="Num2", [0x5B]="Num3", [0x5C]="Num4", [0x5D]="Num5", [0x5E]="Num6", [0x5F]="Num7", [0x60]="Num8", [0x61]="Num9", [0x62]="Num0", [0x63]="Num.", [0x3A]="F1",[0x3B]="F2",[0x3C]="F3",[0x3D]="F4",[0x3E]="F5", [0x3F]="F6",[0x40]="F7",[0x41]="F8",[0x42]="F9",[0x43]="F10", [0x44]="F11",[0x45]="F12", }; if (special.TryGetValue(hid, out string? name)) return name; // Für Zeichen-Tasten: GetKeyNameText gibt den lokalisierten Namen zurück if (!s_hidToScan.TryGetValue(hid, out byte sc)) return $"0x{hid:X2}"; int lParam = sc << 16; var sb = new System.Text.StringBuilder(32); GetKeyNameText(lParam, sb, sb.Capacity); return sb.Length > 0 ? sb.ToString() : $"0x{hid:X2}"; } // HID Consumer Usage IDs (Usage Page 0x0C) private static readonly (ushort Usage, string Name)[] s_consumer = { (0x00CD, "Play / Pause"), (0x00B5, "Nächster Titel"), (0x00B6, "Vorheriger Titel"), (0x00B7, "Stop"), (0x00E9, "Lauter"), (0x00EA, "Leiser"), (0x00E2, "Stummschalten"), (0x0192, "Taschenrechner"), (0x0223, "Browser – Startseite"), (0x0224, "Browser – Zurück"), (0x0225, "Browser – Vor"), (0x00B0, "Aufnahme"), }; // ── Konstruktor ─────────────────────────────────────────────────────────── // Animations-ComboBox-Einträge: (Anzeigename, LedAnimType, Farbe sinnvoll?) private static readonly (string Name, LedAnimType Type, bool ShowColor)[] s_anims = { ("Statisch", LedAnimType.Static, true), ("Blinken", LedAnimType.Blink, true), ("Pulsieren", LedAnimType.Pulse, true), ("Regenbogen", LedAnimType.ColorCycle, false), }; // Geschwindigkeits-Presets: (Anzeigename, Millisekunden) private static readonly (string Name, ushort Ms)[] s_periods = { ("Schnell (0.5s)", 500), ("Mittel (1s)", 1000), ("Langsam (2s)", 2000), ("Sehr langsam (4s)", 4000), }; public ActionDialog(DeviceAction action, Color ledColor, LedAnimType ledAnim = LedAnimType.ColorCycle, ushort ledPeriod = 4000, MacroTable? macros = null, int macroSlot = 0, bool showColor = true) { ResultAction = action.Clone(); ResultColor = ledColor; ResultAnim = ledAnim; ResultPeriod = ledPeriod; _color = ledColor; _macros = macros ?? new MacroTable(); _macroSlot = macroSlot; _showColor = showColor; // Bestehende Makro-Steps aus der Tabelle laden for (int i = 0; i < 8; i++) _stepKeycodes[i] = _macros.Steps[_macroSlot, i].Keycode; // Formular-Grundeinstellungen Text = "Aktion bearbeiten"; FormBorderStyle = FormBorderStyle.FixedDialog; StartPosition = FormStartPosition.CenterParent; MinimizeBox = false; MaximizeBox = false; KeyPreview = true; // Tastendrücke abfangen bevor Controls sie kriegen // ── Typ-Auswahl ─────────────────────────────────────────────────────── var typeLabel = new Label { Text = "Typ:", Location = new Point(12, 16), AutoSize = true }; _typeCombo = new ComboBox { Location = new Point(80, 12), Width = 316, DropDownStyle = ComboBoxStyle.DropDownList, }; _typeCombo.Items.AddRange(new object[] { "Keine Aktion", "HID Tastatur", "HID Consumer", "Host Command", "Makro", "Profil wechseln" }); _typeCombo.SelectedIndex = (int)action.Type; _typeCombo.SelectedIndexChanged += OnTypeChanged; // ── HID-Tastatur-Panel ──────────────────────────────────────────────── _hidKeyPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 110) }; _captureBtn = new Button { Size = new Size(396, 48), Location = new Point(0, 0), FlatStyle = FlatStyle.Flat, Font = new Font(Font.FontFamily, 12, FontStyle.Regular), Cursor = Cursors.Hand, }; _captureBtn.FlatAppearance.BorderColor = Color.DarkGray; _captureBtn.Click += OnCaptureClick; var hint = new Label { Text = "Klicken und dann Taste drücken | Esc = Abbrechen", Location = new Point(0, 54), AutoSize = true, ForeColor = SystemColors.GrayText, Font = new Font(Font.FontFamily, 8), }; _chkCtrl = new CheckBox { Text = "Strg", Location = new Point(0, 78), AutoSize = true }; _chkShift = new CheckBox { Text = "Shift", Location = new Point(60, 78), AutoSize = true }; _chkAlt = new CheckBox { Text = "Alt", Location = new Point(120, 78), AutoSize = true }; _chkWin = new CheckBox { Text = "Win", Location = new Point(176, 78), AutoSize = true }; _chkCtrl.CheckedChanged += (_, _) => RefreshCaptureBtn(); _chkShift.CheckedChanged += (_, _) => RefreshCaptureBtn(); _chkAlt.CheckedChanged += (_, _) => RefreshCaptureBtn(); _chkWin.CheckedChanged += (_, _) => RefreshCaptureBtn(); // Aus bestehender Aktion befüllen if (action.Type == ActionType.HidKey) { _hidKeycode = (byte)(action.Data & 0xFF); byte mod = (byte)(action.Data >> 8); _chkCtrl.Checked = (mod & 0x11) != 0; _chkShift.Checked = (mod & 0x22) != 0; _chkAlt.Checked = (mod & 0x44) != 0; _chkWin.Checked = (mod & 0x88) != 0; } _hidKeyPanel.Controls.AddRange(new Control[] { _captureBtn, hint, _chkCtrl, _chkShift, _chkAlt, _chkWin }); RefreshCaptureBtn(); // erst nach Checkbox-Initialisierung aufrufen // ── Consumer-Panel ──────────────────────────────────────────────────── _consumerPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) }; _consumerCombo = new ComboBox { Location = new Point(0, 0), Width = 396, DropDownStyle = ComboBoxStyle.DropDownList, }; foreach (var (_, name) in s_consumer) _consumerCombo.Items.Add(name); int consumerIdx = action.Type == ActionType.HidConsumer ? Array.FindIndex(s_consumer, e => e.Usage == action.Data) : -1; _consumerCombo.SelectedIndex = consumerIdx >= 0 ? consumerIdx : 0; _consumerPanel.Controls.Add(_consumerCombo); // ── Host-Command-Panel ──────────────────────────────────────────────── _cmdPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) }; var cmdLabel = new Label { Text = "Command-ID:", Location = new Point(0, 8), AutoSize = true }; _cmdBox = new TextBox { Location = new Point(90, 4), Width = 306, Text = action.Type == ActionType.HostCommand ? $"{action.Data}" : "0", }; _cmdPanel.Controls.AddRange(new Control[] { cmdLabel, _cmdBox }); // ── Profil-Panel ────────────────────────────────────────────────────── _profilePanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 30) }; var profileLabel = new Label { Text = "Ziel-Profil:", Location = new Point(0, 8), AutoSize = true }; _profileCombo = new ComboBox { Location = new Point(90, 4), Width = 200, DropDownStyle = ComboBoxStyle.DropDownList, }; _profileCombo.Items.AddRange(new object[] { "Nächstes Profil (Zyklus)", "Profil 1", "Profil 2", "Profil 3" }); _profileCombo.SelectedIndex = action.Type == ActionType.ProfileSwitch ? (action.Data is 0x00FF or 0xFFFF ? 0 : Math.Clamp((int)action.Data + 1, 1, 3)) : 0; _profilePanel.Controls.AddRange(new Control[] { profileLabel, _profileCombo }); // ── Farb-Panel ──────────────────────────────────────────────────────── _colorPanel = new Panel { Location = new Point(12, 168), Size = new Size(396, 30), Visible = showColor, }; var colorLabel = new Label { Text = "LED-Farbe:", Location = new Point(0, 8), AutoSize = true }; _colorBtn = new Button { Location = new Point(80, 2), Size = new Size(316, 26), BackColor = ledColor, FlatStyle = FlatStyle.Flat, Text = string.Empty, }; _colorBtn.FlatAppearance.BorderSize = 1; _colorBtn.Click += OnColorClick; _colorPanel.Controls.AddRange(new Control[] { colorLabel, _colorBtn }); // ── Animation-Panel ─────────────────────────────────────────────────── _animPanel = new Panel { Location = new Point(12, 208), Size = new Size(396, 58), Visible = showColor, }; var animLabel = new Label { Text = "Animation:", Location = new Point(0, 8), AutoSize = true }; _animCombo = new ComboBox { Location = new Point(80, 4), Width = 316, DropDownStyle = ComboBoxStyle.DropDownList, }; foreach (var (name, _, _) in s_anims) _animCombo.Items.Add(name); int animIdx = Array.FindIndex(s_anims, a => a.Type == ledAnim); _animCombo.SelectedIndex = animIdx >= 0 ? animIdx : 0; _animCombo.SelectedIndexChanged += OnAnimChanged; var periodLabel = new Label { Text = "Tempo:", Location = new Point(0, 36), AutoSize = true }; _periodCombo = new ComboBox { Location = new Point(80, 32), Width = 316, DropDownStyle = ComboBoxStyle.DropDownList, }; foreach (var (name, _) in s_periods) _periodCombo.Items.Add(name); int periodIdx = Array.FindIndex(s_periods, p => p.Ms == ledPeriod); _periodCombo.SelectedIndex = periodIdx >= 0 ? periodIdx : 3; // Default: sehr langsam _animPanel.Controls.AddRange(new Control[] { animLabel, _animCombo, periodLabel, _periodCombo }); // ── Makro-Panel (8 Steps) ───────────────────────────────────────────── _macroPanel = new Panel { Location = new Point(12, 48), Size = new Size(396, 8 * 30 + 22) }; var macroHint = new Label { Text = "Klicke einen Step, dann Taste drücken. Der erste leere Step beendet das Makro.", Location = new Point(0, 0), Size = new Size(396, 28), ForeColor = SystemColors.GrayText, Font = new Font(Font.FontFamily, 8), }; _macroPanel.Controls.Add(macroHint); for (int step = 0; step < 8; step++) { int s = step; int y2 = 30 + step * 30; var stepLabel = new Label { Text = $"Step {step + 1}:", Location = new Point(0, y2 + 6), AutoSize = true, }; _stepBtns[step] = new Button { Location = new Point(50, y2), Size = new Size(160, 24), FlatStyle = FlatStyle.Flat, Cursor = Cursors.Hand, }; _stepBtns[step].FlatAppearance.BorderColor = Color.DarkGray; _stepBtns[step].Click += (_, _) => StartMacroCapture(s); _stepCtrl[step] = new CheckBox { Text = "Strg", Location = new Point(218, y2 + 4), AutoSize = true }; _stepShift[step] = new CheckBox { Text = "Shift", Location = new Point(268, y2 + 4), AutoSize = true }; _stepAlt[step] = new CheckBox { Text = "Alt", Location = new Point(326, y2 + 4), AutoSize = true }; // Bestehende Werte aus Makro-Tabelle laden _stepCtrl[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x01) != 0; _stepShift[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x02) != 0; _stepAlt[step].Checked = (_macros.Steps[_macroSlot, step].Modifier & 0x04) != 0; _stepCtrl[step].CheckedChanged += (_, _) => RefreshStepBtn(s); _stepShift[step].CheckedChanged += (_, _) => RefreshStepBtn(s); _stepAlt[step].CheckedChanged += (_, _) => RefreshStepBtn(s); _macroPanel.Controls.AddRange(new Control[] { stepLabel, _stepBtns[step], _stepCtrl[step], _stepShift[step], _stepAlt[step] }); RefreshStepBtn(step); } // ── Buttons Fußzeile ───────────────────────────────────────────────── _okBtn = new Button { Text = "OK", DialogResult = DialogResult.OK, Width = 80, }; _okBtn.Click += OnOk; _cancelBtn = new Button { Text = "Abbrechen", DialogResult = DialogResult.Cancel, Width = 92, }; AcceptButton = _okBtn; CancelButton = _cancelBtn; Controls.AddRange(new Control[] { typeLabel, _typeCombo, _hidKeyPanel, _consumerPanel, _cmdPanel, _profilePanel, _macroPanel, _colorPanel, _animPanel, _okBtn, _cancelBtn, }); UpdatePanelVisibility(); // setzt Visibility + ruft UpdateLayout auf } // ── Panel-Sichtbarkeit ──────────────────────────────────────────────────── private void OnTypeChanged(object? sender, EventArgs e) { StopCapture(); UpdatePanelVisibility(); } private void OnAnimChanged(object? sender, EventArgs e) { UpdateAnimVisibility(); } private void UpdatePanelVisibility() { var t = (ActionType)_typeCombo.SelectedIndex; _hidKeyPanel.Visible = t == ActionType.HidKey; _consumerPanel.Visible = t == ActionType.HidConsumer; _cmdPanel.Visible = t == ActionType.HostCommand; _profilePanel.Visible = t == ActionType.ProfileSwitch; _macroPanel.Visible = t == ActionType.Macro; _colorPanel.Visible = _showColor; _animPanel.Visible = _showColor; UpdateAnimVisibility(); UpdateLayout(); } // Positioniert LED-Panels dynamisch unterhalb des aktiven Typ-Panels // und passt die Formulargröße an. private void UpdateLayout() { var t = (ActionType)_typeCombo.SelectedIndex; int contentH = t switch { ActionType.HidKey => 110, ActionType.HidConsumer => 30, ActionType.HostCommand => 30, ActionType.ProfileSwitch => 30, ActionType.Macro => 8 * 30 + 22, _ => 0, }; int ledY = 48 + contentH + 10; int footerY = ledY; if (_showColor) { _colorPanel.Location = new Point(12, ledY); _animPanel.Location = new Point(12, ledY + 38); footerY = ledY + 38 + 62; } _okBtn.Location = new Point(224, footerY); _cancelBtn.Location = new Point(312, footerY); ClientSize = new Size(420, footerY + 44); } private void UpdateAnimVisibility() { if (_animPanel.Visible && _animCombo.SelectedIndex >= 0) { var (_, animType, showColorFlag) = s_anims[_animCombo.SelectedIndex]; _colorBtn.Enabled = showColorFlag; _periodCombo.Enabled = animType != LedAnimType.Static; } } // ── Tasten-Erfassung ────────────────────────────────────────────────────── private void OnCaptureClick(object? sender, EventArgs e) { if (_capturing) StopCapture(); else StartCapture(); } // ── Makro-Step-Capture ──────────────────────────────────────────────────── private void StartMacroCapture(int step) { _captureStep = step; _stepBtns[step].Text = "Taste drücken..."; _stepBtns[step].BackColor = Color.FromArgb(255, 220, 80); _stepBtns[step].ForeColor = Color.Black; } private void RefreshStepBtn(int step) { if (_captureStep == step) return; // gerade im Capture-Modus byte kc = _stepKeycodes[step]; if (kc == 0) { _stepBtns[step].Text = "— (leer)"; _stepBtns[step].BackColor = SystemColors.Control; _stepBtns[step].ForeColor = SystemColors.GrayText; } else { string name = HidKeyName(kc); var parts = new List(); if (_stepCtrl[step].Checked) parts.Add("Strg"); if (_stepShift[step].Checked) parts.Add("Shift"); if (_stepAlt[step].Checked) parts.Add("Alt"); parts.Add(name); _stepBtns[step].Text = string.Join("+", parts); _stepBtns[step].BackColor = SystemColors.Control; _stepBtns[step].ForeColor = SystemColors.ControlText; } } private void StartCapture() { _capturing = true; _captureBtn.Text = "Taste drücken..."; _captureBtn.BackColor = Color.FromArgb(255, 220, 80); _captureBtn.ForeColor = Color.Black; _captureBtn.FlatAppearance.BorderColor = Color.DarkOrange; } private void StopCapture() { _capturing = false; RefreshCaptureBtn(); } private void RefreshCaptureBtn() { if (_hidKeycode != 0) { var parts = new List(); if (_chkCtrl.Checked) parts.Add("Strg"); if (_chkShift.Checked) parts.Add("Shift"); if (_chkAlt.Checked) parts.Add("Alt"); if (_chkWin.Checked) parts.Add("Win"); parts.Add(HidKeyName(_hidKeycode)); _captureBtn.Text = string.Join(" + ", parts); } else { _captureBtn.Text = "— (klicken zum Erfassen)"; } _captureBtn.BackColor = SystemColors.Control; _captureBtn.ForeColor = SystemColors.ControlText; _captureBtn.FlatAppearance.BorderColor = Color.DarkGray; } // ProcessCmdKey wird vor jeder Dialog-Verarbeitung aufgerufen (Enter, Pfeiltasten, // Tab usw. werden normalerweise als "Dialog-Tasten" verschluckt, bevor OnKeyDown // feuert). Daher hier die Capture-Logik – so kommen auch ↑↓←→ und Enter an. protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { const int WM_KEYDOWN = 0x0100; const int WM_SYSKEYDOWN = 0x0104; if (msg.Msg != WM_KEYDOWN && msg.Msg != WM_SYSKEYDOWN) return base.ProcessCmdKey(ref msg, keyData); if (!_capturing && _captureStep < 0) return base.ProcessCmdKey(ref msg, keyData); Keys vk = keyData & Keys.KeyCode; bool ctrl = (keyData & Keys.Control) != 0; bool shift = (keyData & Keys.Shift) != 0; bool alt = (keyData & Keys.Alt) != 0; // Reine Modifier-Tasten ignorieren – auf echte Taste warten if (vk is Keys.ControlKey or Keys.LControlKey or Keys.RControlKey or Keys.ShiftKey or Keys.LShiftKey or Keys.RShiftKey or Keys.Menu or Keys.LMenu or Keys.RMenu or Keys.LWin or Keys.RWin or Keys.Apps) return true; // schlucken, aber Capture läuft weiter // ── Makro-Step-Capture ──────────────────────────────────────────────── if (_captureStep >= 0) { int s = _captureStep; _captureStep = -1; if (vk == Keys.Escape) { _stepKeycodes[s] = 0; } else { _stepKeycodes[s] = VkToHid(vk); _stepCtrl[s].Checked = ctrl; _stepShift[s].Checked = shift; _stepAlt[s].Checked = alt; } RefreshStepBtn(s); return true; } // ── HID-Tastatur-Capture ────────────────────────────────────────────── if (vk == Keys.Escape) { StopCapture(); } else { _chkCtrl.Checked = ctrl; _chkShift.Checked = shift; _chkAlt.Checked = alt; _hidKeycode = VkToHid(vk); StopCapture(); } return true; } // ── Farbe ───────────────────────────────────────────────────────────────── private void OnColorClick(object? sender, EventArgs e) { using var dlg = new ColorDialog { Color = _color, FullOpen = true }; if (dlg.ShowDialog(this) == DialogResult.OK) { _color = dlg.Color; _colorBtn.BackColor = dlg.Color; } } // ── OK ──────────────────────────────────────────────────────────────────── private void OnOk(object? sender, EventArgs e) { var type = (ActionType)_typeCombo.SelectedIndex; ushort data = 0; switch (type) { case ActionType.HidKey: byte mod = 0; if (_chkCtrl.Checked) mod |= 0x01; // LCtrl if (_chkShift.Checked) mod |= 0x02; // LShift if (_chkAlt.Checked) mod |= 0x04; // LAlt if (_chkWin.Checked) mod |= 0x08; // LGUI data = (ushort)(_hidKeycode | (mod << 8)); break; case ActionType.HidConsumer: data = s_consumer[_consumerCombo.SelectedIndex].Usage; break; case ActionType.HostCommand: if (!ushort.TryParse(_cmdBox.Text.Trim(), out data)) { MessageBox.Show("Ungültige Command-ID (0–65535 erwartet).", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); DialogResult = DialogResult.None; return; } break; case ActionType.ProfileSwitch: data = _profileCombo.SelectedIndex == 0 ? (ushort)0xFFFF // Zyklus: Firmware rechnet (current+1)%3 : (ushort)(_profileCombo.SelectedIndex - 1); // Profil 0/1/2 break; } if (type == ActionType.Macro) { // Steps in die Makro-Tabelle schreiben for (int i = 0; i < 8; i++) { byte mod = 0; if (_stepCtrl[i].Checked) mod |= 0x01; if (_stepShift[i].Checked) mod |= 0x02; if (_stepAlt[i].Checked) mod |= 0x04; _macros.Steps[_macroSlot, i].Keycode = _stepKeycodes[i]; _macros.Steps[_macroSlot, i].Modifier = mod; } data = (ushort)_macroSlot; } ResultAction = new DeviceAction { Type = type, Data = data }; ResultColor = _color; if (_animPanel.Visible && _animCombo.SelectedIndex >= 0) { ResultAnim = s_anims[_animCombo.SelectedIndex].Type; ResultPeriod = _periodCombo.SelectedIndex >= 0 ? s_periods[_periodCombo.SelectedIndex].Ms : (ushort)4000; } } }