From c46a96377977c9d36cc1b58ca61b73537ee9d2ca Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Thu, 23 Apr 2026 17:42:16 -0500 Subject: [PATCH] refactor: select clip editor via DataTemplates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avalonia 12 logs binding warnings on null-pivot path chains. The MainWindow editor region used IsVisible guards on SelectedClip.IsScriptClip, SelectedClip.IsTableClip, etc., which all bind through SelectedClip and spam warnings at startup before any clip is selected. Replace the three sibling editors with nested ContentControls keyed on ClipViewModel.Editor's runtime type. No editor instance exists when no clip is selected, so no binding chain resolves through null. Move the script editor's TextMate install and ScriptEditorController ownership into a ScriptTextEditor subclass so it self-installs from the DataTemplate. Override StyleKeyOverride to inherit TextEditor's control template — Avalonia 12 matches styles by exact type, and without it the subclass renders blank. Drop the corresponding wiring from MainWindow code-behind. --- src/SharpFM/Editors/ScriptTextEditor.cs | 68 +++++++++++++++++++++++++ src/SharpFM/MainWindow.axaml | 56 +++++++++++--------- src/SharpFM/MainWindow.axaml.cs | 51 ------------------- 3 files changed, 100 insertions(+), 75 deletions(-) create mode 100644 src/SharpFM/Editors/ScriptTextEditor.cs diff --git a/src/SharpFM/Editors/ScriptTextEditor.cs b/src/SharpFM/Editors/ScriptTextEditor.cs new file mode 100644 index 0000000..dbfe71a --- /dev/null +++ b/src/SharpFM/Editors/ScriptTextEditor.cs @@ -0,0 +1,68 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia; +using Avalonia.Controls; +using AvaloniaEdit; +using AvaloniaEdit.TextMate; +using SharpFM.Scripting; +using SharpFM.Scripting.Editor; +using SharpFM.ViewModels; +using TextMateSharp.Grammars; + +namespace SharpFM.Editors; + +/// +/// A specialized for script clips. Owns its +/// TextMate installation and so it +/// can be dropped into a DataTemplate without any parent wiring — +/// each instance self-installs on construction and tears down on detach. +/// DataContext is expected to be a . +/// +[ExcludeFromCodeCoverage] +public class ScriptTextEditor : TextEditor +{ + // Avalonia 12 matches styles by exact type. Without this, a subclass of + // TextEditor gets no control template and renders blank — the editor's + // TextArea/TextView visual tree is never built. + protected override Type StyleKeyOverride => typeof(TextEditor); + + private readonly TextMate.Installation _textMate; + private readonly ScriptEditorController _controller; + + public ScriptTextEditor() + { + var registry = new RegistryOptions((ThemeName)(int)ThemeName.DarkPlus); + var fmScriptRegistry = new FmScriptRegistryOptions(registry); + _textMate = this.InstallTextMate(fmScriptRegistry); + _textMate.SetGrammar(FmScriptRegistryOptions.ScopeName); + + _controller = new ScriptEditorController(this); + _controller.StatusMessageRaised += OnStatusMessageRaised; + } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + if (DataContext is not ScriptClipEditor clipEditor) return; + + Document = clipEditor.Document; + _controller.AttachClipEditor(clipEditor); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _controller.StatusMessageRaised -= OnStatusMessageRaised; + _controller.Dispose(); + _textMate.Dispose(); + } + + private void OnStatusMessageRaised(object? sender, StatusMessageEventArgs e) + { + var window = TopLevel.GetTopLevel(this) as Window; + if (window?.DataContext is MainWindowViewModel vm) + vm.ShowStatusMessage(e.Message, e.IsError); + } +} diff --git a/src/SharpFM/MainWindow.axaml b/src/SharpFM/MainWindow.axaml index e80e582..346e6da 100644 --- a/src/SharpFM/MainWindow.axaml +++ b/src/SharpFM/MainWindow.axaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:SharpFM.ViewModels" + xmlns:editors="using:SharpFM.Editors" xmlns:schema="using:SharpFM.Schema.Editor" Icon="/Assets/noun-sharp-teeth-monster-4226695.small.png" Title="SharpFM" @@ -206,33 +207,40 @@ - + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/SharpFM/MainWindow.axaml.cs b/src/SharpFM/MainWindow.axaml.cs index 0927958..4e3002f 100644 --- a/src/SharpFM/MainWindow.axaml.cs +++ b/src/SharpFM/MainWindow.axaml.cs @@ -1,29 +1,21 @@ using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using AvaloniaEdit; -using AvaloniaEdit.TextMate; using SharpFM.Diagnostics; using SharpFM.Plugin; using SharpFM.Plugin.UI; using SharpFM.PluginManager; -using SharpFM.Scripting; using SharpFM.Services; using SharpFM.ViewModels; -using TextMateSharp.Grammars; namespace SharpFM; [ExcludeFromCodeCoverage] public partial class MainWindow : Window { - private readonly RegistryOptions _registryOptions; - private ScriptEditorController? _scriptController; - private TextMate.Installation? _scriptTextMateInstallation; private PluginService? _pluginService; private PluginUIHost? _pluginHost; private PluginConfigService? _pluginConfigService; @@ -32,19 +24,6 @@ public MainWindow() { InitializeComponent(); - _registryOptions = new RegistryOptions((ThemeName)(int)ThemeName.DarkPlus); - - // Script editor - var scriptEditor = this.FindControl("scriptEditor"); - if (scriptEditor != null) - { - var fmScriptRegistry = new FmScriptRegistryOptions(_registryOptions); - _scriptTextMateInstallation = scriptEditor.InstallTextMate(fmScriptRegistry); - _scriptTextMateInstallation.SetGrammar(FmScriptRegistryOptions.ScopeName); - _scriptController = new ScriptEditorController(scriptEditor); - _scriptController.StatusMessageRaised += OnScriptControllerStatusMessage; - } - // "Manage Plugins..." menu item var managePlugins = this.FindControl("managePluginsMenuItem"); if (managePlugins != null) @@ -71,18 +50,11 @@ private void OnDataContextChanged(object? sender, EventArgs e) if (DataContext is not MainWindowViewModel vm) return; BuildPluginMenuItems(vm); - vm.PropertyChanged += OnViewModelPropertyChanged; if (vm.PluginUI is { } pluginUI) pluginUI.PropertyChanged += OnPluginUIPropertyChanged; } - private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainWindowViewModel.SelectedClip)) - AttachScriptClipEditorIfApplicable(); - } - private void OnPluginUIPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PluginUIHost.IsVisible)) @@ -91,22 +63,6 @@ private void OnPluginUIPropertyChanged(object? sender, PropertyChangedEventArgs UpdatePluginPanelContent(); } - private void OnScriptControllerStatusMessage(object? sender, SharpFM.Scripting.Editor.StatusMessageEventArgs e) - { - if (DataContext is MainWindowViewModel vm) - vm.ShowStatusMessage(e.Message, e.IsError); - } - - private void AttachScriptClipEditorIfApplicable() - { - if (_scriptController == null) return; - if (DataContext is not MainWindowViewModel vm) return; - if (vm.SelectedClip?.Editor is SharpFM.Editors.ScriptClipEditor clipEditor) - { - _scriptController.AttachClipEditor(clipEditor); - } - } - private void BuildPluginMenuItems(MainWindowViewModel vm) { var pluginsMenu = this.FindControl("pluginsMenu"); @@ -238,11 +194,4 @@ private void ShowPluginManager() window.Configure(_pluginService, _pluginHost, vm, _pluginConfigService); window.ShowDialog(this); } - - protected override void OnClosed(EventArgs e) - { - base.OnClosed(e); - _scriptController?.Dispose(); - _scriptTextMateInstallation?.Dispose(); - } }