VersaGUI/src/ConfigForm.cs

399 lines
14 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.

// 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 (0255) 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",
};
}