diff --git a/DasherCore b/DasherCore
index 26403f7..5533eb9 160000
--- a/DasherCore
+++ b/DasherCore
@@ -1 +1 @@
-Subproject commit 26403f7c5825daf090f54177be836d4d707d3ce1
+Subproject commit 5533eb9c7ee695e8abe2e5ac17904c68c89b4e99
diff --git a/src/Dasher.Windows/Controls/DasherCanvas.cs b/src/Dasher.Windows/Controls/DasherCanvas.cs
index 632384a..b9d9804 100644
--- a/src/Dasher.Windows/Controls/DasherCanvas.cs
+++ b/src/Dasher.Windows/Controls/DasherCanvas.cs
@@ -58,6 +58,14 @@ public void Initialize(string dataDir, string userDir)
var errorMsg = errorPtr != IntPtr.Zero ? Marshal.PtrToStringUTF8(errorPtr) ?? "Unknown error" : "Unknown error";
throw new InvalidOperationException($"Failed to create Dasher session: {errorMsg}");
}
+ }
+
+ ///
+ /// Starts the engine (sets screen size, triggers Realize, starts timer).
+ /// Call AFTER any pre-Realize parameter migration.
+ ///
+ public void StartEngine()
+ {
NativeBridge.dasher_set_screen_size(_handle, 700, 640);
_timer.Start();
}
diff --git a/src/Dasher.Windows/Controls/SettingsPanel.cs b/src/Dasher.Windows/Controls/SettingsPanel.cs
index d98a4c9..405b9c3 100644
--- a/src/Dasher.Windows/Controls/SettingsPanel.cs
+++ b/src/Dasher.Windows/Controls/SettingsPanel.cs
@@ -210,6 +210,7 @@ private void ShowCategoryCore(string category)
public event EventHandler<(EyeGazeIntegration.TrackerType trackerType, int udpPort)>? InputSourceChanged;
public event EventHandler? JoystickRequested;
public event Action? OutputFontChanged;
+ public event Action? ResetSettingsRequested;
public event Action? KeyboardOpacityChanged;
private static readonly string[] OutputFontPresets =
@@ -1273,6 +1274,133 @@ private void BuildPrivacySettings()
Foreground = mutedText,
});
+ // ── Dasher 5 Import section ──
+ section.Children.Add(new Border
+ {
+ BorderBrush = Application.Current?.FindResource("BorderLight") as IBrush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(0, 1, 0, 0),
+ Margin = new Thickness(0, 24, 0, 0),
+ });
+
+ section.Children.Add(new TextBlock
+ {
+ Text = "Dasher 5 Import",
+ FontSize = 15,
+ FontWeight = FontWeight.SemiBold,
+ Foreground = titleText,
+ });
+
+ if (V5MigrationService.HasV5Data)
+ {
+ var scan = V5MigrationService.Scan();
+
+ var statusText = V5MigrationService.HasBeenOffered
+ ? "Status: Imported"
+ : "Status: Not imported (Dasher 5 data detected)";
+
+ section.Children.Add(new TextBlock
+ {
+ Text = statusText,
+ FontSize = 12,
+ Foreground = bodyText,
+ });
+
+ if (!string.IsNullOrEmpty(scan.Alphabet))
+ section.Children.Add(new TextBlock { Text = $" Alphabet: {scan.Alphabet}", FontSize = 11, Foreground = mutedText });
+ if (!string.IsNullOrEmpty(scan.Colour))
+ section.Children.Add(new TextBlock { Text = $" Colour: {scan.Colour}", FontSize = 11, Foreground = mutedText });
+ if (!string.IsNullOrEmpty(scan.Speed))
+ section.Children.Add(new TextBlock { Text = $" Speed: {scan.Speed}", FontSize = 11, Foreground = mutedText });
+ if (scan.ControlMode)
+ section.Children.Add(new TextBlock { Text = " Control mode: enabled", FontSize = 11, Foreground = mutedText });
+ if (scan.CustomFileCount > 0)
+ section.Children.Add(new TextBlock { Text = $" {scan.CustomFileCount} custom file(s)", FontSize = 11, Foreground = mutedText });
+
+ var importBtn = new Button
+ {
+ Content = "Import from Dasher 5",
+ Padding = new Thickness(16, 6),
+ FontSize = 12,
+ Margin = new Thickness(0, 8, 0, 0),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ Background = Application.Current?.FindResource("ControlBg") as IBrush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(0),
+ };
+ importBtn.Click += (s, e) =>
+ {
+ var result = V5MigrationService.Import(_handle);
+ // Apply deferred params immediately (engine is already realized)
+ foreach (var (key, value) in result.DeferredParameters)
+ {
+ if (value == "true" || value == "false")
+ NativeBridge.dasher_set_bool_parameter(_handle, key, value == "true" ? 1 : 0);
+ else if (int.TryParse(value, out var intVal))
+ NativeBridge.dasher_set_long_parameter(_handle, key, intVal);
+ else
+ NativeBridge.dasher_set_string_parameter(_handle, key, value);
+ }
+ // Refresh the panel
+ ShowCategory("Privacy");
+ };
+ section.Children.Add(importBtn);
+ }
+ else
+ {
+ section.Children.Add(new TextBlock
+ {
+ Text = "No Dasher 5 data found on this computer.",
+ FontSize = 12,
+ Foreground = mutedText,
+ });
+ }
+
+ // ── Reset to defaults ──
+ section.Children.Add(new Border
+ {
+ BorderBrush = Application.Current?.FindResource("BorderLight") as IBrush ?? Brushes.LightGray,
+ BorderThickness = new Thickness(0, 1, 0, 0),
+ Margin = new Thickness(0, 24, 0, 0),
+ });
+
+ section.Children.Add(new TextBlock
+ {
+ Text = "Reset Settings",
+ FontSize = 15,
+ FontWeight = FontWeight.SemiBold,
+ Foreground = titleText,
+ });
+
+ section.Children.Add(new TextBlock
+ {
+ Text = "Restore all Dasher settings to their default values. This cannot be undone.",
+ FontSize = 12,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = bodyText,
+ });
+
+ var resetSettingsBtn = new Button
+ {
+ Content = "Reset to defaults",
+ Padding = new Thickness(16, 6),
+ FontSize = 12,
+ Margin = new Thickness(0, 8, 0, 0),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ Background = new SolidColorBrush(Color.FromRgb(0xEB, 0x5B, 0x5C)),
+ Foreground = Brushes.White,
+ BorderThickness = new Thickness(0),
+ };
+ resetSettingsBtn.Click += (s, e) =>
+ {
+ var btn = s as Button;
+ if (btn != null)
+ {
+ btn.Content = "Restarting engine...";
+ btn.IsEnabled = false;
+ }
+ ResetSettingsRequested?.Invoke();
+ };
+ section.Children.Add(resetSettingsBtn);
+
_panel.Children.Add(section);
}
diff --git a/src/Dasher.Windows/Services/V5MigrationService.cs b/src/Dasher.Windows/Services/V5MigrationService.cs
new file mode 100644
index 0000000..9c816fe
--- /dev/null
+++ b/src/Dasher.Windows/Services/V5MigrationService.cs
@@ -0,0 +1,383 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Xml.Linq;
+using Dasher.Windows.Engine;
+
+namespace Dasher.Windows.Services;
+
+public class V5MigrationResult
+{
+ public List Imported { get; set; } = new();
+ public List Skipped { get; set; } = new();
+ public List CopiedFiles { get; set; } = new();
+ public List<(int key, string value)> DeferredParameters { get; set; } = new();
+ public bool HasData { get; set; }
+ public string Alphabet { get; set; } = "";
+ public string Colour { get; set; } = "";
+ public string Speed { get; set; } = "";
+ public bool ControlMode { get; set; }
+ public int CustomFileCount { get; set; }
+}
+
+///
+/// Detects and imports Dasher v5 settings and user data on Windows.
+/// v5 stores data in %APPDATA%\dasher.rc\ — v6 uses %APPDATA%\Dasher\.
+///
+public static class V5MigrationService
+{
+ private static readonly string V5Dir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "dasher.rc");
+ private static readonly string V5SettingsFile = Path.Combine(V5Dir, "settings.xml");
+
+ private static readonly string V6Dir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Dasher");
+
+ private static readonly string MigrationFlagFile = Path.Combine(V6Dir, "v5_migration_completed");
+
+ public static bool HasBeenOffered => File.Exists(MigrationFlagFile);
+ public static bool HasV5Data => File.Exists(V5SettingsFile);
+
+ // v5 regName → v6 enum name for bool parameters
+ private static readonly Dictionary BoolMappings = new()
+ {
+ ["DrawMouseLine"] = "BP_DRAW_MOUSE_LINE",
+ ["DrawMouse"] = "BP_DRAW_MOUSE",
+ ["CurveMouseLine"] = "BP_CURVE_MOUSE_LINE",
+ ["StartOnLeft"] = "BP_START_MOUSE",
+ ["StartOnSpace"] = "BP_START_SPACE",
+ ["ControlMode"] = "BP_CONTROL_MODE",
+ ["PaletteChange"] = "BP_PALETTE_CHANGE",
+ ["TurboMode"] = "BP_TURBO_MODE",
+ ["ExactDynamics"] = "BP_EXACT_DYNAMICS",
+ ["Autocalibrate"] = "BP_AUTOCALIBRATE",
+ ["RemapXtreme"] = "BP_REMAP_XTREME",
+ ["AutoSpeedControl"] = "BP_AUTO_SPEEDCONTROL",
+ ["LMAdaptive"] = "BP_LM_ADAPTIVE",
+ ["NonlinearY"] = "BP_NONLINEAR_Y",
+ ["PauseOutside"] = "BP_STOP_OUTSIDE",
+ ["BackoffButton"] = "BP_BACKOFF_BUTTON",
+ ["TwoButtonReverse"] = "BP_TWOBUTTON_REVERSE",
+ ["TwoButtonInvertDouble"] = "BP_2B_INVERT_DOUBLE",
+ ["SlowStart"] = "BP_SLOW_START",
+ ["CopyOnStop"] = "BP_COPY_ALL_ON_STOP",
+ ["SpeakOnStop"] = "BP_SPEAK_ALL_ON_STOP",
+ ["SpeakWords"] = "BP_SPEAK_WORDS",
+ ["SlowControlBox"] = "BP_SLOW_CONTROL_BOX",
+ };
+
+ // All parameters must be deferred until after Realize() — some trigger
+ // handlers that dereference m_pDasherModel/m_pNCManager which are null
+ // before Realize. Deferring everything is safest.
+ // (Previously only 3 were deferred, but LP_NODE_BUDGET etc. also crash.)
+
+ // v5 regName → v6 enum name for long parameters
+ private static readonly Dictionary LongMappings = new()
+ {
+ ["ScreenOrientation"] = "LP_ORIENTATION",
+ ["MaxBitRateTimes100"] = "LP_MAX_BITRATE",
+ ["UniformTimes1000"] = "LP_UNIFORM",
+ ["LMAlpha"] = "LP_LM_ALPHA",
+ ["LMBeta"] = "LP_LM_BETA",
+ ["LMMaxOrder"] = "LP_LM_MAX_ORDER",
+ ["LMExclusion"] = "LP_LM_EXCLUSION",
+ ["LMUpdateExclusion"] = "LP_LM_UPDATE_EXCLUSION",
+ ["LMMixture"] = "LP_LM_MIXTURE",
+ ["LineWidth"] = "LP_LINE_WIDTH",
+ ["Zoomsteps"] = "LP_ZOOMSTEPS",
+ ["NodeBudget"] = "LP_NODE_BUDGET",
+ ["MarginWidth"] = "LP_MARGIN_WIDTH",
+ ["TargetOffset"] = "LP_TARGET_OFFSET",
+ ["XLimitSpeed"] = "LP_X_LIMIT_SPEED",
+ ["MinNodeSize"] = "LP_MIN_NODE_SIZE",
+ ["OutlineWidth"] = "LP_OUTLINE_WIDTH",
+ ["NonLinearX"] = "LP_NONLINEAR_X",
+ ["AutospeedSensitivity"] = "LP_AUTOSPEED_SENSITIVITY",
+ ["Geometry"] = "LP_GEOMETRY",
+ ["WordAlpha"] = "LP_LM_WORD_ALPHA",
+ ["MessageFontSize"] = "LP_MESSAGE_FONTSIZE",
+ ["RenderStyle"] = "LP_SHAPE_TYPE",
+ ["CirclePercent"] = "LP_CIRCLE_PERCENT",
+ ["TwoButtonOffset"] = "LP_TWO_BUTTON_OFFSET",
+ ["HoldTime"] = "LP_HOLD_TIME",
+ ["MultipressTime"] = "LP_MULTIPRESS_TIME",
+ ["SlowStartTime"] = "LP_SLOW_START_TIME",
+ ["TapTime"] = "LP_TAP_TIME",
+ ["ClickMaxZoom"] = "LP_MAXZOOM",
+ ["DynamicSpeedInc"] = "LP_DYNAMIC_SPEED_INC",
+ ["DynamicSpeedFreq"] = "LP_DYNAMIC_SPEED_FREQ",
+ ["DynamicSpeedDec"] = "LP_DYNAMIC_SPEED_DEC",
+ ["MousePositionBoxDistance"] = "LP_MOUSEPOSDIST",
+ };
+
+ // v5 regName → v6 enum name for string parameters
+ private static readonly Dictionary StringMappings = new()
+ {
+ ["AlphabetID"] = "SP_ALPHABET_ID",
+ ["DasherFont"] = "SP_DASHER_FONT",
+ ["GameTextFile"] = "SP_GAME_TEXT_FILE",
+ ["InputFilter"] = "SP_INPUT_FILTER",
+ ["InputDevice"] = "SP_INPUT_DEVICE",
+ ["Alphabet1"] = "SP_ALPHABET_1",
+ ["Alphabet2"] = "SP_ALPHABET_2",
+ ["Alphabet3"] = "SP_ALPHABET_3",
+ ["Alphabet4"] = "SP_ALPHABET_4",
+ };
+
+ // v5 platform-specific keys with no v6 equivalent
+ private static readonly HashSet PlatformOnlyKeys = new()
+ {
+ "AppStyle", "EditFont", "EditFontSize", "EditHeight", "EditWidth",
+ "FileEncodingFormat", "FullScreen", "MirrorLayout", "PopupEnable",
+ "PopupFont", "PopupFullScreen", "PopupInfront", "ScreenHeight",
+ "ScreenHeightH", "ScreenWidth", "ScreenWidthH", "TimeStampNewFiles",
+ "ToolbarID", "ViewStatusbar", "ViewToolbar", "WindowState",
+ "XPosition", "YPosition", "ConfirmUnsavedFiles",
+ "Button0", "Button1", "Button2", "Button3", "Button4", "Button10",
+ "ButtonCompassModeRightZoom", "ButtonMenuBoxes", "ButtonMenuSafety",
+ "ButtonMenuScanTime", "ButtonModeNonuniformity", "DemoNoiseMag",
+ "DemoNoiseMem", "DemoSpring", "DynamicButtonLag", "EditSize",
+ "FrameRate", "GameHelpDistance", "GameHelpTime", "LanguageModelID",
+ "MessageTime", "PYProbabilitySortThreshold", "SocketInputXMaxTimes1000",
+ "SocketInputXMinTimes1000", "SocketInputYMaxTimes1000",
+ "SocketInputYMinTimes1000", "SocketPort", "Static1BTime", "Static1BZoom",
+ "TwoPushLong", "TwoPushOuter", "TwoPushShort", "TwoPushTolerance",
+ "UserLogLevelMask", "YScaling", "SocketInputDebug", "GlobalKeyboard",
+ "GameDrawPath", "TwoPushReleaseTime", "ControlBoxID", "JoystickDevice",
+ "SocketInputXLabel", "SocketInputYLabel",
+ };
+
+ ///
+ /// Scan for v5 data without importing. Returns summary for display.
+ ///
+ public static V5MigrationResult Scan()
+ {
+ var result = new V5MigrationResult();
+
+ if (!File.Exists(V5SettingsFile))
+ return result;
+
+ result.HasData = true;
+
+ try
+ {
+ var doc = XDocument.Load(V5SettingsFile);
+ var settings = doc.Root!;
+
+ foreach (var el in settings.Elements())
+ {
+ var name = el.Attribute("name")?.Value ?? "";
+ var value = el.Attribute("value")?.Value ?? "";
+
+ if (name == "AlphabetID") result.Alphabet = value;
+ else if (name == "ColourID" && !string.IsNullOrEmpty(value)) result.Colour = value;
+ else if (name == "MaxBitRateTimes100") result.Speed = (int.Parse(value) / 100.0).ToString("F1") + "x";
+ else if (name == "ControlMode" && value == "True") result.ControlMode = true;
+ }
+ }
+ catch { }
+
+ // Count custom files
+ result.CustomFileCount = CountCustomFiles();
+
+ return result;
+ }
+
+ private static int CountCustomFiles()
+ {
+ int count = 0;
+ try
+ {
+ foreach (var f in Directory.GetFiles(V5Dir, "*.*", SearchOption.TopDirectoryOnly))
+ {
+ var name = Path.GetFileName(f);
+ if (name.StartsWith("alphabet.") || name.StartsWith("colour.") ||
+ name.StartsWith("color.") || name.StartsWith("control.") ||
+ name.StartsWith("training_"))
+ count++;
+ }
+ }
+ catch { }
+ return count;
+ }
+
+ ///
+ /// Import v5 settings. Must be called BEFORE dasher_set_screen_size().
+ /// Returns deferred parameters to apply AFTER screen size is set.
+ ///
+ public static V5MigrationResult Import(IntPtr handle)
+ {
+ var result = Scan();
+ if (!result.HasData)
+ return result;
+
+ try
+ {
+ var doc = XDocument.Load(V5SettingsFile);
+ var settings = doc.Root!;
+
+ foreach (var el in settings.Elements())
+ {
+ var name = el.Attribute("name")?.Value ?? "";
+ var value = el.Attribute("value")?.Value ?? "";
+ var tag = el.Name.LocalName;
+
+ switch (tag)
+ {
+ case "bool" when BoolMappings.TryGetValue(name, out var enumName):
+ ImportBool(handle, result, enumName, value == "True", name);
+ break;
+
+ case "long" when LongMappings.TryGetValue(name, out var enumName):
+ ImportLong(handle, result, enumName, value, name);
+ break;
+
+ case "string" when StringMappings.TryGetValue(name, out var enumName):
+ ImportString(handle, result, enumName, value, name);
+ break;
+
+ case "long" when name == "DasherFontSize":
+ ImportFontSize(handle, result, value);
+ break;
+
+ // Start mode from two bools
+ case "bool" when name == "CircleStart" && value == "True":
+ SetStartMode(handle, result, 2);
+ break;
+ case "bool" when name == "StartOnMousePosition" && value == "True":
+ SetStartMode(handle, result, 1);
+ break;
+
+ // Colour ID
+ case "string" when name == "ColourID" && !string.IsNullOrEmpty(value):
+ ImportColourId(handle, result, value);
+ break;
+
+ // Skip platform-only and unknown keys
+ default:
+ if (!PlatformOnlyKeys.Contains(name) &&
+ !BoolMappings.ContainsKey(name) &&
+ !LongMappings.ContainsKey(name) &&
+ !StringMappings.ContainsKey(name) &&
+ name != "DasherFontSize" && name != "ColourID" &&
+ name != "CircleStart" && name != "StartOnMousePosition")
+ {
+ result.Skipped.Add(name);
+ }
+ break;
+ }
+ }
+ }
+ catch { }
+
+ // Copy user data files
+ CopyUserDataFiles(result);
+
+ // Mark as completed
+ MarkCompleted();
+
+ return result;
+ }
+
+ private static void ImportBool(IntPtr handle, V5MigrationResult result, string enumName, bool value, string regName)
+ {
+ var key = NativeBridge.dasher_find_parameter_key(enumName);
+ if (key < 0) { result.Skipped.Add(regName); return; }
+
+ result.DeferredParameters.Add((key, value ? "true" : "false"));
+ result.Imported.Add($"{regName} = {value}");
+ }
+
+ private static void ImportLong(IntPtr handle, V5MigrationResult result, string enumName, string valueStr, string regName)
+ {
+ if (!int.TryParse(valueStr, out var value)) { result.Skipped.Add(regName); return; }
+ var key = NativeBridge.dasher_find_parameter_key(enumName);
+ if (key < 0) { result.Skipped.Add(regName); return; }
+
+ result.DeferredParameters.Add((key, value.ToString()));
+ result.Imported.Add($"{regName} = {value}");
+ }
+
+ private static void ImportString(IntPtr handle, V5MigrationResult result, string enumName, string value, string regName)
+ {
+ var key = NativeBridge.dasher_find_parameter_key(enumName);
+ if (key < 0) { result.Skipped.Add(regName); return; }
+
+ result.DeferredParameters.Add((key, value));
+ result.Imported.Add($"{regName} = \"{value}\"");
+ }
+
+ private static void ImportFontSize(IntPtr handle, V5MigrationResult result, string valueStr)
+ {
+ if (!int.TryParse(valueStr, out var index)) return;
+ var points = index switch
+ {
+ 0 => 14, 1 => 18, 2 => 22, 3 => 28, 4 => 36,
+ _ => Math.Min(index * 8, 72),
+ };
+ var key = NativeBridge.dasher_find_parameter_key("LP_DASHER_FONTSIZE");
+ if (key >= 0)
+ {
+ result.DeferredParameters.Add((key, points.ToString()));
+ result.Imported.Add($"DasherFontSize: index {index} → {points}pt");
+ }
+ }
+
+ private static void SetStartMode(IntPtr handle, V5MigrationResult result, int mode)
+ {
+ var key = NativeBridge.dasher_find_parameter_key("LP_START_MODE");
+ if (key >= 0)
+ {
+ result.DeferredParameters.Add((key, mode.ToString()));
+ result.Imported.Add($"StartMode = {mode}");
+ }
+ }
+
+ private static void ImportColourId(IntPtr handle, V5MigrationResult result, string value)
+ {
+ var key = NativeBridge.dasher_find_parameter_key("SP_COLOUR_ID");
+ if (key >= 0)
+ {
+ result.DeferredParameters.Add((key, value));
+ result.Imported.Add($"ColourID = \"{value}\"");
+ }
+ }
+
+ private static void CopyUserDataFiles(V5MigrationResult result)
+ {
+ try
+ {
+ Directory.CreateDirectory(V6Dir);
+
+ foreach (var f in Directory.GetFiles(V5Dir, "*.*", SearchOption.TopDirectoryOnly))
+ {
+ var name = Path.GetFileName(f);
+ var shouldCopy = name.StartsWith("alphabet.") ||
+ name.StartsWith("colour.") ||
+ name.StartsWith("color.") ||
+ name.StartsWith("control.") ||
+ name.StartsWith("training_");
+ if (!shouldCopy) continue;
+
+ var dest = Path.Combine(V6Dir, name);
+ if (File.Exists(dest))
+ {
+ result.Skipped.Add($"File: {name} (already exists)");
+ }
+ else
+ {
+ File.Copy(f, dest);
+ result.CopiedFiles.Add(name);
+ }
+ }
+ }
+ catch { }
+ }
+
+ public static void MarkCompleted()
+ {
+ try { File.WriteAllText(MigrationFlagFile, DateTime.UtcNow.ToString("o")); }
+ catch { }
+ }
+}
diff --git a/src/Dasher.Windows/Views/MainWindow.axaml.cs b/src/Dasher.Windows/Views/MainWindow.axaml.cs
index 3051a23..c9b2f94 100644
--- a/src/Dasher.Windows/Views/MainWindow.axaml.cs
+++ b/src/Dasher.Windows/Views/MainWindow.axaml.cs
@@ -133,9 +133,19 @@ protected override void OnOpened(EventArgs e)
var dataDir = Path.Combine(appData, "Dasher");
Directory.CreateDirectory(dataDir);
+ // Check for reset-to-defaults flag (written by Settings > Privacy > Reset)
+ var resetFlag = Path.Combine(dataDir, "reset_settings_pending");
+ if (File.Exists(resetFlag))
+ {
+ var settingsFile = Path.Combine(dataDir, "dasher_settings.xml");
+ try { if (File.Exists(settingsFile)) File.Delete(settingsFile); } catch { }
+ try { File.Delete(resetFlag); } catch { }
+ }
+
var coreDataDir = FindCoreDataDir();
CopyDataIfNeeded(coreDataDir, dataDir);
+ // Phase 1: Create engine (but don't start it yet)
_canvas.Initialize(dataDir, dataDir);
_canvas.EngineMessage += OnEngineMessage;
_vm.SetHandle(_canvas.GetHandle());
@@ -161,7 +171,144 @@ protected override void OnOpened(EventArgs e)
var accessConfig = AccessConfiguration.Load();
accessConfig.Apply(_vm.Handle);
- _vm.ApplySpeed();
+ // Phase 2: Check for v5 migration BEFORE starting engine
+ if (V5MigrationService.HasV5Data && !V5MigrationService.HasBeenOffered)
+ {
+ ShowV5MigrationPrompt();
+ }
+ else
+ {
+ CompleteStartup(null);
+ }
+ }
+
+ private void ShowV5MigrationPrompt()
+ {
+ var scan = V5MigrationService.Scan();
+
+ var dialog = new Window
+ {
+ Title = "Dasher 5 settings found",
+ Width = 460,
+ Height = 400,
+ WindowStartupLocation = WindowStartupLocation.CenterOwner,
+ CanResize = false,
+ ShowInTaskbar = false,
+ };
+
+ var panel = new StackPanel
+ {
+ Margin = new Thickness(24),
+ Spacing = 12,
+ };
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "We found your Dasher 5 configuration",
+ FontSize = 16,
+ FontWeight = FontWeight.Bold,
+ Foreground = new SolidColorBrush(Color.FromRgb(0x0F, 0x4B, 0x75)),
+ });
+
+ var summary = new System.Text.StringBuilder();
+ if (!string.IsNullOrEmpty(scan.Alphabet))
+ summary.AppendLine($" Alphabet: {scan.Alphabet}");
+ if (!string.IsNullOrEmpty(scan.Colour))
+ summary.AppendLine($" Colour palette: {scan.Colour}");
+ if (!string.IsNullOrEmpty(scan.Speed))
+ summary.AppendLine($" Speed: {scan.Speed}");
+ if (scan.ControlMode)
+ summary.AppendLine(" Control mode: enabled");
+ if (scan.CustomFileCount > 0)
+ summary.AppendLine($" {scan.CustomFileCount} custom file(s)");
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = summary.ToString().TrimEnd(),
+ FontSize = 13,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = new SolidColorBrush(Color.FromRgb(0x5A, 0x62, 0x70)),
+ });
+
+ panel.Children.Add(new TextBlock
+ {
+ Text = "Would you like to import these settings into Dasher 6?",
+ FontSize = 13,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = new SolidColorBrush(Color.FromRgb(0x5A, 0x62, 0x70)),
+ });
+
+ var btnRow = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 12,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 8, 0, 0),
+ };
+
+ var laterBtn = new Button
+ {
+ Content = "Start fresh",
+ Padding = new Thickness(20, 8),
+ Background = Brushes.Transparent,
+ BorderBrush = new SolidColorBrush(Color.FromRgb(0xE0, 0xE6, 0xE8)),
+ };
+
+ var importBtn = new Button
+ {
+ Content = "Import settings",
+ Padding = new Thickness(20, 8),
+ Background = new SolidColorBrush(Color.FromRgb(0x99, 0xD4, 0xCD)),
+ Foreground = new SolidColorBrush(Color.FromRgb(0x0F, 0x4B, 0x75)),
+ FontWeight = FontWeight.SemiBold,
+ BorderThickness = new Thickness(0),
+ };
+
+ V5MigrationResult? migrationResult = null;
+
+ laterBtn.Click += (_, _) =>
+ {
+ V5MigrationService.MarkCompleted();
+ dialog.Close();
+ };
+
+ importBtn.Click += (_, _) =>
+ {
+ migrationResult = V5MigrationService.Import(_vm!.Handle);
+ dialog.Close();
+ };
+
+ btnRow.Children.Add(laterBtn);
+ btnRow.Children.Add(importBtn);
+ panel.Children.Add(btnRow);
+
+ dialog.Content = panel;
+ dialog.Closed += (_, _) =>
+ Avalonia.Threading.Dispatcher.UIThread.Post(() => CompleteStartup(migrationResult));
+ dialog.ShowDialog(this);
+ }
+
+ private void CompleteStartup(V5MigrationResult? migrationResult)
+ {
+ // Phase 3: Start engine (triggers Realize)
+ _canvas!.StartEngine();
+
+ // Phase 4: Apply deferred parameters (must be after Realize)
+ if (migrationResult != null)
+ {
+ foreach (var (key, value) in migrationResult.DeferredParameters)
+ {
+ if (value == "true" || value == "false")
+ NativeBridge.dasher_set_bool_parameter(_vm!.Handle, key, value == "true" ? 1 : 0);
+ else if (int.TryParse(value, out var intVal))
+ NativeBridge.dasher_set_long_parameter(_vm!.Handle, key, intVal);
+ else
+ NativeBridge.dasher_set_string_parameter(_vm!.Handle, key, value);
+ }
+ }
+
+ // Phase 5: Continue with UI setup
+ _vm!.ApplySpeed();
_vm.AutoSpeed = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_AUTO_SPEEDCONTROL) != 0;
_vm.Learning = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_LM_ADAPTIVE) != 0;
_controlModeActive = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_CONTROL_MODE) != 0;
@@ -559,10 +706,68 @@ private void InitializeSettingsPanel()
panel.KeyboardOpacityChanged += OnKeyboardOpacityChanged;
panel.InputSourceChanged += OnInputSourceChanged;
panel.JoystickRequested += OnJoystickRequested;
+ panel.ResetSettingsRequested += OnResetSettingsRequested;
BuildSettingsTabs(panel);
}
+ private void OnResetSettingsRequested()
+ {
+ if (_canvas == null || _vm == null) return;
+
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var dataDir = Path.Combine(appData, "Dasher");
+
+ // Destroy engine (this saves current settings to file)
+ _canvas.Shutdown();
+
+ // Delete the saved settings so defaults load
+ var settingsFile = Path.Combine(dataDir, "dasher_settings.xml");
+ try { if (File.Exists(settingsFile)) File.Delete(settingsFile); } catch { }
+
+ // Recreate engine with defaults
+ _canvas.Initialize(dataDir, dataDir);
+ _canvas.EngineMessage += OnEngineMessage;
+ _vm.SetHandle(_canvas.GetHandle());
+
+ // Re-wire callbacks
+ if (_speakCallback != null)
+ NativeBridge.dasher_set_speak_callback(_vm.Handle, _speakCallback, IntPtr.Zero);
+ if (_parameterCallback != null)
+ NativeBridge.dasher_set_parameter_callback(_vm.Handle, _parameterCallback, IntPtr.Zero);
+
+ var accessConfig = AccessConfiguration.Load();
+ accessConfig.Apply(_vm.Handle);
+
+ // Start engine
+ _canvas.StartEngine();
+
+ // Refresh UI state
+ _vm.ApplySpeed();
+ _vm.AutoSpeed = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_AUTO_SPEEDCONTROL) != 0;
+ _vm.Learning = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_LM_ADAPTIVE) != 0;
+ _controlModeActive = NativeBridge.dasher_get_bool_parameter(_vm.Handle, ParameterKeys.BP_CONTROL_MODE) != 0;
+ UpdateControlModeLabel();
+ _vm.LoadAlphabets();
+ var currentAlphaPtr = NativeBridge.dasher_get_alphabet_id(_vm.Handle);
+ var currentAlpha = currentAlphaPtr != IntPtr.Zero
+ ? Marshal.PtrToStringUTF8(currentAlphaPtr) ?? "" : "";
+ _vm.SelectedLanguageIndex = Math.Max(0, _vm.Languages.IndexOf(currentAlpha));
+
+ // Re-initialize settings panel with new handle
+ _settingsInitialized = false;
+ var panel = this.FindControl("DockedSettingsPanel");
+ if (panel != null)
+ {
+ panel.Initialize(_vm.Handle);
+ panel.ShowCategory("Privacy");
+ }
+
+ // Clear output
+ _vm.OutputText = "";
+ _previousOutput = "";
+ }
+
private void BuildSettingsTabs(SettingsPanel settingsPanel)
{
var container = this.FindControl("SettingsTabContainer");