diff --git a/OneWare.slnx b/OneWare.slnx index c86871a8..2ccfc40d 100644 --- a/OneWare.slnx +++ b/OneWare.slnx @@ -91,6 +91,7 @@ + diff --git a/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs b/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs new file mode 100644 index 00000000..b4380559 --- /dev/null +++ b/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using OneWare.Essentials.Services; +using OneWare.MarkdownViewer.ViewModels; + +namespace OneWare.MarkdownViewer; + +public class MarkdownViewerModule : OneWareModuleBase +{ + public override void RegisterServices(IServiceCollection services) + { + services.AddTransient(); + } + + public override void Initialize(IServiceProvider serviceProvider) + { + serviceProvider.Resolve() + .RegisterDocumentView(".md", ".markdown"); + } +} + diff --git a/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj b/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj new file mode 100644 index 00000000..48b97a29 --- /dev/null +++ b/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs b/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs new file mode 100644 index 00000000..361b41d9 --- /dev/null +++ b/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs @@ -0,0 +1,128 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using OneWare.Core.Services; +using OneWare.Core.ViewModels.DockViews; +using OneWare.Essentials.LanguageService; +using OneWare.Essentials.Models; +using OneWare.Essentials.Services; + +namespace OneWare.MarkdownViewer.ViewModels; + +/// +/// An for markdown files that adds a live preview pane. +/// The editor and the preview can be toggled independently to allow editor-only, +/// preview-only or split-screen layouts. +/// +public class MarkdownEditViewModel : EditViewModel +{ + private readonly CompositeDisposable _markdownComposite = new(); + + private bool _showEditor = true; + private bool _showPreview = true; + private string? _markdownText; + + public MarkdownEditViewModel(string fullPath, ILogger logger, IFileIconService fileIconService, + ISettingsService settingsService, IMainDockService mainDockService, ILanguageManager languageManager, + IWindowService windowService, IProjectExplorerService projectExplorerService, IErrorService errorService, + BackupService backupService) : base(fullPath, logger, fileIconService, settingsService, mainDockService, + languageManager, windowService, projectExplorerService, errorService, backupService) + { + ToggleEditorCommand = new RelayCommand(() => ShowEditor = !ShowEditor); + TogglePreviewCommand = new RelayCommand(() => ShowPreview = !ShowPreview); + + // Keep the preview in sync with the editor content. The document instance can be + // replaced when the file is (re)loaded, so we listen to both events. + Editor.DocumentChanged += OnEditorChanged; + + Observable.FromEventPattern( + h => Editor.TextChanged += h, + h => Editor.TextChanged -= h) + .Throttle(TimeSpan.FromMilliseconds(250)) + .Subscribe(_ => UpdateMarkdown()) + .DisposeWith(_markdownComposite); + } + + public RelayCommand ToggleEditorCommand { get; } + + public RelayCommand TogglePreviewCommand { get; } + + public string? MarkdownText + { + get => _markdownText; + private set => SetProperty(ref _markdownText, value); + } + + public bool ShowEditor + { + get => _showEditor; + set + { + // Always keep at least one pane visible. + if (!value && !ShowPreview) ShowPreview = true; + if (SetProperty(ref _showEditor, value)) + { + OnPropertyChanged(nameof(IsSplitterVisible)); + OnPropertyChanged(nameof(EditorColumnWidth)); + } + } + } + + public bool ShowPreview + { + get => _showPreview; + set + { + // Always keep at least one pane visible. + if (!value && !ShowEditor) ShowEditor = true; + if (SetProperty(ref _showPreview, value)) + { + OnPropertyChanged(nameof(IsSplitterVisible)); + OnPropertyChanged(nameof(PreviewColumnWidth)); + if (value) UpdateMarkdown(); + } + } + } + + public bool IsSplitterVisible => ShowEditor && ShowPreview; + + /// Star width when the editor is visible, collapsed otherwise. + public GridLength EditorColumnWidth => + ShowEditor ? new GridLength(1, GridUnitType.Star) : new GridLength(0, GridUnitType.Auto); + + /// Star width when the preview is visible, collapsed otherwise. + public GridLength PreviewColumnWidth => + ShowPreview ? new GridLength(1, GridUnitType.Star) : new GridLength(0, GridUnitType.Auto); + + private void OnEditorChanged(object? sender, EventArgs e) + { + UpdateMarkdown(); + } + + private void UpdateMarkdown() + { + if (Dispatcher.UIThread.CheckAccess()) + MarkdownText = Editor.Document?.Text ?? string.Empty; + else + Dispatcher.UIThread.Post(() => MarkdownText = Editor.Document?.Text ?? string.Empty); + } + + public override bool OnClose() + { + var result = base.OnClose(); + if (!result) return false; + + Editor.DocumentChanged -= OnEditorChanged; + _markdownComposite.Dispose(); + return true; + } +} + + + + + + diff --git a/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml new file mode 100644 index 00000000..309330d6 --- /dev/null +++ b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs new file mode 100644 index 00000000..785eaffa --- /dev/null +++ b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace OneWare.MarkdownViewer.Views; + +public partial class MarkdownEditView : UserControl +{ + public MarkdownEditView() + { + InitializeComponent(); + } +} + diff --git a/studio/OneWare.Studio/OneWare.Studio.csproj b/studio/OneWare.Studio/OneWare.Studio.csproj index accb5e86..9b832698 100644 --- a/studio/OneWare.Studio/OneWare.Studio.csproj +++ b/studio/OneWare.Studio/OneWare.Studio.csproj @@ -21,6 +21,7 @@ + diff --git a/studio/OneWare.Studio/StudioApp.cs b/studio/OneWare.Studio/StudioApp.cs index 7760944e..a907f80f 100644 --- a/studio/OneWare.Studio/StudioApp.cs +++ b/studio/OneWare.Studio/StudioApp.cs @@ -8,6 +8,7 @@ using OneWare.CruviAdapterExtensions; using OneWare.Essentials.Models; using OneWare.Essentials.Services; +using OneWare.MarkdownViewer; using OneWare.Settings; using OneWare.Studio.Styles; using OneWare.UniversalFpgaProjectSystem; @@ -98,5 +99,6 @@ protected override void ConfigureModuleCatalog(OneWareModuleCatalog moduleCatalo moduleCatalog.AddModule(); moduleCatalog.AddModule(); moduleCatalog.AddModule(); + moduleCatalog.AddModule(); } } \ No newline at end of file