399 lines
14 KiB
C#
399 lines
14 KiB
C#
// ConfigForm.cs
|
||
// Hauptfenster der VersaPad-Konfiguration.
|
||
//
|
||
// Layout:
|
||
// ┌── Tasten (4 × 5 Grid) ──────────────────────────────────────────────┐
|
||
// │ Jede Taste = Button mit Hintergrundfarbe (= LED-Base) und │
|
||
// │ Aktionsbeschriftung. Klick öffnet ActionDialog. │
|
||
// └────────────────────────────────────────────────────────────────────┘
|
||
// ┌── Encoder ─────────────────────────────────────────────────────────┐
|
||
// │ 4 Zeilen à [SW][CW][CCW] – ohne LED-Farbe │
|
||
// └────────────────────────────────────────────────────────────────────┘
|
||
// [Auf Board speichern] [Schließen]
|
||
//
|
||
// "Auf Board speichern" sendet die Config in 6-Byte-Chunks über Serial.
|
||
// Firmware-Seite (WRITE_CONFIG) ist noch TODO → Schaltfläche ist
|
||
// deaktiviert wenn das Board nicht verbunden ist.
|
||
|
||
namespace VersaGUI;
|
||
|
||
public class ConfigForm : Form
|
||
{
|
||
private readonly DeviceConfig _config;
|
||
private readonly MacroTable _macros;
|
||
private readonly SerialManager _serial;
|
||
|
||
private readonly Button[] _btnGrid = new Button[20];
|
||
private readonly Button[,] _encBtns = new Button[4, 3];
|
||
private Button _saveBtn = null!;
|
||
|
||
public ConfigForm(DeviceConfig config, MacroTable macros, SerialManager serial)
|
||
{
|
||
_config = config;
|
||
_macros = macros;
|
||
_serial = serial;
|
||
|
||
Text = "VersaPad – Konfiguration";
|
||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
MaximizeBox = false;
|
||
ClientSize = new Size(520, 480);
|
||
|
||
int y = 12;
|
||
y = BuildButtonGrid(y);
|
||
y = BuildEncoderPanel(y);
|
||
BuildFooter(y);
|
||
|
||
// Verbindungsstatus → Save-Button
|
||
_serial.Connected += (_, _) => InvokeOnUi(() => _saveBtn.Enabled = true);
|
||
_serial.Disconnected += (_, _) => InvokeOnUi(() => _saveBtn.Enabled = false);
|
||
_saveBtn.Enabled = _serial.IsConnected;
|
||
}
|
||
|
||
// ── Tasten-Grid (4 Spalten × 5 Zeilen) ───────────────────────────────────
|
||
|
||
private int BuildButtonGrid(int startY)
|
||
{
|
||
var group = new GroupBox
|
||
{
|
||
Text = "Tasten",
|
||
Location = new Point(12, startY),
|
||
Size = new Size(496, 220),
|
||
};
|
||
|
||
var grid = new TableLayoutPanel
|
||
{
|
||
Location = new Point(8, 20),
|
||
Size = new Size(480, 192),
|
||
ColumnCount = 4,
|
||
RowCount = 5,
|
||
CellBorderStyle = TableLayoutPanelCellBorderStyle.Single,
|
||
};
|
||
|
||
// Gleichmäßige Spalten/Zeilen
|
||
for (int c = 0; c < 4; c++)
|
||
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25f));
|
||
for (int r = 0; r < 5; r++)
|
||
grid.RowStyles.Add(new RowStyle(SizeType.Percent, 20f));
|
||
|
||
// MX-Buttons: col=0..3 (physisch COL1..4), row=0..4
|
||
// mx_idx = col * 5 + row (entspricht key_id - 5)
|
||
for (int col = 0; col < 4; col++)
|
||
{
|
||
for (int row = 0; row < 5; row++)
|
||
{
|
||
int mxIdx = col * 5 + row;
|
||
var btn = new Button
|
||
{
|
||
Dock = DockStyle.Fill,
|
||
Margin = new Padding(2),
|
||
FlatStyle = FlatStyle.Flat,
|
||
Tag = mxIdx,
|
||
};
|
||
btn.FlatAppearance.BorderSize = 0;
|
||
btn.Click += OnMxButtonClick;
|
||
_btnGrid[mxIdx] = btn;
|
||
grid.Controls.Add(btn, col, row);
|
||
RefreshMxButton(mxIdx);
|
||
}
|
||
}
|
||
|
||
group.Controls.Add(grid);
|
||
Controls.Add(group);
|
||
return startY + group.Height + 8;
|
||
}
|
||
|
||
// ── Encoder-Panel ─────────────────────────────────────────────────────────
|
||
|
||
private int BuildEncoderPanel(int startY)
|
||
{
|
||
var group = new GroupBox
|
||
{
|
||
Text = "Encoder",
|
||
Location = new Point(12, startY),
|
||
Size = new Size(496, 145),
|
||
};
|
||
|
||
string[] labels = { "SW", "CW", "CCW" };
|
||
|
||
// Header-Zeile
|
||
for (int i = 0; i < 3; i++)
|
||
{
|
||
group.Controls.Add(new Label
|
||
{
|
||
Text = labels[i],
|
||
Location = new Point(110 + i * 122, 22),
|
||
AutoSize = true,
|
||
Font = new Font(Font, FontStyle.Bold),
|
||
TextAlign = ContentAlignment.MiddleCenter,
|
||
});
|
||
}
|
||
|
||
for (int enc = 0; enc < 4; enc++)
|
||
{
|
||
group.Controls.Add(new Label
|
||
{
|
||
Text = $"ENC {enc}:",
|
||
Location = new Point(10, 44 + enc * 26),
|
||
AutoSize = true,
|
||
});
|
||
|
||
for (int act = 0; act < 3; act++)
|
||
{
|
||
int e = enc, a = act;
|
||
var btn = new Button
|
||
{
|
||
Location = new Point(80 + act * 122, 40 + enc * 26),
|
||
Size = new Size(116, 22),
|
||
FlatStyle = FlatStyle.Flat,
|
||
Tag = (e, a),
|
||
};
|
||
btn.FlatAppearance.BorderColor = Color.Gray;
|
||
btn.Click += OnEncButtonClick;
|
||
_encBtns[enc, act] = btn;
|
||
group.Controls.Add(btn);
|
||
RefreshEncButton(enc, act);
|
||
}
|
||
}
|
||
|
||
Controls.Add(group);
|
||
return startY + group.Height + 8;
|
||
}
|
||
|
||
// ── Fußzeile ──────────────────────────────────────────────────────────────
|
||
|
||
private void BuildFooter(int startY)
|
||
{
|
||
_saveBtn = new Button
|
||
{
|
||
Text = "Auf Board speichern",
|
||
Location = new Point(12, startY),
|
||
Size = new Size(160, 30),
|
||
};
|
||
_saveBtn.Click += OnSave;
|
||
|
||
var pingBtn = new Button
|
||
{
|
||
Text = "Ping",
|
||
Location = new Point(184, startY),
|
||
Size = new Size(60, 30),
|
||
};
|
||
pingBtn.Click += (_, _) => _serial.Send(new SerialPacket(Protocol.CmdPing));
|
||
|
||
var exportBtn = new Button
|
||
{
|
||
Text = "Exportieren",
|
||
Location = new Point(256, startY),
|
||
Size = new Size(80, 30),
|
||
};
|
||
exportBtn.Click += OnExport;
|
||
|
||
var importBtn = new Button
|
||
{
|
||
Text = "Importieren",
|
||
Location = new Point(340, startY),
|
||
Size = new Size(80, 30),
|
||
};
|
||
importBtn.Click += OnImport;
|
||
|
||
var closeBtn = new Button
|
||
{
|
||
Text = "Schließen",
|
||
Location = new Point(424, startY),
|
||
Size = new Size(84, 30),
|
||
};
|
||
closeBtn.Click += (_, _) => Close();
|
||
|
||
Controls.AddRange(new Control[] { _saveBtn, pingBtn, exportBtn, importBtn, closeBtn });
|
||
|
||
// Fensterhöhe anpassen
|
||
ClientSize = new Size(ClientSize.Width, startY + 50);
|
||
}
|
||
|
||
// ── Klick-Handler ─────────────────────────────────────────────────────────
|
||
|
||
private void OnMxButtonClick(object? sender, EventArgs e)
|
||
{
|
||
if (sender is not Button btn || btn.Tag is not int mxIdx) return;
|
||
|
||
int slot = MacroTable.SlotForMx(mxIdx);
|
||
using var dlg = new ActionDialog(
|
||
_config.MxActions[mxIdx],
|
||
_config.LedBase[mxIdx],
|
||
_config.LedAnim[mxIdx],
|
||
_config.LedPeriod[mxIdx],
|
||
_macros, slot,
|
||
showColor: true);
|
||
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
_config.MxActions[mxIdx] = dlg.ResultAction;
|
||
_config.LedBase[mxIdx] = dlg.ResultColor;
|
||
_config.LedAnim[mxIdx] = dlg.ResultAnim;
|
||
_config.LedPeriod[mxIdx] = dlg.ResultPeriod;
|
||
RefreshMxButton(mxIdx);
|
||
}
|
||
|
||
private void OnEncButtonClick(object? sender, EventArgs e)
|
||
{
|
||
if (sender is not Button btn || btn.Tag is not (int enc, int act)) return;
|
||
|
||
int slot = MacroTable.SlotForEncoder(enc, act);
|
||
using var dlg = new ActionDialog(
|
||
_config.EncActions[enc, act],
|
||
Color.Black,
|
||
LedAnimType.Static, 0,
|
||
_macros, slot,
|
||
showColor: false);
|
||
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
_config.EncActions[enc, act] = dlg.ResultAction;
|
||
RefreshEncButton(enc, act);
|
||
}
|
||
|
||
private void OnSave(object? sender, EventArgs e)
|
||
{
|
||
_saveBtn.Enabled = false;
|
||
_saveBtn.Text = "Wird gesendet...";
|
||
|
||
// SendConfig + SendMacros blockieren ~400ms → Background-Thread
|
||
Task.Run(() =>
|
||
{
|
||
_serial.SendConfig(_config);
|
||
Thread.Sleep(50); // kurze Pause zwischen Config- und Makro-Transfer
|
||
_serial.SendMacros(_macros);
|
||
InvokeOnUi(() =>
|
||
{
|
||
_saveBtn.Text = "Auf Board speichern";
|
||
_saveBtn.Enabled = _serial.IsConnected;
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Anzeige aktualisieren ─────────────────────────────────────────────────
|
||
|
||
// Alle Buttons neu zeichnen (z.B. nach Config-Laden vom Board)
|
||
public void RefreshAll()
|
||
{
|
||
for (int i = 0; i < 20; i++) RefreshMxButton(i);
|
||
for (int enc = 0; enc < 4; enc++)
|
||
for (int act = 0; act < 3; act++)
|
||
RefreshEncButton(enc, act);
|
||
}
|
||
|
||
private void RefreshMxButton(int mxIdx)
|
||
{
|
||
var btn = _btnGrid[mxIdx];
|
||
var action = _config.MxActions[mxIdx];
|
||
var color = _config.LedBase[mxIdx];
|
||
var anim = _config.LedAnim[mxIdx];
|
||
|
||
// Regenbogen: Hintergrund als Verlauf andeuten (dunkles Grau als Neutral)
|
||
if (anim == LedAnimType.ColorCycle)
|
||
{
|
||
btn.BackColor = Color.FromArgb(40, 40, 40);
|
||
btn.ForeColor = Color.White;
|
||
}
|
||
else
|
||
{
|
||
btn.BackColor = color;
|
||
btn.ForeColor = Luminance(color) > 128 ? Color.Black : Color.White;
|
||
}
|
||
|
||
string animName = anim switch
|
||
{
|
||
LedAnimType.Blink => " [Blinken]",
|
||
LedAnimType.Pulse => " [Pulsieren]",
|
||
LedAnimType.ColorCycle => " [Regenbogen]",
|
||
_ => "",
|
||
};
|
||
btn.Text = action.Display + animName;
|
||
btn.ToolTipText(action.TypeDescription());
|
||
}
|
||
|
||
private void RefreshEncButton(int enc, int act)
|
||
{
|
||
var btn = _encBtns[enc, act];
|
||
var action = _config.EncActions[enc, act];
|
||
btn.Text = action.Display;
|
||
}
|
||
|
||
// Helligkeit einer Farbe (0–255) für Kontrast-Entscheidung
|
||
private static int Luminance(Color c)
|
||
=> (c.R * 299 + c.G * 587 + c.B * 114) / 1000;
|
||
|
||
// ── Import / Export ───────────────────────────────────────────────────────
|
||
|
||
private void OnExport(object? sender, EventArgs e)
|
||
{
|
||
using var dlg = new SaveFileDialog
|
||
{
|
||
Title = "Konfiguration exportieren",
|
||
Filter = "VersaPad Config (*.json)|*.json",
|
||
DefaultExt = "json",
|
||
FileName = "versapad_config.json",
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
try
|
||
{
|
||
string json = ConfigJson.Serialize(_config);
|
||
File.WriteAllText(dlg.FileName, json, System.Text.Encoding.UTF8);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"Export fehlgeschlagen:\n{ex.Message}",
|
||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private void OnImport(object? sender, EventArgs e)
|
||
{
|
||
using var dlg = new OpenFileDialog
|
||
{
|
||
Title = "Konfiguration importieren",
|
||
Filter = "VersaPad Config (*.json)|*.json",
|
||
};
|
||
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
||
|
||
try
|
||
{
|
||
string json = File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8);
|
||
ConfigJson.Deserialize(json, _config);
|
||
RefreshAll();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show($"Import fehlgeschlagen:\n{ex.Message}",
|
||
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
private void InvokeOnUi(Action a)
|
||
{
|
||
if (InvokeRequired) Invoke(a);
|
||
else a();
|
||
}
|
||
}
|
||
|
||
// ── Erweiterungsmethoden ──────────────────────────────────────────────────────
|
||
|
||
internal static class Extensions
|
||
{
|
||
// Button.ToolTipText existiert in WinForms nicht direkt – ToolTip-Komponente nötig.
|
||
// Wir verwenden einen statischen ToolTip für alle Buttons.
|
||
private static readonly ToolTip _tip = new();
|
||
|
||
public static void ToolTipText(this Control ctrl, string text)
|
||
=> _tip.SetToolTip(ctrl, text);
|
||
|
||
public static string TypeDescription(this DeviceAction a) => a.Type switch
|
||
{
|
||
ActionType.HidKey => "HID Tastatur-Keycode",
|
||
ActionType.HidConsumer => "HID Consumer Control (Media/Volume)",
|
||
ActionType.HostCommand => "Host Command – App führt aus",
|
||
_ => "Keine Aktion",
|
||
};
|
||
}
|