diff --git a/src/SharpFM.Model/ClipData.cs b/src/SharpFM.Model/ClipData.cs index 89e00f0..4b2d307 100644 --- a/src/SharpFM.Model/ClipData.cs +++ b/src/SharpFM.Model/ClipData.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace SharpFM.Model; /// @@ -7,4 +9,13 @@ namespace SharpFM.Model; /// Clip display name (filename without extension for file-based storage). /// Clipboard format identifier (e.g. "Mac-XMSS"). /// Raw XML content of the clip. -public record ClipData(string Name, string ClipType, string Xml); +public record ClipData(string Name, string ClipType, string Xml) +{ + /// + /// Hierarchy the clip lives under, as an ordered list of folder segment + /// names. Empty = root. Segments are plain strings (no slashes); each + /// repository implementation decides how to map them to its storage + /// (subdirectories, record columns, URL path, etc.). + /// + public IReadOnlyList FolderPath { get; init; } = []; +} diff --git a/src/SharpFM/Editors/ClipEditorViewFactory.cs b/src/SharpFM/Editors/ClipEditorViewFactory.cs new file mode 100644 index 0000000..2352efa --- /dev/null +++ b/src/SharpFM/Editors/ClipEditorViewFactory.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Media; +using AvaloniaEdit; +using AvaloniaEdit.Highlighting; +using SharpFM.Schema.Editor; + +namespace SharpFM.Editors; + +/// +/// Builds the Avalonia control used to edit a given clip. Kept centralised so +/// the clip view-model can cache a single control instance per clip — tab +/// switches reparent that control rather than reconstructing it, which +/// matters because installs TextMate on +/// construction and that is expensive. +/// +[ExcludeFromCodeCoverage] +public static class ClipEditorViewFactory +{ + private static readonly FontFamily MonoFont = + new("Cascadia Code,Consolas,Menlo,Monospace"); + + public static Control Create(IClipEditor editor) => editor switch + { + ScriptClipEditor s => new ScriptTextEditor + { + FontFamily = MonoFont, + ShowLineNumbers = true, + WordWrap = false, + DataContext = s, + }, + TableClipEditor t => new TableEditorControl + { + DataContext = t.ViewModel, + }, + FallbackXmlEditor f => new TextEditor + { + FontFamily = MonoFont, + ShowLineNumbers = true, + WordWrap = false, + Document = f.Document, + SyntaxHighlighting = HighlightingManager.Instance.GetDefinition("Xml"), + }, + _ => new TextBlock { Text = "No editor available for this clip type." }, + }; +} diff --git a/src/SharpFM/Editors/ScriptTextEditor.cs b/src/SharpFM/Editors/ScriptTextEditor.cs index dbfe71a..4ee6c71 100644 --- a/src/SharpFM/Editors/ScriptTextEditor.cs +++ b/src/SharpFM/Editors/ScriptTextEditor.cs @@ -19,21 +19,28 @@ namespace SharpFM.Editors; /// DataContext is expected to be a . /// [ExcludeFromCodeCoverage] -public class ScriptTextEditor : TextEditor +public class ScriptTextEditor : TextEditor, IDisposable { // 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); + // Shared across every script editor in the process. Building RegistryOptions + // pulls bundled themes into memory; doing that once (and reusing the result) + // measurably improves the hitch on first tab realisation for new clips. + private static readonly RegistryOptions SharedRegistry = + new((ThemeName)(int)ThemeName.DarkPlus); + + private static readonly FmScriptRegistryOptions SharedFmRegistry = + new(SharedRegistry); + 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 = this.InstallTextMate(SharedFmRegistry); _textMate.SetGrammar(FmScriptRegistryOptions.ScopeName); _controller = new ScriptEditorController(this); @@ -50,10 +57,17 @@ protected override void OnDataContextChanged(EventArgs e) _controller.AttachClipEditor(clipEditor); } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); + // Intentionally no dispose-on-detach. Tab switches reparent this control + // many times; tearing down TextMate on each detach would force a full + // grammar/theme reinstall on every reattach. Lifetime is owned by the + // ClipViewModel, which calls when the clip is + // removed or its editor is replaced. + private bool _disposed; + public void Dispose() + { + if (_disposed) return; + _disposed = true; _controller.StatusMessageRaised -= OnStatusMessageRaised; _controller.Dispose(); _textMate.Dispose(); diff --git a/src/SharpFM/MainWindow.axaml b/src/SharpFM/MainWindow.axaml index 36d6d91..82d6f26 100644 --- a/src/SharpFM/MainWindow.axaml +++ b/src/SharpFM/MainWindow.axaml @@ -138,16 +138,20 @@ Text="{Binding SearchText}" /> - + - - - + + - - - - - - + + + + + + + + Opacity="0.6" + Text="{Binding Clip.ClipTypeDisplay}" /> - - - - + + + + @@ -207,40 +212,103 @@ - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +