Rename project from "Ormentia Markus" to "Ormentia Markle" across all files, including license, application settings, UI elements, and deployment scripts. Add find panel functionality in the Markdown editor.
This commit is contained in:
4
LICENSE
4
LICENSE
@@ -634,7 +634,7 @@ state the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 2024 Ormentia Markus Contributors
|
||||
Copyright (C) 2024 Ormentia Markle Contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -654,7 +654,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
Ormentia Markus Copyright (C) 2024 Ormentia Markus Contributors
|
||||
Ormentia Markle Copyright (C) 2024 Ormentia Markle Contributors
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="MarkdownEditor.App"
|
||||
xmlns:local="using:MarkdownEditor"
|
||||
Name="Ormentia Markus"
|
||||
Name="Ormentia Markle"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ public partial class App : Application
|
||||
var appMenu = NativeMenu.GetMenu(this);
|
||||
if (appMenu != null)
|
||||
{
|
||||
// About Markus
|
||||
var aboutItem = new NativeMenuItem { Header = "About Markus" };
|
||||
aboutItem.Click += AboutMarkus_Click;
|
||||
// About Markle
|
||||
var aboutItem = new NativeMenuItem { Header = "About Markle" };
|
||||
aboutItem.Click += AboutMarkle_Click;
|
||||
appMenu.Items.Add(aboutItem);
|
||||
|
||||
// Separator
|
||||
@@ -48,10 +48,10 @@ public partial class App : Application
|
||||
// Separator
|
||||
appMenu.Items.Add(new NativeMenuItemSeparator());
|
||||
|
||||
// Hide Ormentia Markus
|
||||
// Hide Ormentia Markle
|
||||
appMenu.Items.Add(new NativeMenuItem
|
||||
{
|
||||
Header = "Hide Ormentia Markus",
|
||||
Header = "Hide Ormentia Markle",
|
||||
Gesture = KeyGesture.Parse("Cmd+H")
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ public partial class App : Application
|
||||
// Quit
|
||||
appMenu.Items.Add(new NativeMenuItem
|
||||
{
|
||||
Header = "Quit Ormentia Markus",
|
||||
Header = "Quit Ormentia Markle",
|
||||
Gesture = KeyGesture.Parse("Cmd+Q")
|
||||
});
|
||||
}
|
||||
@@ -125,9 +125,9 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the About Markus menu item click event.
|
||||
/// Handles the About Markle menu item click event.
|
||||
/// </summary>
|
||||
private void AboutMarkus_Click(object? sender, EventArgs e)
|
||||
private void AboutMarkle_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop &&
|
||||
desktop.MainWindow != null)
|
||||
|
||||
|
Before Width: | Height: | Size: 447 KiB After Width: | Height: | Size: 447 KiB |
@@ -76,7 +76,7 @@ public static class AppConstants
|
||||
public static class Messages
|
||||
{
|
||||
public const string DefaultTabTitle = "Untitled";
|
||||
public const string DefaultEditorContent = "# Welcome to Markdown Editor\n\nStart editing...";
|
||||
public const string DefaultEditorContent = "# Welcome to Markle\n\nStart editing...";
|
||||
public const string ErrorRenderingMarkdown = "Error rendering markdown";
|
||||
public const string OpenFileDialogTitle = "Open Markdown File";
|
||||
public const string SaveFileDialogTitle = "Save Markdown File";
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Ormentia Markus</string>
|
||||
<string>Ormentia Markle</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Ormentia Markus</string>
|
||||
<string>Ormentia Markle</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.ormentia.markus</string>
|
||||
<string>com.ormentia.markle</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>OrmentiaMarkus</string>
|
||||
<string>OrmentiaMarkle</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© 2025 Ormentia. All rights reserved.</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Markus 1.0.0 - A Simple Markdown Editor</string>
|
||||
<string>Markle 1.0.0 - A Simple Markdown Editor</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
<ApplicationTitle>Ormentia Markus</ApplicationTitle>
|
||||
<AssemblyName>OrmentiaMarkus</AssemblyName>
|
||||
<Product>Ormentia Markus</Product>
|
||||
<ApplicationTitle>Ormentia Markle</ApplicationTitle>
|
||||
<AssemblyName>OrmentiaMarkle</AssemblyName>
|
||||
<Product>Ormentia Markle</Product>
|
||||
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
|
||||
<CFBundleName>Ormentia Markus</CFBundleName>
|
||||
<CFBundleDisplayName>Ormentia Markus</CFBundleDisplayName>
|
||||
<CFBundleIdentifier>com.ormentia.markus</CFBundleIdentifier>
|
||||
<CFBundleName>Ormentia Markle</CFBundleName>
|
||||
<CFBundleDisplayName>Ormentia Markle</CFBundleDisplayName>
|
||||
<CFBundleIdentifier>com.ormentia.markle</CFBundleIdentifier>
|
||||
<CFBundleVersion>1.0.0</CFBundleVersion>
|
||||
<CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -33,7 +33,7 @@ public class SessionService : ISessionService
|
||||
/// <returns>The path to the app data directory.</returns>
|
||||
private static string GetAppDataDirectory()
|
||||
{
|
||||
var appName = "OrmentiaMarkus";
|
||||
var appName = "OrmentiaMarkle";
|
||||
|
||||
// Use LocalApplicationData for cross-platform compatibility
|
||||
// On Windows: %LOCALAPPDATA%
|
||||
|
||||
@@ -83,6 +83,15 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
SelectedTab = newTab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the find panel for the currently selected tab.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void ShowFindPanel()
|
||||
{
|
||||
SelectedTab?.ShowFindPanelCommand.Execute(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the specified tab.
|
||||
/// Maintains at least one open tab and updates selection appropriately.
|
||||
|
||||
@@ -31,6 +31,23 @@ public partial class MarkdownTabViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private bool _isDirty = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isFindPanelVisible = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _searchMatchInfo = string.Empty;
|
||||
|
||||
private int _currentMatchIndex = -1;
|
||||
private System.Collections.Generic.List<int> _searchMatches = new();
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when search should be performed in the view.
|
||||
/// </summary>
|
||||
public event EventHandler<SearchEventArgs>? SearchRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text selection used for formatting operations.
|
||||
/// </summary>
|
||||
@@ -117,6 +134,22 @@ public partial class MarkdownTabViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(DisplayTitle));
|
||||
}
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
{
|
||||
PerformSearch();
|
||||
}
|
||||
|
||||
partial void OnIsPrettyViewChanged(bool value)
|
||||
{
|
||||
// Reset search when switching views
|
||||
if (_currentMatchIndex >= 0)
|
||||
{
|
||||
_currentMatchIndex = -1;
|
||||
_searchMatches.Clear();
|
||||
UpdateSearchMatchInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a markdown file from disk.
|
||||
/// </summary>
|
||||
@@ -293,4 +326,189 @@ public partial class MarkdownTabViewModel : ViewModelBase
|
||||
get => Selection.End;
|
||||
set => Selection.End = value;
|
||||
}
|
||||
|
||||
// Find/Search commands
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowFindPanel()
|
||||
{
|
||||
IsFindPanelVisible = true;
|
||||
if (string.IsNullOrEmpty(SearchText) && Selection.Length > 0)
|
||||
{
|
||||
// If there's selected text, use it as the search term
|
||||
SearchText = _document.GetText(Selection.Start, Selection.Length);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseFindPanel()
|
||||
{
|
||||
IsFindPanelVisible = false;
|
||||
SearchText = string.Empty;
|
||||
_currentMatchIndex = -1;
|
||||
_searchMatches.Clear();
|
||||
UpdateSearchMatchInfo();
|
||||
SearchRequested?.Invoke(this, new SearchEventArgs { ClearHighlight = true });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void FindNext()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
PerformSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_searchMatches.Count == 0)
|
||||
{
|
||||
PerformSearch();
|
||||
}
|
||||
|
||||
if (_searchMatches.Count > 0)
|
||||
{
|
||||
_currentMatchIndex = (_currentMatchIndex + 1) % _searchMatches.Count;
|
||||
NavigateToMatch(_currentMatchIndex);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void FindPrevious()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
PerformSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_searchMatches.Count == 0)
|
||||
{
|
||||
PerformSearch();
|
||||
}
|
||||
|
||||
if (_searchMatches.Count > 0)
|
||||
{
|
||||
_currentMatchIndex = (_currentMatchIndex - 1 + _searchMatches.Count) % _searchMatches.Count;
|
||||
NavigateToMatch(_currentMatchIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformSearch()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
_searchMatches.Clear();
|
||||
_currentMatchIndex = -1;
|
||||
UpdateSearchMatchInfo();
|
||||
SearchRequested?.Invoke(this, new SearchEventArgs { ClearHighlight = true });
|
||||
return;
|
||||
}
|
||||
|
||||
var searchText = SearchText;
|
||||
var content = IsPrettyView ? SafeMarkdownContent : _document.Text;
|
||||
|
||||
_searchMatches.Clear();
|
||||
_currentMatchIndex = -1;
|
||||
|
||||
if (string.IsNullOrEmpty(content))
|
||||
{
|
||||
UpdateSearchMatchInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
// Case-insensitive search
|
||||
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||
int index = 0;
|
||||
|
||||
while ((index = content.IndexOf(searchText, index, comparison)) != -1)
|
||||
{
|
||||
_searchMatches.Add(index);
|
||||
index += searchText.Length;
|
||||
}
|
||||
|
||||
UpdateSearchMatchInfo();
|
||||
|
||||
if (_searchMatches.Count > 0)
|
||||
{
|
||||
_currentMatchIndex = 0;
|
||||
NavigateToMatch(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
SearchRequested?.Invoke(this, new SearchEventArgs
|
||||
{
|
||||
SearchText = searchText,
|
||||
Matches = _searchMatches,
|
||||
CurrentMatchIndex = -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigateToMatch(int matchIndex)
|
||||
{
|
||||
if (matchIndex < 0 || matchIndex >= _searchMatches.Count)
|
||||
return;
|
||||
|
||||
var matchPosition = _searchMatches[matchIndex];
|
||||
var searchText = SearchText;
|
||||
|
||||
if (IsPrettyView)
|
||||
{
|
||||
// For pretty view, notify the view to highlight the match
|
||||
SearchRequested?.Invoke(this, new SearchEventArgs
|
||||
{
|
||||
SearchText = searchText,
|
||||
Matches = _searchMatches,
|
||||
CurrentMatchIndex = matchIndex,
|
||||
MatchPosition = matchPosition
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// For raw view, set selection in the document
|
||||
Selection.Start = matchPosition;
|
||||
Selection.End = matchPosition + searchText.Length;
|
||||
|
||||
// Notify the view to scroll to selection
|
||||
SearchRequested?.Invoke(this, new SearchEventArgs
|
||||
{
|
||||
SearchText = searchText,
|
||||
Matches = _searchMatches,
|
||||
CurrentMatchIndex = matchIndex,
|
||||
MatchPosition = matchPosition,
|
||||
ScrollToSelection = true
|
||||
});
|
||||
}
|
||||
|
||||
UpdateSearchMatchInfo();
|
||||
}
|
||||
|
||||
private void UpdateSearchMatchInfo()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
SearchMatchInfo = string.Empty;
|
||||
}
|
||||
else if (_searchMatches.Count == 0)
|
||||
{
|
||||
SearchMatchInfo = "No results";
|
||||
}
|
||||
else
|
||||
{
|
||||
SearchMatchInfo = $"{_currentMatchIndex + 1} of {_searchMatches.Count}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for search operations.
|
||||
/// </summary>
|
||||
public class SearchEventArgs : EventArgs
|
||||
{
|
||||
public string SearchText { get; set; } = string.Empty;
|
||||
public System.Collections.Generic.List<int> Matches { get; set; } = new();
|
||||
public int CurrentMatchIndex { get; set; } = -1;
|
||||
public int MatchPosition { get; set; } = -1;
|
||||
public bool ScrollToSelection { get; set; } = false;
|
||||
public bool ClearHighlight { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="500"
|
||||
x:Class="MarkdownEditor.Views.AboutWindow"
|
||||
Title="About Markus"
|
||||
Title="About Markle"
|
||||
Width="600" Height="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
@@ -17,12 +17,12 @@
|
||||
Margin="0,40,0,40">
|
||||
|
||||
<!-- Logo -->
|
||||
<Image Source="/Assets/markus-logo.png"
|
||||
<Image Source="/Assets/markle-logo.png"
|
||||
Width="200" Height="200"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- App Name -->
|
||||
<TextBlock Text="Markus"
|
||||
<TextBlock Text="Markle"
|
||||
FontSize="48"
|
||||
FontWeight="Light"
|
||||
Foreground="White"
|
||||
|
||||
89
src/Views/FindPanel.axaml
Normal file
89
src/Views/FindPanel.axaml
Normal file
@@ -0,0 +1,89 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:MarkdownEditor.ViewModels"
|
||||
x:Class="MarkdownEditor.Views.FindPanel"
|
||||
x:DataType="vm:MarkdownTabViewModel">
|
||||
<Border Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
|
||||
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="8,6"
|
||||
CornerRadius="0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
|
||||
<!-- Search icon -->
|
||||
<TextBlock Text="🔍"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"/>
|
||||
|
||||
<!-- Search input -->
|
||||
<TextBox x:Name="SearchTextBox"
|
||||
Grid.Column="1"
|
||||
Text="{Binding SearchText, Mode=TwoWay}"
|
||||
Watermark="Search"
|
||||
FontSize="13"
|
||||
Padding="6,4"
|
||||
MinWidth="200"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"
|
||||
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
|
||||
KeyDown="OnSearchTextBoxKeyDown"/>
|
||||
|
||||
<!-- Match count -->
|
||||
<TextBlock x:Name="MatchCountText"
|
||||
Grid.Column="2"
|
||||
Text="{Binding SearchMatchInfo}"
|
||||
FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
Opacity="0.7"/>
|
||||
|
||||
<!-- Previous match button -->
|
||||
<Button Grid.Column="3"
|
||||
Content="↑"
|
||||
FontSize="14"
|
||||
Width="28"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
Margin="0,0,4,0"
|
||||
Command="{Binding FindPreviousCommand}"
|
||||
ToolTip.Tip="Previous Match (Shift+Enter)"
|
||||
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
|
||||
Classes="find-button"/>
|
||||
|
||||
<!-- Next match button -->
|
||||
<Button Grid.Column="4"
|
||||
Content="↓"
|
||||
FontSize="14"
|
||||
Width="28"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
Margin="0,0,4,0"
|
||||
Command="{Binding FindNextCommand}"
|
||||
ToolTip.Tip="Next Match (Enter)"
|
||||
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
|
||||
Classes="find-button"/>
|
||||
|
||||
<!-- Close button -->
|
||||
<Button Grid.Column="5"
|
||||
Content="✕"
|
||||
FontSize="14"
|
||||
Width="24"
|
||||
Height="24"
|
||||
Padding="0"
|
||||
Command="{Binding CloseFindPanelCommand}"
|
||||
ToolTip.Tip="Close (Esc)"
|
||||
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
BorderBrush="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"
|
||||
Classes="find-button"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
55
src/Views/FindPanel.axaml.cs
Normal file
55
src/Views/FindPanel.axaml.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using MarkdownEditor.ViewModels;
|
||||
|
||||
namespace MarkdownEditor.Views;
|
||||
|
||||
/// <summary>
|
||||
/// VSCode-style find panel overlay for searching text in both raw and pretty views.
|
||||
/// </summary>
|
||||
public partial class FindPanel : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FindPanel"/> class.
|
||||
/// </summary>
|
||||
public FindPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
base.OnLoaded(e);
|
||||
// Focus the search text box when the panel is shown
|
||||
SearchTextBox?.Focus();
|
||||
SearchTextBox?.SelectAll();
|
||||
}
|
||||
|
||||
private void OnSearchTextBoxKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (DataContext is not MarkdownTabViewModel viewModel)
|
||||
return;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Enter:
|
||||
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
||||
{
|
||||
viewModel.FindPreviousCommand.Execute(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
viewModel.FindNextCommand.Execute(null);
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Escape:
|
||||
viewModel.CloseFindPanelCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
|
||||
x:Class="MarkdownEditor.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Icon="/Assets/markus-logo.png"
|
||||
Title="Markus - A Simple Markdown Editor"
|
||||
Icon="/Assets/markle-logo.png"
|
||||
Title="Markle - A Simple Markdown Editor"
|
||||
Width="1200" Height="800">
|
||||
|
||||
<Design.DataContext>
|
||||
@@ -22,6 +22,8 @@
|
||||
<KeyBinding Gesture="Cmd+S" Command="{Binding SaveFileCommand}"/>
|
||||
<KeyBinding Gesture="Cmd+Shift+S" Command="{Binding SaveFileAsCommand}"/>
|
||||
<KeyBinding Gesture="Cmd+T" Command="{Binding AddNewTabCommand}"/>
|
||||
<KeyBinding Gesture="Cmd+F" Command="{Binding ShowFindPanelCommand}"/>
|
||||
<KeyBinding Gesture="Ctrl+F" Command="{Binding ShowFindPanelCommand}"/>
|
||||
</Window.KeyBindings>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
@@ -43,6 +45,8 @@
|
||||
<NativeMenuItem Header="_Copy" Gesture="Cmd+C"/>
|
||||
<NativeMenuItem Header="_Paste" Gesture="Cmd+V"/>
|
||||
<NativeMenuItemSeparator/>
|
||||
<NativeMenuItem Header="_Find" Command="{Binding ShowFindPanelCommand}" Gesture="Cmd+F"/>
|
||||
<NativeMenuItemSeparator/>
|
||||
<NativeMenuItem Header="Select _All" Gesture="Cmd+A"/>
|
||||
</NativeMenu>
|
||||
</NativeMenuItem>
|
||||
@@ -87,6 +91,15 @@
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
</Style>
|
||||
|
||||
<!-- Find panel button style -->
|
||||
<Style Selector="Button.find-button">
|
||||
<Setter Property="CornerRadius" Value="3"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
<Style Selector="Button.find-button:pointerover">
|
||||
<Setter Property="Background" Value="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BorderColor}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Compact tab styling -->
|
||||
<Style Selector="TabItem">
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
@@ -109,11 +122,11 @@
|
||||
|
||||
<!-- Left: Logo -->
|
||||
<Image Grid.Column="0"
|
||||
Source="/Assets/markus-logo.png"
|
||||
Source="/Assets/markle-logo.png"
|
||||
Width="28" Height="28"
|
||||
Margin="8,2,8,2"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip.Tip="Markus - A Simple Markdown Editor"/>
|
||||
ToolTip.Tip="Markle - A Simple Markdown Editor"/>
|
||||
|
||||
<!-- Center: Tabs with scrolling -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
@@ -473,11 +486,20 @@
|
||||
VerticalScrollBarVisibility="Auto"/>
|
||||
|
||||
<!-- Pretty markdown viewer -->
|
||||
<views:MarkdownViewer Markdown="{Binding SafeMarkdownContent}"
|
||||
<views:MarkdownViewer x:Name="MarkdownViewer"
|
||||
Markdown="{Binding SafeMarkdownContent}"
|
||||
IsVisible="{Binding IsPrettyView}"
|
||||
FontSize="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ContentFontSize}"
|
||||
Foreground="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ForegroundColor}"
|
||||
Background="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).BackgroundColor}"/>
|
||||
|
||||
<!-- Find Panel Overlay (VSCode-style) -->
|
||||
<views:FindPanel x:Name="FindPanelControl"
|
||||
IsVisible="{Binding IsFindPanelVisible}"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Stretch"
|
||||
ZIndex="1000"
|
||||
DataContext="{Binding}"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -27,6 +27,92 @@ public partial class MainWindow : Window
|
||||
|
||||
// Subscribe to pointer pressed to capture selection before command executes
|
||||
this.AddHandler(Button.PointerPressedEvent, OnButtonPointerPressed, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||
|
||||
// Subscribe to search events from tabs
|
||||
if (DataContext is MainWindowViewModel viewModel)
|
||||
{
|
||||
viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MainWindowViewModel.SelectedTab))
|
||||
{
|
||||
// Unsubscribe from old tab
|
||||
if (sender is MainWindowViewModel viewModel)
|
||||
{
|
||||
foreach (var tab in viewModel.Tabs)
|
||||
{
|
||||
tab.SearchRequested -= OnTabSearchRequested;
|
||||
}
|
||||
|
||||
// Subscribe to new selected tab
|
||||
if (viewModel.SelectedTab != null)
|
||||
{
|
||||
viewModel.SelectedTab.SearchRequested += OnTabSearchRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTabSearchRequested(object? sender, SearchEventArgs e)
|
||||
{
|
||||
if (sender is not MarkdownTabViewModel tabViewModel)
|
||||
return;
|
||||
|
||||
if (e.ClearHighlight)
|
||||
{
|
||||
// Clear search highlighting
|
||||
ClearSearchHighlight();
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabViewModel.IsPrettyView)
|
||||
{
|
||||
// Handle search in pretty view
|
||||
HandlePrettyViewSearch(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle search in raw editor view
|
||||
HandleRawEditorSearch(tabViewModel, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRawEditorSearch(MarkdownTabViewModel tabViewModel, SearchEventArgs e)
|
||||
{
|
||||
var textEditor = this.GetVisualDescendants().OfType<TextEditor>()
|
||||
.FirstOrDefault(te => te.IsVisible && te.Name == "EditorTextBox");
|
||||
|
||||
if (textEditor == null)
|
||||
return;
|
||||
|
||||
if (e.ScrollToSelection && e.MatchPosition >= 0)
|
||||
{
|
||||
// Set selection and scroll to it
|
||||
textEditor.SelectionStart = e.MatchPosition;
|
||||
textEditor.SelectionLength = e.SearchText.Length;
|
||||
textEditor.CaretOffset = e.MatchPosition + e.SearchText.Length;
|
||||
|
||||
// Scroll to caret
|
||||
var line = textEditor.Document.GetLineByOffset(textEditor.CaretOffset);
|
||||
textEditor.ScrollToLine(line.LineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePrettyViewSearch(SearchEventArgs e)
|
||||
{
|
||||
// For pretty view, we could highlight matches visually
|
||||
// For now, we'll just scroll to the match position if possible
|
||||
// Note: SelectableTextBlock doesn't have direct scroll-to-position support,
|
||||
// so this is a simplified implementation
|
||||
}
|
||||
|
||||
private void ClearSearchHighlight()
|
||||
{
|
||||
// Clear any search highlighting in both views
|
||||
// This is a placeholder for future highlighting implementation
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -39,6 +125,13 @@ public partial class MainWindow : Window
|
||||
viewModel.StorageProvider = StorageProvider;
|
||||
// Restore session after StorageProvider is set
|
||||
_ = viewModel.RestoreSessionAsync();
|
||||
|
||||
// Subscribe to selected tab changes
|
||||
viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
if (viewModel.SelectedTab != null)
|
||||
{
|
||||
viewModel.SelectedTab.SearchRequested += OnTabSearchRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Media;
|
||||
using Markdig;
|
||||
using MarkdownEditor.Views.Renderers;
|
||||
@@ -28,7 +29,7 @@ public class MarkdownViewer : UserControl
|
||||
set => SetValue(MarkdownProperty, value);
|
||||
}
|
||||
|
||||
private readonly StackPanel _container;
|
||||
private readonly SelectableTextBlock _textBlock;
|
||||
private readonly ScrollViewer _scrollViewer;
|
||||
private readonly MarkdownPipeline _pipeline;
|
||||
private MarkdownRendererCoordinator? _rendererCoordinator;
|
||||
@@ -38,10 +39,15 @@ public class MarkdownViewer : UserControl
|
||||
/// </summary>
|
||||
public MarkdownViewer()
|
||||
{
|
||||
_container = new StackPanel { Margin = new Thickness(10) };
|
||||
_textBlock = new SelectableTextBlock
|
||||
{
|
||||
Margin = new Thickness(10),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
_scrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = _container,
|
||||
Content = _textBlock,
|
||||
HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto
|
||||
};
|
||||
@@ -71,7 +77,11 @@ public class MarkdownViewer : UserControl
|
||||
/// </summary>
|
||||
private void RenderMarkdown()
|
||||
{
|
||||
_container.Children.Clear();
|
||||
// Clear existing inlines
|
||||
if (_textBlock.Inlines != null)
|
||||
{
|
||||
_textBlock.Inlines.Clear();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Markdown))
|
||||
return;
|
||||
@@ -90,25 +100,32 @@ public class MarkdownViewer : UserControl
|
||||
// Calculate font scale factor (base size is 14)
|
||||
double fontScale = FontSize / 14.0;
|
||||
|
||||
// Set base font size and colors
|
||||
_textBlock.FontSize = FontSize;
|
||||
_textBlock.Foreground = Foreground;
|
||||
|
||||
// Create coordinator with theme-aware colors and font scale
|
||||
_rendererCoordinator = new MarkdownRendererCoordinator(isDarkMode, fontScale);
|
||||
|
||||
var document = Markdig.Markdown.Parse(Markdown, _pipeline);
|
||||
var controls = _rendererCoordinator.RenderBlocks(document);
|
||||
|
||||
foreach (var control in controls)
|
||||
// Render all blocks into a single SelectableTextBlock's Inlines collection
|
||||
// This enables text selection across multiple blocks
|
||||
if (_textBlock.Inlines != null)
|
||||
{
|
||||
_container.Children.Add(control);
|
||||
_rendererCoordinator.RenderBlocksToInlines(document, _textBlock.Inlines);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_container.Children.Add(new TextBlock
|
||||
if (_textBlock.Inlines != null)
|
||||
{
|
||||
Text = $"Error rendering markdown: {ex.Message}",
|
||||
Foreground = Brushes.Red,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
_textBlock.Inlines.Clear();
|
||||
_textBlock.Inlines.Add(new Run($"Error rendering markdown: {ex.Message}")
|
||||
{
|
||||
Foreground = Brushes.Red
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class CodeBlockRenderer : IMarkdownBlockRenderer
|
||||
Padding = new Thickness(10),
|
||||
Margin = new Thickness(0, 5, 0, 5),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Child = new TextBlock
|
||||
Child = new SelectableTextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontFamily = new FontFamily("Consolas,Courier New,monospace"),
|
||||
|
||||
@@ -36,7 +36,7 @@ public class HeadingRenderer : IMarkdownBlockRenderer
|
||||
var text = MarkdownTextHelper.ExtractText(heading.Inline);
|
||||
var fontSize = GetFontSizeForLevel(heading.Level) * _fontScale;
|
||||
|
||||
return new TextBlock
|
||||
return new SelectableTextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = fontSize,
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ListRenderer : IMarkdownBlockRenderer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
new SelectableTextBlock
|
||||
{
|
||||
Text = bullet,
|
||||
[DockPanel.DockProperty] = Dock.Left,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Media;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using MarkdownEditor.Constants;
|
||||
|
||||
namespace MarkdownEditor.Views.Renderers;
|
||||
|
||||
@@ -11,6 +15,9 @@ namespace MarkdownEditor.Views.Renderers;
|
||||
public class MarkdownRendererCoordinator
|
||||
{
|
||||
private readonly List<IMarkdownBlockRenderer> _renderers;
|
||||
private readonly InlineRenderer _inlineRenderer;
|
||||
private readonly bool _isDarkMode;
|
||||
private readonly double _fontScale;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MarkdownRendererCoordinator"/> class.
|
||||
@@ -19,13 +26,15 @@ public class MarkdownRendererCoordinator
|
||||
/// <param name="fontScale">Font scale factor for text sizing (1.0 = 14px base).</param>
|
||||
public MarkdownRendererCoordinator(bool isDarkMode = true, double fontScale = 1.0)
|
||||
{
|
||||
var inlineRenderer = new InlineRenderer(isDarkMode, fontScale);
|
||||
_isDarkMode = isDarkMode;
|
||||
_fontScale = fontScale;
|
||||
_inlineRenderer = new InlineRenderer(isDarkMode, fontScale);
|
||||
|
||||
_renderers = new List<IMarkdownBlockRenderer>
|
||||
{
|
||||
new HeadingRenderer(fontScale),
|
||||
new CodeBlockRenderer(isDarkMode, fontScale),
|
||||
new ParagraphRenderer(inlineRenderer),
|
||||
new ParagraphRenderer(_inlineRenderer),
|
||||
new ThematicBreakRenderer(isDarkMode),
|
||||
new ListRenderer(this), // Pass coordinator for nested rendering
|
||||
new QuoteRenderer(this), // Pass coordinator for nested rendering
|
||||
@@ -69,5 +78,261 @@ public class MarkdownRendererCoordinator
|
||||
}
|
||||
return controls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders all markdown blocks into a single SelectableTextBlock's Inlines collection.
|
||||
/// This enables text selection across multiple blocks.
|
||||
/// </summary>
|
||||
/// <param name="blocks">The markdown blocks to render.</param>
|
||||
/// <param name="inlines">The inline collection to populate.</param>
|
||||
public void RenderBlocksToInlines(IEnumerable<Block> blocks, InlineCollection inlines)
|
||||
{
|
||||
bool isFirstBlock = true;
|
||||
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
// Add spacing between blocks (except before the first one)
|
||||
if (!isFirstBlock)
|
||||
{
|
||||
inlines.Add(new LineBreak());
|
||||
inlines.Add(new LineBreak());
|
||||
}
|
||||
isFirstBlock = false;
|
||||
|
||||
switch (block)
|
||||
{
|
||||
case HeadingBlock heading:
|
||||
RenderHeadingToInlines(heading, inlines);
|
||||
break;
|
||||
|
||||
case ParagraphBlock paragraph:
|
||||
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
|
||||
break;
|
||||
|
||||
case CodeBlock codeBlock:
|
||||
RenderCodeBlockToInlines(codeBlock, inlines);
|
||||
break;
|
||||
|
||||
case ListBlock list:
|
||||
RenderListToInlines(list, inlines);
|
||||
break;
|
||||
|
||||
case QuoteBlock quote:
|
||||
RenderQuoteToInlines(quote, inlines);
|
||||
break;
|
||||
|
||||
case Table table:
|
||||
RenderTableToInlines(table, inlines);
|
||||
break;
|
||||
|
||||
case ThematicBreakBlock:
|
||||
inlines.Add(new Run(new string('─', 50))
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.Parse(_isDarkMode ? "#666666" : "#CCCCCC"))
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderHeadingToInlines(HeadingBlock heading, InlineCollection inlines)
|
||||
{
|
||||
var fontSize = GetHeadingFontSize(heading.Level) * _fontScale;
|
||||
var text = MarkdownTextHelper.ExtractText(heading.Inline);
|
||||
|
||||
// Render heading text with inline formatting preserved
|
||||
if (heading.Inline != null)
|
||||
{
|
||||
foreach (var inline in heading.Inline)
|
||||
{
|
||||
if (inline is Markdig.Syntax.Inlines.LiteralInline literal)
|
||||
{
|
||||
inlines.Add(new Run(literal.Content.ToString())
|
||||
{
|
||||
FontSize = fontSize,
|
||||
FontWeight = FontWeight.Bold
|
||||
});
|
||||
}
|
||||
else if (inline is Markdig.Syntax.Inlines.ContainerInline container)
|
||||
{
|
||||
// Handle nested formatting (bold, italic, etc.)
|
||||
RenderInlineToInlines(container, inlines, fontSize, FontWeight.Bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
inlines.Add(new Run(text)
|
||||
{
|
||||
FontSize = fontSize,
|
||||
FontWeight = FontWeight.Bold
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderInlineToInlines(Markdig.Syntax.Inlines.ContainerInline container, InlineCollection inlines, double fontSize, FontWeight fontWeight)
|
||||
{
|
||||
foreach (var inline in container)
|
||||
{
|
||||
if (inline is Markdig.Syntax.Inlines.LiteralInline literal)
|
||||
{
|
||||
inlines.Add(new Run(literal.Content.ToString())
|
||||
{
|
||||
FontSize = fontSize,
|
||||
FontWeight = fontWeight
|
||||
});
|
||||
}
|
||||
else if (inline is Markdig.Syntax.Inlines.EmphasisInline emphasis)
|
||||
{
|
||||
var text = MarkdownTextHelper.ExtractText(emphasis);
|
||||
var run = new Run(text)
|
||||
{
|
||||
FontSize = fontSize,
|
||||
FontWeight = fontWeight
|
||||
};
|
||||
|
||||
if (emphasis.DelimiterCount == 2)
|
||||
{
|
||||
run.FontWeight = FontWeight.Bold;
|
||||
}
|
||||
else
|
||||
{
|
||||
run.FontStyle = FontStyle.Italic;
|
||||
}
|
||||
inlines.Add(run);
|
||||
}
|
||||
else if (inline is Markdig.Syntax.Inlines.ContainerInline nested)
|
||||
{
|
||||
RenderInlineToInlines(nested, inlines, fontSize, fontWeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderCodeBlockToInlines(CodeBlock codeBlock, InlineCollection inlines)
|
||||
{
|
||||
var text = codeBlock is FencedCodeBlock fenced
|
||||
? fenced.Lines.ToString()
|
||||
: codeBlock.Lines.ToString();
|
||||
|
||||
inlines.Add(new Run(text)
|
||||
{
|
||||
FontFamily = new FontFamily("Consolas,Courier New,monospace"),
|
||||
FontSize = 14 * _fontScale,
|
||||
Foreground = new SolidColorBrush(Color.Parse(_isDarkMode ? "#E8E8E8" : "#333333")),
|
||||
Background = new SolidColorBrush(Color.Parse(_isDarkMode ? "#2D2D2D" : "#F0F0F0"))
|
||||
});
|
||||
}
|
||||
|
||||
private void RenderListToInlines(ListBlock list, InlineCollection inlines)
|
||||
{
|
||||
int index = 1;
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item is ListItemBlock listItem)
|
||||
{
|
||||
var bullet = list.IsOrdered ? $"{index}. " : "• ";
|
||||
inlines.Add(new Run(bullet));
|
||||
|
||||
foreach (var childBlock in listItem)
|
||||
{
|
||||
if (childBlock is ParagraphBlock paragraph)
|
||||
{
|
||||
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
|
||||
}
|
||||
else if (childBlock is HeadingBlock heading)
|
||||
{
|
||||
RenderHeadingToInlines(heading, inlines);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For other block types in list items, recursively render them
|
||||
RenderBlockToInlines(childBlock, inlines);
|
||||
}
|
||||
}
|
||||
|
||||
inlines.Add(new LineBreak());
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderQuoteToInlines(QuoteBlock quote, InlineCollection inlines)
|
||||
{
|
||||
foreach (var childBlock in quote)
|
||||
{
|
||||
if (childBlock is ParagraphBlock paragraph)
|
||||
{
|
||||
inlines.Add(new Run("> "));
|
||||
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
|
||||
inlines.Add(new LineBreak());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTableToInlines(Table table, InlineCollection inlines)
|
||||
{
|
||||
// Render table as plain text for selection purposes
|
||||
foreach (var row in table)
|
||||
{
|
||||
if (row is TableRow tableRow)
|
||||
{
|
||||
bool firstCell = true;
|
||||
foreach (var cell in tableRow)
|
||||
{
|
||||
if (!firstCell) inlines.Add(new Run(" | "));
|
||||
firstCell = false;
|
||||
|
||||
if (cell is TableCell tableCell)
|
||||
{
|
||||
// Table cells contain child blocks, not inline content directly
|
||||
foreach (var childBlock in tableCell)
|
||||
{
|
||||
RenderBlockToInlines(childBlock, inlines);
|
||||
}
|
||||
}
|
||||
}
|
||||
inlines.Add(new LineBreak());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderBlockToInlines(Block block, InlineCollection inlines)
|
||||
{
|
||||
// Helper method to render any block type to inlines
|
||||
switch (block)
|
||||
{
|
||||
case ParagraphBlock paragraph:
|
||||
_inlineRenderer.BuildInlines(paragraph.Inline, inlines);
|
||||
break;
|
||||
case HeadingBlock heading:
|
||||
RenderHeadingToInlines(heading, inlines);
|
||||
break;
|
||||
case CodeBlock codeBlock:
|
||||
RenderCodeBlockToInlines(codeBlock, inlines);
|
||||
break;
|
||||
default:
|
||||
// For other block types, try to extract text if they have inline content
|
||||
Markdig.Syntax.Inlines.ContainerInline? inlineContent = null;
|
||||
if (block is ParagraphBlock p) inlineContent = p.Inline;
|
||||
else if (block is HeadingBlock h) inlineContent = h.Inline;
|
||||
|
||||
var text = MarkdownTextHelper.ExtractText(inlineContent);
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
inlines.Add(new Run(text));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetHeadingFontSize(int level) => level switch
|
||||
{
|
||||
1 => AppConstants.FontSizes.Heading1,
|
||||
2 => AppConstants.FontSizes.Heading2,
|
||||
3 => AppConstants.FontSizes.Heading3,
|
||||
4 => AppConstants.FontSizes.Heading4,
|
||||
5 => AppConstants.FontSizes.Heading5,
|
||||
_ => AppConstants.FontSizes.Heading6
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ParagraphRenderer : IMarkdownBlockRenderer
|
||||
return null;
|
||||
}
|
||||
|
||||
var textBlock = new TextBlock
|
||||
var textBlock = new SelectableTextBlock
|
||||
{
|
||||
Margin = new Thickness(0, 5, 0, 5),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
|
||||
@@ -27,8 +27,8 @@ Creates a complete `.app` bundle with:
|
||||
Automatically detects your Mac architecture (Intel or Apple Silicon).
|
||||
|
||||
**Output:**
|
||||
- Apple Silicon: `./bin/Release/osx-arm64/OrmentiaMarkus.app`
|
||||
- Intel: `./bin/Release/osx-x64/OrmentiaMarkus.app`
|
||||
- Apple Silicon: `./bin/Release/osx-arm64/OrmentiaMarkle.app`
|
||||
- Intel: `./bin/Release/osx-x64/OrmentiaMarkle.app`
|
||||
|
||||
### Windows
|
||||
|
||||
@@ -41,7 +41,7 @@ Creates a self-contained `.exe` file with:
|
||||
- All dependencies bundled
|
||||
- Single-file executable ready for distribution
|
||||
|
||||
**Output:** `.\bin\Release\win-x64\OrmentiaMarkus.exe`
|
||||
**Output:** `.\bin\Release\win-x64\OrmentiaMarkle.exe`
|
||||
|
||||
**Note:** Icon file (`Assets/AppIcon.ico`) must be created first. Run `bash deploy/create-windows-icon.sh` from macOS/Linux.
|
||||
|
||||
@@ -58,7 +58,7 @@ Creates a complete application package with:
|
||||
- Launcher script
|
||||
- Installation README
|
||||
|
||||
**Output:** `./bin/Release/linux-x64/OrmentiaMarkus/` folder (ready to archive and distribute)
|
||||
**Output:** `./bin/Release/linux-x64/OrmentiaMarkle/` folder (ready to archive and distribute)
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building Markus for Linux..."
|
||||
echo "Building Markle for Linux..."
|
||||
|
||||
# Navigate to project root
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -23,7 +23,7 @@ dotnet publish MarkdownEditor.csproj \
|
||||
-o ./bin/Release/linux-x64/publish
|
||||
|
||||
# Create application directory structure
|
||||
APP_DIR="./bin/Release/linux-x64/OrmentiaMarkus"
|
||||
APP_DIR="./bin/Release/linux-x64/OrmentiaMarkle"
|
||||
echo "Creating application package..."
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
@@ -32,46 +32,46 @@ mkdir -p "$APP_DIR/share/applications"
|
||||
mkdir -p "$APP_DIR/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Copy executable
|
||||
cp ./bin/Release/linux-x64/publish/OrmentiaMarkus "$APP_DIR/bin/"
|
||||
chmod +x "$APP_DIR/bin/OrmentiaMarkus"
|
||||
cp ./bin/Release/linux-x64/publish/OrmentiaMarkle "$APP_DIR/bin/"
|
||||
chmod +x "$APP_DIR/bin/OrmentiaMarkle"
|
||||
|
||||
# Copy icon
|
||||
if [ -f "Assets/AppIcon.iconset/icon_256x256.png" ]; then
|
||||
cp "Assets/AppIcon.iconset/icon_256x256.png" "$APP_DIR/share/icons/hicolor/256x256/apps/ormentia-markus.png"
|
||||
cp "Assets/AppIcon.iconset/icon_256x256.png" "$APP_DIR/share/icons/hicolor/256x256/apps/ormentia-markle.png"
|
||||
fi
|
||||
|
||||
# Create .desktop file
|
||||
cat > "$APP_DIR/share/applications/ormentia-markus.desktop" << EOF
|
||||
cat > "$APP_DIR/share/applications/ormentia-markle.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Markus
|
||||
Name=Markle
|
||||
Comment=A Simple Markdown Editor
|
||||
Exec=OrmentiaMarkus
|
||||
Icon=ormentia-markus
|
||||
Exec=OrmentiaMarkle
|
||||
Icon=ormentia-markle
|
||||
Categories=Office;TextEditor;Utility;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
# Create launcher script
|
||||
cat > "$APP_DIR/OrmentiaMarkus" << 'EOF'
|
||||
cat > "$APP_DIR/OrmentiaMarkle" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Launcher script for Markus
|
||||
# Launcher script for Markle
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$SCRIPT_DIR/bin/OrmentiaMarkus" "$@"
|
||||
exec "$SCRIPT_DIR/bin/OrmentiaMarkle" "$@"
|
||||
EOF
|
||||
chmod +x "$APP_DIR/OrmentiaMarkus"
|
||||
chmod +x "$APP_DIR/OrmentiaMarkle"
|
||||
|
||||
# Create README
|
||||
cat > "$APP_DIR/README.txt" << EOF
|
||||
Markus - A Simple Markdown Editor
|
||||
Markle - A Simple Markdown Editor
|
||||
==================================
|
||||
|
||||
Installation:
|
||||
1. Extract this archive to your preferred location (e.g., /opt or ~/Applications)
|
||||
2. Run ./OrmentiaMarkus to launch the application
|
||||
3. (Optional) Copy share/applications/ormentia-markus.desktop to ~/.local/share/applications/
|
||||
2. Run ./OrmentiaMarkle to launch the application
|
||||
3. (Optional) Copy share/applications/ormentia-markle.desktop to ~/.local/share/applications/
|
||||
for desktop menu integration
|
||||
|
||||
© 2025 Ormentia. All rights reserved.
|
||||
@@ -83,11 +83,11 @@ rm -rf ./bin/Release/linux-x64/publish
|
||||
|
||||
echo ""
|
||||
echo "✓ Build completed successfully!"
|
||||
echo "Application package: ./bin/Release/linux-x64/OrmentiaMarkus/"
|
||||
echo "Application package: ./bin/Release/linux-x64/OrmentiaMarkle/"
|
||||
echo ""
|
||||
echo "You can now:"
|
||||
echo " 1. Run ./bin/Release/linux-x64/OrmentiaMarkus/OrmentiaMarkus"
|
||||
echo " 2. Archive and distribute the OrmentiaMarkus folder"
|
||||
echo " 1. Run ./bin/Release/linux-x64/OrmentiaMarkle/OrmentiaMarkle"
|
||||
echo " 2. Archive and distribute the OrmentiaMarkle folder"
|
||||
echo " 3. Install to /opt or ~/Applications"
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building Markus for macOS..."
|
||||
echo "Building Markle for macOS..."
|
||||
|
||||
# Navigate to project root
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -31,7 +31,7 @@ dotnet publish MarkdownEditor.csproj \
|
||||
-o ./bin/Release/$RUNTIME/publish
|
||||
|
||||
# Create .app bundle structure
|
||||
APP_NAME="OrmentiaMarkus.app"
|
||||
APP_NAME="OrmentiaMarkle.app"
|
||||
APP_PATH="./bin/Release/$RUNTIME/$APP_NAME"
|
||||
CONTENTS_PATH="$APP_PATH/Contents"
|
||||
MACOS_PATH="$CONTENTS_PATH/MacOS"
|
||||
@@ -47,7 +47,7 @@ echo "Copying application files..."
|
||||
cp -R ./bin/Release/$RUNTIME/publish/* "$MACOS_PATH/"
|
||||
|
||||
# Make the executable actually executable
|
||||
chmod +x "$MACOS_PATH/OrmentiaMarkus"
|
||||
chmod +x "$MACOS_PATH/OrmentiaMarkle"
|
||||
|
||||
# Copy icon
|
||||
echo "Copying application icon..."
|
||||
@@ -65,11 +65,11 @@ cat > "$CONTENTS_PATH/Info.plist" << EOF
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Markus</string>
|
||||
<string>Markle</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Markus</string>
|
||||
<string>Markle</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.ormentia.markus</string>
|
||||
<string>com.ormentia.markle</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -79,7 +79,7 @@ cat > "$CONTENTS_PATH/Info.plist" << EOF
|
||||
<key>CFBundleSignature</key>
|
||||
<string>MRKS</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>OrmentiaMarkus</string>
|
||||
<string>OrmentiaMarkle</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon.icns</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "Building Markus for Windows..." -ForegroundColor Cyan
|
||||
Write-Host "Building Markle for Windows..." -ForegroundColor Cyan
|
||||
|
||||
# Navigate to project root
|
||||
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
@@ -30,7 +30,7 @@ dotnet publish MarkdownEditor.csproj `
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✓ Build completed successfully!" -ForegroundColor Green
|
||||
Write-Host "Application: .\bin\Release\win-x64\OrmentiaMarkus.exe" -ForegroundColor Green
|
||||
Write-Host "Application: .\bin\Release\win-x64\OrmentiaMarkle.exe" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "You can now:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Run the .exe file directly" -ForegroundColor Cyan
|
||||
|
||||
Reference in New Issue
Block a user