From 041a77e39c3d74b8c654a41526581786f419aca5 Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 17 Jun 2026 19:28:17 +0100 Subject: [PATCH 1/6] Replace docked prefs panel with modal settings dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New SettingsWindow with always-visible top tab bar (Lucide icons + labels) and scrollable content area. Tab clicks swap content in-place — no more two-tier drill-down navigation. - Created SettingsWindow.axaml/.cs with tab bar, Done button - Removed Back button from SettingsPanel (tab bar handles navigation) - Removed BackRequested event from SettingsPanel - Removed old docked prefs panel (PrefsTabStrip/PrefsContentPanel) from MainWindow and all related BuildPrefsTabs/ActivatePrefsTab/OnPrefsTabClick/ OnSettingsBack methods - Prefs button now opens SettingsWindow as ShowDialog (modal) - Event callbacks wired via SettingsWindowCallbacks class - Dark mode support via DynamicResource bindings --- src/Dasher.Windows/Controls/SettingsPanel.cs | 14 -- src/Dasher.Windows/Views/MainWindow.axaml | 13 -- src/Dasher.Windows/Views/MainWindow.axaml.cs | 95 ++------------ src/Dasher.Windows/Views/SettingsWindow.axaml | 74 +++++++++++ .../Views/SettingsWindow.axaml.cs | 121 ++++++++++++++++++ 5 files changed, 204 insertions(+), 113 deletions(-) create mode 100644 src/Dasher.Windows/Views/SettingsWindow.axaml create mode 100644 src/Dasher.Windows/Views/SettingsWindow.axaml.cs diff --git a/src/Dasher.Windows/Controls/SettingsPanel.cs b/src/Dasher.Windows/Controls/SettingsPanel.cs index e2abbf7..f1c89eb 100644 --- a/src/Dasher.Windows/Controls/SettingsPanel.cs +++ b/src/Dasher.Windows/Controls/SettingsPanel.cs @@ -117,19 +117,6 @@ private void ShowCategoryCore(string category) _panel.Children.Clear(); - var backBtn = new Button - { - Content = "\u2190 Back", - FontSize = 12, - FontWeight = FontWeight.Medium, - Foreground = new SolidColorBrush(Color.FromRgb(0x8B, 0x92, 0x9A)), - Background = Brushes.Transparent, - Padding = new Thickness(0, 0, 0, 6), - BorderThickness = new Thickness(0), - }; - backBtn.Click += (s, e) => BackRequested?.Invoke(this, EventArgs.Empty); - _panel.Children.Add(backBtn); - if (category == "Input") { var inputSourceRow = BuildInputSourceRow(); @@ -212,7 +199,6 @@ private void ShowCategoryCore(string category) } } - public event EventHandler? BackRequested; public event EventHandler<(EyeGazeIntegration.TrackerType trackerType, int udpPort)>? InputSourceChanged; public event EventHandler? JoystickRequested; public event Action? OutputFontChanged; diff --git a/src/Dasher.Windows/Views/MainWindow.axaml b/src/Dasher.Windows/Views/MainWindow.axaml index 0879684..06304d0 100644 --- a/src/Dasher.Windows/Views/MainWindow.axaml +++ b/src/Dasher.Windows/Views/MainWindow.axaml @@ -263,19 +263,6 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Dasher.Windows/Views/MainWindow.axaml.cs b/src/Dasher.Windows/Views/MainWindow.axaml.cs index 82a9f10..ed11691 100644 --- a/src/Dasher.Windows/Views/MainWindow.axaml.cs +++ b/src/Dasher.Windows/Views/MainWindow.axaml.cs @@ -1,14 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia; -using Avalonia.Collections; using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; @@ -27,6 +25,8 @@ public partial class MainWindow : Window private DasherCanvas? _canvas; private MainWindowViewModel? _vm; private string _previousOutput = ""; + private Button[]? _settingsTabs; + private bool _settingsInitialized; private NativeBridge.SpeakCallback? _speakCallback; private NativeBridge.ParameterCallback? _parameterCallback; private int _bitrateKey; @@ -492,50 +492,118 @@ private void OnBack(object? sender, RoutedEventArgs e) { } - private void ApplyMode() + private void OnTogglePrefs(object? sender, RoutedEventArgs e) { if (_vm == null) return; + var dock = this.FindControl("SettingsDock"); + if (dock == null) return; - var txtKeyboardLabel = this.FindControl("TxtKeyboardLabel"); + var wasVisible = dock.IsVisible; + dock.IsVisible = !wasVisible; - if (_vm.IsKeyboardMode) + if (sender is Button btn) { - Topmost = true; - MessagePane.IsVisible = false; - MessageSplitter.IsVisible = false; - TxtModeLabel.Text = "Keyboard"; - BtnMode.Classes.Add("accent"); - if (txtKeyboardLabel != null) txtKeyboardLabel.Text = "Exit"; - BtnKeyboard.Classes.Add("accent"); - Width = Math.Min(Width, 600); + if (!wasVisible) btn.Classes.Add("accent"); + else btn.Classes.Remove("accent"); } - else + + if (!wasVisible && !_settingsInitialized) { - Topmost = false; - MessagePane.IsVisible = true; - MessageSplitter.IsVisible = true; - TxtModeLabel.Text = "Right side"; - BtnMode.Classes.Remove("accent"); - if (txtKeyboardLabel != null) txtKeyboardLabel.Text = "Keyboard"; - BtnKeyboard.Classes.Remove("accent"); - if (Width < 700) Width = 900; + InitializeSettingsPanel(); + _settingsInitialized = true; } - _previousOutput = _vm.OutputText; + // Pause/resume canvas timer when settings are open + if (_canvas != null) + { + if (!wasVisible) + _canvas.PauseTimer(); + else + _canvas.ResumeTimer(); + } } - private void OnTogglePrefs(object? sender, RoutedEventArgs e) + private static readonly Dictionary SettingsTabIcons = new() { - if (_vm == null) return; - var dialog = new SettingsWindow(); - dialog.Initialize(_vm.Handle, new SettingsWindowCallbacks + { "Input", LucideIconKind.MousePointerClick }, + { "Language", LucideIconKind.Languages }, + { "Customization", LucideIconKind.Palette }, + { "Speed", LucideIconKind.Gauge }, + { "Output", LucideIconKind.Type }, + { "Speech", LucideIconKind.Volume2 }, + { "Appearance", LucideIconKind.Eye }, + { "Advanced", LucideIconKind.Wrench }, + { "Other", LucideIconKind.Ellipsis }, + }; + + private void InitializeSettingsPanel() + { + var panel = this.FindControl("DockedSettingsPanel"); + if (panel == null || _vm == null) return; + + panel.Initialize(_vm.Handle); + panel.OutputFontChanged += OnOutputFontChanged; + panel.KeyboardOpacityChanged += OnKeyboardOpacityChanged; + panel.InputSourceChanged += OnInputSourceChanged; + panel.JoystickRequested += OnJoystickRequested; + + BuildSettingsTabs(panel); + } + + private void BuildSettingsTabs(SettingsPanel settingsPanel) + { + var container = this.FindControl("SettingsTabContainer"); + if (container == null) return; + + container.Children.Clear(); + _settingsTabs = []; + + foreach (var category in settingsPanel.GetCategoryNames()) { - OutputFontChanged = OnOutputFontChanged, - KeyboardOpacityChanged = OnKeyboardOpacityChanged, - InputSourceChanged = (tracker, port) => OnInputSourceChanged(null, (tracker, port)), - JoystickRequested = () => OnJoystickRequested(null, EventArgs.Empty), - }); - dialog.ShowDialog(this); + var btn = new Button + { + Classes = { "settings-tab" }, + Tag = category, + }; + + var stack = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 6, + }; + var iconKind = SettingsTabIcons.GetValueOrDefault(category, LucideIconKind.Circle); + stack.Children.Add(new LucideIcon { Kind = iconKind, Size = 16 }); + stack.Children.Add(new TextBlock { Text = category }); + + btn.Content = stack; + btn.Click += OnSettingsTabClick; + container.Children.Add(btn); + _settingsTabs = [.. _settingsTabs, btn]; + } + + ActivateSettingsTab(0); + settingsPanel.ShowCategory(settingsPanel.GetCategoryNames().FirstOrDefault() ?? ""); + } + + private void OnSettingsTabClick(object? sender, RoutedEventArgs e) + { + if (sender is not Button btn) return; + var category = btn.Tag as string ?? ""; + var idx = Array.FindIndex(_settingsTabs!, t => t.Tag as string == category); + if (idx < 0) return; + ActivateSettingsTab(idx); + var panel = this.FindControl("DockedSettingsPanel"); + panel?.ShowCategory(category); + } + + private void ActivateSettingsTab(int index) + { + if (_settingsTabs == null) return; + for (int i = 0; i < _settingsTabs.Length; i++) + { + if (i == index) _settingsTabs[i].Classes.Add("active"); + else _settingsTabs[i].Classes.Remove("active"); + } } private void OnNew(object? sender, RoutedEventArgs e) @@ -601,10 +669,6 @@ private async void OnSave(object? sender, RoutedEventArgs e) - private void OnStats(object? sender, RoutedEventArgs e) - { - } - private void OnLanguageChanged(object? sender, SelectionChangedEventArgs e) { if (_vm == null || _vm.Handle == IntPtr.Zero || _vm.SelectedLanguageIndex < 0) return; diff --git a/src/Dasher.Windows/Views/SettingsWindow.axaml b/src/Dasher.Windows/Views/SettingsWindow.axaml deleted file mode 100644 index 7f0ff29..0000000 --- a/src/Dasher.Windows/Views/SettingsWindow.axaml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/Dasher.Windows/Views/MainWindow.axaml.cs b/src/Dasher.Windows/Views/MainWindow.axaml.cs index ed11691..8440c83 100644 --- a/src/Dasher.Windows/Views/MainWindow.axaml.cs +++ b/src/Dasher.Windows/Views/MainWindow.axaml.cs @@ -372,7 +372,11 @@ private void ApplyPaneLayout() if (isKeyboard) { - // Keyboard mode: canvas only, no message pane + // Keyboard mode: hide full toolbar, show mini floating bar + TopBar.IsVisible = false; + KeyboardMiniBar.IsVisible = true; + + // Canvas only, no message pane MainGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star))); MainGrid.Children.Add(DasherCanvas); Grid.SetColumn(DasherCanvas, 0); @@ -400,6 +404,9 @@ private void ApplyPaneLayout() } else { + TopBar.IsVisible = true; + KeyboardMiniBar.IsVisible = false; + Topmost = false; this.Opacity = 1.0; SetNoActivate(false); @@ -513,6 +520,10 @@ private void OnTogglePrefs(object? sender, RoutedEventArgs e) _settingsInitialized = true; } + // In keyboard mode, hide mini-bar while settings are open + if (_vm.IsKeyboardMode) + KeyboardMiniBar.IsVisible = wasVisible; // show when closing settings, hide when opening + // Pause/resume canvas timer when settings are open if (_canvas != null) { @@ -852,6 +863,12 @@ private void UpdateControlModeLabel() var label = this.FindControl("TxtControlLabel"); if (label != null) label.Text = _controlModeActive ? "Leave" : "Control"; + + // Sync mini-bar control button accent state + if (_controlModeActive) + KbControlBtn.Classes.Add("accent"); + else + KbControlBtn.Classes.Remove("accent"); } private void OnToggleGameMode(object? sender, RoutedEventArgs e) From 998ce2e041e0b35da4895e58b54107ffa0a278fb Mon Sep 17 00:00:00 2001 From: will wade Date: Wed, 17 Jun 2026 21:01:31 +0100 Subject: [PATCH 6/6] Fix double character entry in keyboard mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The output callback (OnOutputEvent) and tick-based polling (dasher_get_output_text) were both updating OutputText independently. The callback used deferred Dispatcher.Post, so the tick read engine text before the callback's append ran — both saw a change, both fired OnOutputTextChanged, sending each character twice via SendInput. Removed the output callback entirely. The tick already reads the full output text every frame, making the callback redundant. --- src/Dasher.Windows/Controls/DasherCanvas.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Dasher.Windows/Controls/DasherCanvas.cs b/src/Dasher.Windows/Controls/DasherCanvas.cs index 3e0565a..632384a 100644 --- a/src/Dasher.Windows/Controls/DasherCanvas.cs +++ b/src/Dasher.Windows/Controls/DasherCanvas.cs @@ -22,7 +22,6 @@ public partial class DasherCanvas : Control private EyeGazeIntegration? _eyeGazeIntegration; private bool _useEyeGazeInput; - private NativeBridge.OutputCallback? _outputCallback; private NativeBridge.MessageCallback? _messageCallback; private bool _callbacksRegistered; private int _lastScreenWidth; @@ -136,13 +135,6 @@ private void EnsureCallbacksRegistered() if (_callbacksRegistered) return; _callbacksRegistered = true; - try - { - _outputCallback = new NativeBridge.OutputCallback(OnOutputEvent); - NativeBridge.dasher_set_output_callback(_handle, _outputCallback, IntPtr.Zero); - } - catch { } - try { _messageCallback = new NativeBridge.MessageCallback(OnEngineMessage); @@ -224,18 +216,6 @@ private void OnTick(object? sender, EventArgs e) InvalidateVisual(); } - private void OnOutputEvent(int eventType, IntPtr textPtr, IntPtr userData) - { - var text = textPtr != IntPtr.Zero ? Marshal.PtrToStringUTF8(textPtr) ?? "" : ""; - Dispatcher.UIThread.Post(() => - { - if (eventType == 0) - OutputText += text; - else if (eventType == 1 && OutputText.Length >= text.Length) - OutputText = OutputText[..^text.Length]; - }); - } - private void OnEngineMessage(int messageType, IntPtr textPtr, IntPtr userData) { var text = textPtr != IntPtr.Zero ? Marshal.PtrToStringUTF8(textPtr) ?? "" : "";