Files
VersaGUI/src/ActionDialog.cs
T

775 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<byte, byte> 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, // 10
[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, // F1F5
[0x40]=0x3F,[0x41]=0x40,[0x42]=0x41,[0x43]=0x42,[0x44]=0x43, // F6F10
[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<byte, byte> 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<Keys, byte> 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<byte, string>)new Dictionary<byte, string>
{
[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<string>();
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<string>();
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 (065535 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;
}
}
}