├── .editorconfig ├── .gitignore ├── AvRichTextBox.sln ├── AvRichTextBox ├── .gitignore ├── AvRichTextBox.csproj ├── Blocks │ ├── Block.cs │ ├── Paragraph.cs │ └── Table.cs ├── EditableBlocks │ ├── EditableParagraph │ │ ├── EditableParagraph.cs │ │ ├── EditableParagraph_EventHandlers.cs │ │ └── EditableParagraph_Methods.cs │ ├── EditableTable │ │ └── EditableTable.cs │ └── IEditableBlock.cs ├── Editables │ ├── EditableInlineUIContainer.cs │ ├── EditableLineBreak.cs │ ├── EditableRun.cs │ └── IEditable.cs ├── FlowDocument │ ├── FlowDocument.cs │ ├── FlowDocument_Debugger.cs │ ├── FlowDocument_EditingFunctions.cs │ ├── FlowDocument_Formatting.cs │ ├── FlowDocument_InlineManipulation.cs │ ├── FlowDocument_Insert-Delete.cs │ ├── FlowDocument_LoadSave.cs │ ├── FlowDocument_RangeXaml.cs │ ├── FlowDocument_SelectionExtend.cs │ └── FlowDocument_SelectionMove.cs ├── LICENSE.txt ├── README.md ├── RequestExtensions.cs ├── RichTextBox │ ├── RichTextBox.axaml │ ├── RichTextBox.axaml.cs │ ├── RichTextBoxInputClient.cs │ ├── RichTextBox_DocEditing.cs │ ├── RichTextBox_KeyDown.cs │ ├── RichTextBox_PointerEvents.cs │ ├── RichTextBox_RunFormatting.cs │ └── RichTextBox_SelectionRect.cs ├── RichTextBoxViewModel │ └── RichTextBoxViewModel.cs ├── RtfDomParserAv.dll ├── SaveAndLoadFormats │ ├── HtmlConversions │ │ ├── ContentToHtml.cs │ │ └── HtmlToContent.cs │ ├── RtfConversions │ │ ├── ContentToRtf.cs │ │ └── RtfToContent.cs │ ├── WordImportExport │ │ ├── CreateFlowDocument │ │ │ ├── GetFlowDocument.cs │ │ │ ├── GetParagraph.cs │ │ │ ├── GetRun.cs │ │ │ └── GetTable.cs │ │ ├── CreateWordDocument │ │ │ ├── CreateWordDrawing.cs │ │ │ ├── CreateWordParagraph.cs │ │ │ ├── CreateWordRun.cs │ │ │ ├── CreateWordTable.cs │ │ │ └── WordDocumentCreator.cs │ │ └── HelperMethods.cs │ └── Xaml │ │ ├── FlowDocFromXamlString.cs │ │ ├── XamlPackageData │ │ ├── .rels │ │ └── [Content_Types].xml │ │ └── XamlStringFromFlowDoc.cs ├── TextRange │ └── TextRange.cs ├── Undo │ ├── IUndo.cs │ └── Undos.cs ├── UniqueBitmap.cs └── VisualHelper.cs ├── DemoApp_AvRichtextBox ├── App.axaml ├── App.axaml.cs ├── Assets │ └── avalonia-logo.ico ├── DemoApp_AvRichTextBox.csproj ├── NumericSpinner │ ├── NumericSpinner.axaml │ └── NumericSpinner.axaml.cs ├── Program.cs ├── TestFiles │ ├── TestDocumentRtf.rtf │ ├── TestDocumentWord.docx │ ├── TestDocumentWord.html │ └── TestDocumentXamlPackage.xamlp ├── ViewLocator.cs ├── ViewModels │ ├── MainWindowViewModel.cs │ └── ViewModelBase.cs ├── Views │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ └── MainWindow_File.xaml.cs └── app.manifest └── Directory.Build.props /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0220: Add explicit cast 4 | dotnet_style_prefer_foreach_explicit_cast_in_source = always 5 | 6 | # IDE0220: Add explicit cast 7 | dotnet_diagnostic.IDE0220.severity = none 8 | csharp_using_directive_placement = outside_namespace:silent 9 | csharp_prefer_simple_using_statement = true:suggestion 10 | csharp_prefer_braces = true:silent 11 | csharp_style_namespace_declarations = block_scoped:silent 12 | csharp_style_prefer_method_group_conversion = true:silent 13 | csharp_style_prefer_top_level_statements = true:silent 14 | csharp_style_prefer_primary_constructors = true:suggestion 15 | csharp_prefer_system_threading_lock = true:suggestion 16 | csharp_style_expression_bodied_methods = false:silent 17 | csharp_style_expression_bodied_constructors = false:silent 18 | csharp_style_expression_bodied_operators = false:silent 19 | csharp_style_expression_bodied_properties = true:silent 20 | csharp_style_expression_bodied_indexers = true:silent 21 | csharp_style_expression_bodied_accessors = true:silent 22 | csharp_style_expression_bodied_lambdas = true:silent 23 | csharp_style_expression_bodied_local_functions = false:silent 24 | csharp_indent_labels = no_change 25 | 26 | [*.{cs,vb}] 27 | #### Naming styles #### 28 | 29 | # Naming rules 30 | 31 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 32 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 33 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 34 | 35 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 36 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 37 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 38 | 39 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 40 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 41 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 42 | 43 | # Symbol specifications 44 | 45 | dotnet_naming_symbols.interface.applicable_kinds = interface 46 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 47 | dotnet_naming_symbols.interface.required_modifiers = 48 | 49 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 50 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 51 | dotnet_naming_symbols.types.required_modifiers = 52 | 53 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 54 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 55 | dotnet_naming_symbols.non_field_members.required_modifiers = 56 | 57 | # Naming styles 58 | 59 | dotnet_naming_style.begins_with_i.required_prefix = I 60 | dotnet_naming_style.begins_with_i.required_suffix = 61 | dotnet_naming_style.begins_with_i.word_separator = 62 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 63 | 64 | dotnet_naming_style.pascal_case.required_prefix = 65 | dotnet_naming_style.pascal_case.required_suffix = 66 | dotnet_naming_style.pascal_case.word_separator = 67 | dotnet_naming_style.pascal_case.capitalization = pascal_case 68 | 69 | dotnet_naming_style.pascal_case.required_prefix = 70 | dotnet_naming_style.pascal_case.required_suffix = 71 | dotnet_naming_style.pascal_case.word_separator = 72 | dotnet_naming_style.pascal_case.capitalization = pascal_case 73 | dotnet_style_coalesce_expression = true:suggestion 74 | dotnet_style_null_propagation = true:suggestion 75 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 76 | dotnet_style_prefer_auto_properties = true:silent 77 | dotnet_style_object_initializer = true:suggestion 78 | dotnet_style_collection_initializer = true:suggestion 79 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 80 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 81 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 82 | tab_width = 3 83 | indent_size = 3 84 | end_of_line = crlf 85 | dotnet_style_prefer_conditional_expression_over_return = true:silent 86 | -------------------------------------------------------------------------------- /AvRichTextBox.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvRichTextBox", "AvRichTextBox\AvRichTextBox.csproj", "{13845574-3E03-4CFC-8F3E-A076EBC750DE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AvRTB_App.Desktop", "AvRTB_App.Desktop\AvRTB_App.Desktop.csproj", "{E4E264C7-494F-4B81-820D-F5A8738886AC}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvRTB_NugetTesting", "AvRTB_NugetTesting\AvRTB_NugetTesting.csproj", "{C1F95367-C38D-4048-A390-A2E0E02B3030}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{33B80D73-F48F-4CA1-8077-D1AFFBEC773B}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoApp_AvRichTextBox", "DemoApp_AvRichtextBox\DemoApp_AvRichTextBox.csproj", "{ACDA0832-4647-4EB4-BA3A-C3EC71C56900}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {13845574-3E03-4CFC-8F3E-A076EBC750DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {13845574-3E03-4CFC-8F3E-A076EBC750DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {13845574-3E03-4CFC-8F3E-A076EBC750DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {13845574-3E03-4CFC-8F3E-A076EBC750DE}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {E4E264C7-494F-4B81-820D-F5A8738886AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {E4E264C7-494F-4B81-820D-F5A8738886AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {E4E264C7-494F-4B81-820D-F5A8738886AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {E4E264C7-494F-4B81-820D-F5A8738886AC}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {C1F95367-C38D-4048-A390-A2E0E02B3030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {C1F95367-C38D-4048-A390-A2E0E02B3030}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {C1F95367-C38D-4048-A390-A2E0E02B3030}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {C1F95367-C38D-4048-A390-A2E0E02B3030}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {ACDA0832-4647-4EB4-BA3A-C3EC71C56900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {ACDA0832-4647-4EB4-BA3A-C3EC71C56900}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {ACDA0832-4647-4EB4-BA3A-C3EC71C56900}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {ACDA0832-4647-4EB4-BA3A-C3EC71C56900}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {D289C971-2F8E-40AE-A616-CCB56DC9D517} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /AvRichTextBox/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | BatchFiles/ 4 | Assets/ 5 | AvRichTextBox.csproj.vscb.json 6 | -------------------------------------------------------------------------------- /AvRichTextBox/AvRichTextBox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | latest 7 | 8 | 9 | Simplectoplasm 10 | --- 11 | AvRichTextBox 12 | Simplecto.Avalonia.RichTextBox 13 | 1.3.5 14 | A RichTextBox control to be used in Avalonia 15 | Avalonia;UserControl;RichTextBox 16 | A RichTextBox control for Avalonia 17 | README.md 18 | MIT 19 | 20 | https://github.com/cuikp/AvRichTextBox 21 | git 22 | ver 1.3.5: Fixed Rtf line height divide-by-zero save error, and changed referenced RtfDomParser to RtfDomParserAv 23 | 24 | false 25 | false 26 | D:\PROG\LocalNuget\AvRichTextBox 27 | 1.2.5 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | false 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | RtfDomParserAv.dll 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | %(Filename) 86 | 87 | 88 | Code 89 | %(Filename) 90 | 91 | 92 | %(Filename) 93 | 94 | 95 | %(Filename) 96 | 97 | 98 | %(Filename) 99 | 100 | 101 | %(Filename) 102 | 103 | 104 | %(Filename) 105 | 106 | 107 | %(Filename) 108 | 109 | 110 | %(Filename) 111 | 112 | 113 | %(Filename) 114 | 115 | 116 | %(Filename) 117 | 118 | 119 | 120 | 121 | 122 | True 123 | \ 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /AvRichTextBox/Blocks/Block.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Reactive.Linq; 7 | using System.Runtime.CompilerServices; 8 | using System.Threading.Tasks; 9 | 10 | namespace AvRichTextBox; 11 | 12 | public class Block : INotifyPropertyChanged 13 | { 14 | public event PropertyChangedEventHandler? PropertyChanged; 15 | public void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 16 | 17 | private Thickness _Margin = new (0, 0, 0, 0); 18 | public Thickness Margin { get => _Margin; set { _Margin = value; NotifyPropertyChanged(nameof(Margin)); } } 19 | 20 | public string Text 21 | { 22 | get 23 | { 24 | string returnText = ""; 25 | 26 | switch (this.GetType()) 27 | { 28 | case Type t when t == typeof(Paragraph): 29 | returnText = string.Join("", ((Paragraph)this).Inlines.ToList().ConvertAll(ied => ied.InlineText)); 30 | break; 31 | //case Type t when t == typeof(Table): 32 | // returnText = "$"; 33 | // break; 34 | } 35 | return returnText; 36 | } 37 | } 38 | 39 | public bool IsParagraph => this.GetType() == typeof(Paragraph); 40 | //public bool IsTable => this.GetType() == typeof(Table); 41 | 42 | internal int SelectionLength => SelectionEndInBlock - SelectionStartInBlock; 43 | public int BlockLength => this.IsParagraph ? ((Paragraph)this).Inlines.ToList().Sum(il => il.InlineLength) + 1 : 1; //Add one for paragraph itself 44 | 45 | private int _StartInDoc = 0; 46 | internal int StartInDoc 47 | { 48 | get => _StartInDoc; 49 | set { if (_StartInDoc != value) { _StartInDoc = value; NotifyPropertyChanged(nameof(StartInDoc)); } } 50 | } 51 | 52 | internal int EndInDoc => StartInDoc + BlockLength; 53 | 54 | private int _SelectionStartInBlock; 55 | public int SelectionStartInBlock 56 | { 57 | get => _SelectionStartInBlock; 58 | set { if (_SelectionStartInBlock != value) { _SelectionStartInBlock = value; NotifyPropertyChanged(nameof(SelectionStartInBlock)); } } 59 | } 60 | 61 | private int _SelectionEndInBlock; 62 | public int SelectionEndInBlock 63 | { 64 | get => _SelectionEndInBlock; 65 | set 66 | { 67 | 68 | if (_SelectionEndInBlock != value) 69 | { 70 | _SelectionEndInBlock = value; // Set the correct value 71 | NotifyPropertyChanged(nameof(SelectionEndInBlock)); 72 | } 73 | 74 | } 75 | 76 | } 77 | 78 | 79 | public static bool IsFocusable => false; 80 | 81 | internal void ClearSelection() { this.SelectionStartInBlock = 0; this.SelectionEndInBlock = 0; } 82 | internal void CollapseToStart() { if (SelectionStartInBlock != SelectionEndInBlock) SelectionEndInBlock = SelectionStartInBlock; } 83 | internal void CollapseToEnd() { if (SelectionStartInBlock != SelectionEndInBlock) SelectionStartInBlock = SelectionEndInBlock; } 84 | 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /AvRichTextBox/Blocks/Table.cs: -------------------------------------------------------------------------------- 1 | //using Avalonia; 2 | //using Avalonia.Controls; 3 | //using Avalonia.Media; 4 | //using System.Collections.ObjectModel; 5 | //using System.ComponentModel; 6 | //using System.Runtime.CompilerServices; 7 | 8 | //namespace AvRichTextBox; 9 | 10 | //public partial class Table : Block, INotifyPropertyChanged 11 | //{ 12 | // public new event PropertyChangedEventHandler? PropertyChanged; 13 | // private new void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 14 | 15 | // public Thickness BorderThickness => new Thickness(1); 16 | 17 | // public ObservableCollection Cells { get; set; } = []; 18 | // public ColumnDefinitions ColDefs { get; set; } = []; 19 | // public RowDefinitions RowDefs { get; set; } = []; 20 | 21 | 22 | // public Table(int noCols, int noRows, double TableWidth) 23 | // { 24 | // double eqSpacedColWidth = 10D / (double)noCols; 25 | 26 | // for (int rowno = 0; rowno < noRows; rowno++) 27 | // this.RowDefs.Add(new RowDefinition()); 28 | // for (int colno = 0; colno < noCols; colno++) 29 | // this.ColDefs.Add(new ColumnDefinition(eqSpacedColWidth, GridUnitType.Star)); 30 | 31 | // for (int rowno = 0; rowno < noRows; rowno++) 32 | // { 33 | // for (int colno = 0; colno < noCols; colno++) 34 | // { 35 | // Border cellborder = new() { BorderBrush = Brushes.Black, BorderThickness = new Thickness(0.5) }; 36 | // SelectableTextBlock seltb = new() { Text = "this is some text, with wrapping as well." }; 37 | // seltb.Background = Brushes.White; 38 | // seltb.TextWrapping = TextWrapping.Wrap; 39 | // cellborder.Child = seltb; 40 | // Cells.Add(cellborder); 41 | // Grid.SetRow(cellborder, rowno); 42 | // Grid.SetColumn(cellborder, colno); 43 | // } 44 | // } 45 | 46 | 47 | // } 48 | 49 | 50 | 51 | 52 | 53 | //} 54 | 55 | -------------------------------------------------------------------------------- /AvRichTextBox/EditableBlocks/EditableParagraph/EditableParagraph.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Documents; 4 | using Avalonia.Input; 5 | using Avalonia.Interactivity; 6 | using Avalonia.Media; 7 | using Avalonia.Threading; 8 | using System; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | 12 | namespace AvRichTextBox; 13 | 14 | public partial class EditableParagraph : SelectableTextBlock 15 | { 16 | 17 | 18 | public bool IsEditable { get; set; } = true; 19 | 20 | private readonly SolidColorBrush cursorBrush = new (Colors.Cyan, 0.55); 21 | 22 | public int RectCharacterIndex = 0; 23 | 24 | public EditableParagraph() 25 | { 26 | this.SelectionBrush = cursorBrush; 27 | 28 | this.Loaded += EditableParagraph_Loaded; 29 | 30 | this.PropertyChanged += EditableParagraph_PropertyChanged; 31 | 32 | //this.KeyDown += EditableParagraph_KeyDown; 33 | 34 | } 35 | 36 | private void EditableParagraph_Loaded(object? sender, RoutedEventArgs e) 37 | { 38 | UpdateInlines(); 39 | 40 | } 41 | 42 | public static readonly StyledProperty TextLayoutInfoStartRequestedProperty = AvaloniaProperty.Register(nameof(TextLayoutInfoStartRequested)); 43 | public bool TextLayoutInfoStartRequested { get => GetValue(TextLayoutInfoStartRequestedProperty); set { SetValue(TextLayoutInfoStartRequestedProperty, value); } } 44 | 45 | public static readonly StyledProperty TextLayoutInfoEndRequestedProperty = AvaloniaProperty.Register(nameof(TextLayoutInfoEndRequested)); 46 | public bool TextLayoutInfoEndRequested { get => GetValue(TextLayoutInfoEndRequestedProperty); set { SetValue(TextLayoutInfoEndRequestedProperty, value); } } 47 | 48 | public void UpdateVMFromEPStart() 49 | { 50 | SelectionStartRect_Changed?.Invoke(this); 51 | this.SetValue(TextLayoutInfoStartRequestedProperty, false); 52 | 53 | } 54 | 55 | public void UpdateVMFromEPEnd() 56 | { 57 | SelectionEndRect_Changed?.Invoke(this); 58 | this.SetValue(TextLayoutInfoEndRequestedProperty, false); 59 | } 60 | 61 | //public void NotifyCharIndexRect(EditableParagraph ep, Rect selEndRect) 62 | //{ 63 | 64 | // if (CharIndexRect_Notified != null) 65 | // CharIndexRect_Notified(ep, selEndRect); 66 | //} 67 | 68 | 69 | private void EditableParagraph_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) 70 | { 71 | 72 | //Debug.WriteLine("e.propertyName = " + e.Property.Name); 73 | 74 | if (thisPar != null) //because this may be called right after paragraph has been deleted 75 | { 76 | 77 | switch (e.Property.Name) 78 | { 79 | case "Bounds": 80 | //Necessary for initial setting for each created paragraph 81 | if (thisPar != null) 82 | thisPar.FirstIndexLastLine = this.TextLayout.TextLines[^1].FirstTextSourceIndex; 83 | break; 84 | 85 | //case "Inlines": 86 | // UpdateVMFromEPStart(); 87 | // UpdateVMFromEPEnd(); 88 | // break; 89 | 90 | case "LineSpacing": 91 | this.UpdateLayout(); 92 | 93 | if (TextLayout != null && TextLayout.TextLines.Count > 0) 94 | { 95 | double maxLineHeight = Math.Max(TextLayout.TextLines[0].Height, TextLayout.TextLines[^1].Height); 96 | thisPar.LineHeight = maxLineHeight; 97 | } 98 | //Debug.WriteLine("\nline spacing changed: LINESpacing = " + this.LineSpacing); 99 | 100 | break; 101 | 102 | case "SelectionStart": 103 | UpdateVMFromEPStart(); 104 | break; 105 | 106 | case "SelectionEnd": 107 | UpdateVMFromEPEnd(); 108 | break; 109 | 110 | case "SelectedText": 111 | thisPar.UpdateUIContainersSelected(); 112 | 113 | break; 114 | 115 | case "TextLayoutInfoStartRequested": 116 | this.SetValue(TextLayoutInfoStartRequestedProperty, false); 117 | if (thisPar == null) 118 | Dispatcher.UIThread.Post(() => UpdateVMFromEPStart(), DispatcherPriority.Background); 119 | else 120 | UpdateVMFromEPStart(); 121 | break; 122 | 123 | case "TextLayoutInfoEndRequested": 124 | this.SetValue(TextLayoutInfoEndRequestedProperty, false); 125 | if (thisPar == null) 126 | Dispatcher.UIThread.Post(() => UpdateVMFromEPEnd(), DispatcherPriority.Background); 127 | else 128 | UpdateVMFromEPEnd(); 129 | break; 130 | } 131 | 132 | } 133 | 134 | } 135 | 136 | protected override void OnPointerPressed(PointerPressedEventArgs e) 137 | { 138 | //Prevent default behavior 139 | 140 | 141 | } 142 | 143 | protected override void OnKeyDown(KeyEventArgs e) 144 | { 145 | //Keep to override 146 | //Debug.WriteLine("thisEP focused = " + this.IsFocused); 147 | //if (!this.IsFocused) 148 | // e.Handled = true; 149 | //UpdateVMFromEPEnd(); 150 | } 151 | 152 | protected override void OnLostFocus(RoutedEventArgs e) 153 | { 154 | base.OnLostFocus(e); 155 | this.Focusable = false; 156 | } 157 | 158 | protected override void OnGotFocus(GotFocusEventArgs e) 159 | { 160 | base.OnGotFocus(e); 161 | this.Focusable = true; 162 | } 163 | 164 | 165 | protected override void OnPointerMoved(PointerEventArgs e) 166 | { 167 | //e.Handled = true; 168 | 169 | TextHitTestResult result = this.TextLayout.HitTestPoint(e.GetPosition(this)); 170 | 171 | MouseMove?.Invoke(this, result.TextPosition); 172 | 173 | } 174 | 175 | protected override void OnPointerReleased(PointerReleasedEventArgs e) 176 | { 177 | // Prevent default behavior 178 | //e.Handled = true; 179 | } 180 | 181 | internal void UpdateInlines() 182 | { 183 | 184 | if (((Paragraph)this.DataContext!).Inlines != null) 185 | this.Inlines = GetFormattedInlines(); 186 | 187 | //foreach (Inline thisIL in this.Inlines!) 188 | // Debug.WriteLine("1:\n" + ((Run)thisIL).Text + " ::: " + thisIL.FontWeight); 189 | 190 | //this.Height = this.Inlines[0].get 191 | 192 | 193 | this.InvalidateMeasure(); 194 | this.InvalidateVisual(); 195 | 196 | } 197 | 198 | public int SelectionLength => SelectionEnd - SelectionStart; 199 | 200 | Paragraph? thisPar => this.DataContext as Paragraph; 201 | 202 | 203 | public new string Text => string.Join("", ((Paragraph)this.DataContext!).Inlines.ToList().ConvertAll(edinline => edinline.InlineText)); 204 | 205 | 206 | } 207 | 208 | -------------------------------------------------------------------------------- /AvRichTextBox/EditableBlocks/EditableParagraph/EditableParagraph_EventHandlers.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System.Collections.Generic; 3 | 4 | namespace AvRichTextBox; 5 | 6 | public partial class EditableParagraph 7 | { 8 | public delegate void MouseMoveHandler(EditableParagraph sender, int charIndex); 9 | public event MouseMoveHandler? MouseMove; 10 | 11 | public delegate void SelectionStartRectChangedHandler(EditableParagraph sender); 12 | public event SelectionStartRectChangedHandler? SelectionStartRect_Changed; 13 | 14 | public delegate void SelectionEndRectChangedHandler(EditableParagraph sender); 15 | public event SelectionEndRectChangedHandler? SelectionEndRect_Changed; 16 | 17 | //public delegate void TextChangedHandler(EditableParagraph sender); 18 | //public event TextChangedHandler? EditableParagraph_TextChanged; 19 | 20 | //public delegate void CharIndexRectNotifiedHandler(EditableParagraph sender, Rect selEndRect); 21 | //public event CharIndexRectNotifiedHandler? CharIndexRect_Notified; 22 | 23 | //public delegate void KeyDownHandler(EditableParagraph sender, double hitPositionFromLeft); 24 | //public new event KeyDownHandler? KeyDown; 25 | 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /AvRichTextBox/EditableBlocks/EditableParagraph/EditableParagraph_Methods.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls.Documents; 2 | using Avalonia.Media; 3 | using System; 4 | using System.Diagnostics; 5 | 6 | namespace AvRichTextBox; 7 | 8 | public partial class EditableParagraph 9 | { 10 | 11 | private InlineCollection GetFormattedInlines() 12 | { 13 | 14 | InlineCollection returnInlines = []; 15 | foreach (IEditable ied in ((Paragraph)this.DataContext!).Inlines) 16 | returnInlines.Add(ied.BaseInline); 17 | 18 | return returnInlines; 19 | 20 | } 21 | 22 | 23 | private int GetClosestIndex(int lineNo, double distanceFromLeft, int direction) 24 | { 25 | CharacterHit chit = this.TextLayout.TextLines[lineNo + direction].GetCharacterHitFromDistance(distanceFromLeft); 26 | 27 | double CharDistanceDiffThis = Math.Abs(distanceFromLeft - this.TextLayout.HitTestTextPosition(chit.FirstCharacterIndex).Left); 28 | double CharDistanceDiffNext = Math.Abs(distanceFromLeft - this.TextLayout.HitTestTextPosition(chit.FirstCharacterIndex + 1).Left); 29 | 30 | if (CharDistanceDiffThis > CharDistanceDiffNext) 31 | return chit.FirstCharacterIndex + 1; 32 | else 33 | return chit.FirstCharacterIndex; 34 | 35 | 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /AvRichTextBox/EditableBlocks/EditableTable/EditableTable.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Interactivity; 4 | using System.Collections.ObjectModel; 5 | 6 | namespace AvRichTextBox; 7 | 8 | 9 | public partial class EditableTable : Grid, IEditableBlock 10 | { 11 | ////public bool IsEditable { get; set; } = true; 12 | ////SolidColorBrush cursorBrush = new SolidColorBrush(Colors.Cyan, 0.55); 13 | 14 | //public static readonly StyledProperty> CellsProperty = AvaloniaProperty.Register>(nameof(Cells)); 15 | //public static readonly StyledProperty ColDefsProperty = AvaloniaProperty.Register(nameof(ColDefs)); 16 | //public static readonly StyledProperty RowDefsProperty = AvaloniaProperty.Register(nameof(RowDefs)); 17 | 18 | //public ColumnDefinitions ColDefs 19 | //{ 20 | // get => GetValue(ColDefsProperty); 21 | // set { SetValue(ColDefsProperty, value); } 22 | //} 23 | 24 | //public RowDefinitions RowDefs 25 | //{ 26 | // get => GetValue(RowDefsProperty); 27 | // set { SetValue(RowDefsProperty, value); } 28 | //} 29 | 30 | //public ObservableCollection Cells 31 | //{ 32 | // get => GetValue(CellsProperty); 33 | // set => SetValue(CellsProperty, value); 34 | //} 35 | 36 | //public EditableTable() 37 | //{ 38 | // this.Loaded += EditableTable_Loaded; 39 | 40 | //} 41 | 42 | //private void Cells_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 43 | //{ 44 | // foreach (Border newCell in e.NewItems) 45 | // this.Children.Add(newCell); 46 | //} 47 | 48 | //private void ColDefs_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 49 | //{ 50 | // foreach (ColumnDefinition cdef in e.NewItems) 51 | // ColumnDefinitions.Add(cdef); 52 | // foreach (ColumnDefinition cdef in e.OldItems) 53 | // ColumnDefinitions.Remove(cdef); 54 | 55 | //} 56 | 57 | //private void RowDefs_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 58 | //{ 59 | // foreach (RowDefinition rdef in e.NewItems) 60 | // RowDefinitions.Add(rdef); 61 | // foreach (RowDefinition rdef in e.OldItems) 62 | // RowDefinitions.Remove(rdef); 63 | 64 | //} 65 | 66 | //private void EditableTable_Loaded(object? sender, RoutedEventArgs e) 67 | //{ 68 | // //ColDefs.CollectionChanged += ColDefs_CollectionChanged; 69 | // //RowDefs.CollectionChanged += RowDefs_CollectionChanged; 70 | // Cells.CollectionChanged += Cells_CollectionChanged; 71 | 72 | // RowDefinitions = RowDefs; 73 | // ColumnDefinitions = ColDefs; 74 | 75 | // foreach (Border b in Cells) 76 | // { 77 | // this.Children.Add(b); 78 | // //Debug.WriteLine(Grid.GetRow(b)); 79 | // } 80 | 81 | // //Debug.WriteLine("cellcount=" + Cells.Count); 82 | 83 | // this.UpdateLayout(); 84 | //} 85 | 86 | ////public string Text => string.Join("", ((Table)this.DataContext).Inlines.ToList().ConvertAll(edinline => edinline.InlineText)); 87 | 88 | 89 | 90 | } 91 | 92 | -------------------------------------------------------------------------------- /AvRichTextBox/EditableBlocks/IEditableBlock.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Data; 4 | using Avalonia.Input; 5 | using Avalonia.Interactivity; 6 | using Avalonia.Media; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | 10 | namespace AvRichTextBox; 11 | 12 | public interface IEditableBlock 13 | { 14 | 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /AvRichTextBox/Editables/EditableInlineUIContainer.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Documents; 4 | using Avalonia.Media; 5 | using System.ComponentModel; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace AvRichTextBox; 9 | 10 | public class EditableInlineUIContainer : InlineUIContainer, IEditable, INotifyPropertyChanged 11 | { 12 | public new event PropertyChangedEventHandler? PropertyChanged; 13 | private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 14 | 15 | public EditableInlineUIContainer(Control c) { Child = c; } 16 | 17 | public Inline BaseInline => this; 18 | public Paragraph? myParagraph { get; set; } 19 | public int TextPositionOfInlineInParagraph { get; set; } 20 | public string InlineText { get; set; } = "@"; 21 | public string DisplayInlineText { get => " => " + (this.Child != null && this.Child.GetType() == typeof(Image) ? "Image" : "NoChild"); } 22 | public string FontName => "---"; 23 | public int InlineLength => 1; 24 | public bool IsEmpty => false; 25 | public bool IsLastInlineOfParagraph { get; set; } 26 | //public double InlineHeight => (this.Child != null && this.Child.GetType() == typeof(Image) ? : this.Child.Bounds.Height; 27 | public double InlineHeight => Child == null ? 0 : this.Child.Bounds.Height; 28 | 29 | 30 | public int ImageNo; 31 | 32 | public IEditable Clone() { return new EditableInlineUIContainer(this.Child){ myParagraph = this.myParagraph }; } 33 | 34 | //for DebuggerPanel 35 | private bool _IsStartInline = false; 36 | public bool IsStartInline { get => _IsStartInline; set { _IsStartInline = value; NotifyPropertyChanged(nameof(BackBrush)); NotifyPropertyChanged(nameof(InlineSelectedBorderThickness)); } } 37 | private bool _IsEndInline = false; 38 | public bool IsEndInline { get => _IsEndInline; set { _IsEndInline = value; NotifyPropertyChanged(nameof(BackBrush)); } } 39 | private bool _IsWithinSelectionInline = false; 40 | public bool IsWithinSelectionInline { get => _IsWithinSelectionInline; set { _IsWithinSelectionInline = value; NotifyPropertyChanged(nameof(BackBrush)); } } 41 | public Thickness InlineSelectedBorderThickness => (IsStartInline || IsEndInline) ? new Thickness(3) : new Thickness(1); 42 | 43 | public SolidColorBrush BackBrush 44 | { 45 | get 46 | { 47 | if (IsStartInline) return new SolidColorBrush(Colors.LawnGreen); 48 | else if (IsEndInline) return new SolidColorBrush(Colors.Pink); 49 | else if (IsWithinSelectionInline) return new SolidColorBrush(Colors.LightGray); 50 | else return new SolidColorBrush(Colors.Transparent); 51 | } 52 | } 53 | 54 | public string InlineToolTip => ""; 55 | 56 | private bool _IsSelected = false; 57 | public bool IsSelected { get => _IsSelected; set { _IsSelected = value; this.Child.Opacity = value ? 0.2 : 1; } } 58 | 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /AvRichTextBox/Editables/EditableLineBreak.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Documents; 4 | using Avalonia.Media; 5 | using System.ComponentModel; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace AvRichTextBox; 9 | 10 | public class EditableLineBreak : LineBreak, IEditable, INotifyPropertyChanged 11 | { 12 | public new event PropertyChangedEventHandler? PropertyChanged; 13 | private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 14 | 15 | public EditableLineBreak() { } 16 | 17 | public Inline BaseInline => this; 18 | public Paragraph? myParagraph { get; set; } 19 | public int TextPositionOfInlineInParagraph { get; set; } 20 | public string InlineText { get; set; } = @"\v"; //make literal to count as 2 characters 21 | //public string InlineText { get; set; } = "\v"; 22 | public string DisplayInlineText => "{>LINEBREAK<}"; 23 | public int InlineLength => 2; //because LineBreak acts as a double character in TextBlock? - anyway don't use LineBreak, use \v instead 24 | public double InlineHeight => FontSize; 25 | public string FontName => "---"; 26 | public bool IsEmpty => false; 27 | public bool IsLastInlineOfParagraph { get; set; } 28 | 29 | public IEditable Clone() => new EditableLineBreak() { myParagraph = this.myParagraph }; 30 | 31 | 32 | //for DebuggerPanel 33 | private bool _IsStartInline = false; 34 | public bool IsStartInline { get => _IsStartInline; set { _IsStartInline = value; NotifyPropertyChanged(nameof(BackBrush)); NotifyPropertyChanged(nameof(InlineSelectedBorderThickness)); } } 35 | private bool _IsEndInline = false; 36 | public bool IsEndInline { get => _IsEndInline; set { _IsEndInline = value; NotifyPropertyChanged(nameof(BackBrush)); NotifyPropertyChanged(nameof(InlineSelectedBorderThickness)); } } 37 | private bool _IsWithinSelectionInline = false; 38 | public bool IsWithinSelectionInline { get => _IsWithinSelectionInline; set { _IsWithinSelectionInline = value; NotifyPropertyChanged(nameof(BackBrush)); } } 39 | public Thickness InlineSelectedBorderThickness => (IsStartInline || IsEndInline) ? new Thickness(3) : new Thickness(1); 40 | 41 | public SolidColorBrush BackBrush 42 | { 43 | get 44 | { 45 | if (IsStartInline) return new SolidColorBrush(Colors.LawnGreen); 46 | else if (IsEndInline) return new SolidColorBrush(Colors.Pink); 47 | else if (IsWithinSelectionInline) return new SolidColorBrush(Colors.LightGray); 48 | else return new SolidColorBrush(Colors.Transparent); 49 | } 50 | } 51 | 52 | public string InlineToolTip => ""; 53 | 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /AvRichTextBox/Editables/EditableRun.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.Documents; 3 | using Avalonia.Media; 4 | using System.ComponentModel; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AvRichTextBox; 8 | 9 | public class EditableRun : Run, IEditable, INotifyPropertyChanged 10 | { 11 | public new event PropertyChangedEventHandler? PropertyChanged; 12 | private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 13 | 14 | public EditableRun() { } 15 | 16 | public EditableRun(string text) 17 | { 18 | this.Text = text; 19 | //FontFamily = "Meiryo"; 20 | FontSize = 16; 21 | BaselineAlignment = BaselineAlignment.Baseline; 22 | } 23 | 24 | public Inline BaseInline => this; 25 | public Paragraph? myParagraph { get; set; } 26 | public int TextPositionOfInlineInParagraph { get; set; } 27 | public string InlineText { get => Text!; set => Text = value; } 28 | public string DisplayInlineText 29 | { 30 | get => IsEmpty ? "{>EMPTY<}" : (InlineText.Length == 1 ? Text!.Replace(" ", "{>SPACE<}").Replace("\t", "{>TAB<}") : Text!.Replace("\t", "{>TAB<}")); 31 | } 32 | 33 | public int InlineLength => InlineText.Length; 34 | public double InlineHeight => FontSize; 35 | public bool IsEmpty => InlineText.Length == 0; 36 | public string FontName => FontFamily?.Name == null ? "" : FontFamily?.Name!; 37 | 38 | public bool IsLastInlineOfParagraph { get; set; } 39 | 40 | public IEditable Clone() 41 | { 42 | return new EditableRun(this.Text!) 43 | { 44 | FontStyle = this.FontStyle, 45 | FontWeight = this.FontWeight, 46 | TextDecorations = this.TextDecorations, 47 | FontSize = this.FontSize, 48 | FontFamily = this.FontFamily, 49 | Background = this.Background, 50 | myParagraph = this.myParagraph, 51 | TextPositionOfInlineInParagraph = this.TextPositionOfInlineInParagraph, //necessary because clone is produced when calculating range inline positions 52 | IsLastInlineOfParagraph = this.IsLastInlineOfParagraph, 53 | BaselineAlignment = this.BaselineAlignment, 54 | Foreground = this.Foreground, 55 | }; 56 | } 57 | 58 | //For DebuggerPanel 59 | private bool _IsStartInline = false; 60 | public bool IsStartInline { get => _IsStartInline; set { _IsStartInline = value; NotifyPropertyChanged(nameof(BackBrush)); NotifyPropertyChanged(nameof(InlineSelectedBorderThickness)); } } 61 | private bool _IsEndInline = false; 62 | public bool IsEndInline { get => _IsEndInline; set { _IsEndInline = value; NotifyPropertyChanged(nameof(BackBrush)); NotifyPropertyChanged(nameof(InlineSelectedBorderThickness)); } } 63 | private bool _IsWithinSelectionInline = false; 64 | public bool IsWithinSelectionInline { get => _IsWithinSelectionInline; set { _IsWithinSelectionInline = value; NotifyPropertyChanged(nameof(BackBrush)); } } 65 | public Thickness InlineSelectedBorderThickness => (IsStartInline || IsEndInline) ? new Thickness(3) : new Thickness(0.7); 66 | 67 | public SolidColorBrush BackBrush 68 | { 69 | get 70 | { 71 | if (IsStartInline) return new SolidColorBrush(Colors.LawnGreen); 72 | else if (IsEndInline) return new SolidColorBrush(Colors.Pink); 73 | else if (IsWithinSelectionInline) return new SolidColorBrush(Colors.LightGray); 74 | else return new SolidColorBrush(Colors.Transparent); 75 | } 76 | } 77 | 78 | public string InlineToolTip => $"Background: {Background}\nForeground: {Foreground}\nFontFamily: {FontFamily}"; 79 | 80 | 81 | } 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /AvRichTextBox/Editables/IEditable.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.Documents; 3 | using Avalonia.Media; 4 | using System.ComponentModel; 5 | 6 | namespace AvRichTextBox; 7 | 8 | public interface IEditable : INotifyPropertyChanged 9 | { 10 | 11 | Inline BaseInline { get; } 12 | 13 | internal Paragraph? myParagraph { get; set; } 14 | 15 | internal bool IsStartInline { get; set; } 16 | internal bool IsEndInline { get; set; } 17 | internal bool IsWithinSelectionInline { get; set; } 18 | internal bool IsLastInlineOfParagraph { get; set; } 19 | internal int TextPositionOfInlineInParagraph { get; set; } 20 | internal int GetCharPosInInline(int charPos) => charPos - myParagraph!.StartInDoc - TextPositionOfInlineInParagraph; 21 | public string InlineText { get; set; } 22 | public bool IsEmpty { get; } 23 | public int InlineLength { get; } 24 | public double InlineHeight { get; } 25 | public IEditable Clone(); 26 | public bool IsRun => this.GetType() == typeof(EditableRun); 27 | public bool IsUIContainer => this.GetType() == typeof(EditableInlineUIContainer); 28 | public bool IsLineBreak => this.GetType() == typeof(EditableLineBreak); 29 | 30 | public Thickness InlineSelectedBorderThickness { get; } 31 | public SolidColorBrush BackBrush { get; } 32 | public string DisplayInlineText { get; } 33 | public string InlineToolTip { get; } 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /AvRichTextBox/FlowDocument/FlowDocument_Debugger.cs: -------------------------------------------------------------------------------- 1 | using DynamicData; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Reactive.Linq; 6 | 7 | namespace AvRichTextBox; 8 | 9 | public partial class FlowDocument 10 | { 11 | 12 | internal bool ShowDebugger = false; 13 | internal ObservableCollection SelectionParagraphs { get; set; } = []; // for DebuggerPanel 14 | 15 | private void UpdateDebuggerSelectionParagraphs() 16 | { 17 | 18 | #if DEBUG 19 | 20 | SelectionParagraphs.Clear(); 21 | SelectionParagraphs.AddRange(Blocks.Where(p => p.StartInDoc + p.BlockLength > Selection.Start && p.StartInDoc <= Selection.End).ToList().ConvertAll(bb => (Paragraph)bb)); 22 | 23 | //Visuals for DebuggerPanel 24 | foreach (Paragraph p in SelectionParagraphs) 25 | { 26 | foreach (IEditable ied in p.Inlines) 27 | { 28 | IEditable startInline = Selection.GetStartInline(); 29 | IEditable endInline = Selection.GetEndInline(); 30 | int thisRunIndex = p.Inlines.IndexOf(ied); 31 | ied.IsStartInline = ied == startInline; 32 | ied.IsEndInline = ied == endInline; 33 | ied.IsWithinSelectionInline = 34 | startInline != null && endInline != null && thisRunIndex > p.Inlines.IndexOf(startInline) && thisRunIndex < p.Inlines.IndexOf(endInline); 35 | } 36 | } 37 | 38 | #endif 39 | 40 | } 41 | 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /AvRichTextBox/FlowDocument/FlowDocument_EditingFunctions.cs: -------------------------------------------------------------------------------- 1 | using DynamicData; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace AvRichTextBox; 9 | 10 | public partial class FlowDocument 11 | { 12 | 13 | private Dictionary> KeepParsAndInlines(TextRange tRange) 14 | { 15 | Dictionary> returnDict = []; 16 | 17 | List allBlocks = GetRangeBlocks(tRange); 18 | foreach (Block b in allBlocks) 19 | { 20 | List inlines = []; 21 | if (b is Paragraph p) 22 | inlines.AddRange(p.Inlines.ToList().ConvertAll(il => il.Clone())); 23 | returnDict.TryAdd(b, inlines); 24 | 25 | } 26 | 27 | return returnDict; 28 | 29 | } 30 | 31 | internal int SetRangeToInlines(TextRange tRange, List newInlines) 32 | { // All of this should constitute one Undo operation 33 | //Debug.WriteLine("newinlines=\n" + string.Join("\n", newInlines.ConvertAll(il => il.InlineText))); 34 | 35 | int addedCharCount = 0; 36 | 37 | Paragraph startPar = tRange.StartParagraph; 38 | 39 | int rangeStart = tRange.Start; 40 | int deleteRangeLength = tRange.Length; 41 | int parIndex = Blocks.IndexOf(startPar); 42 | 43 | Undos.Add(new PasteUndo(KeepParsAndInlines(tRange), parIndex, this, rangeStart, deleteRangeLength - newInlines.Sum(nil=>nil.InlineLength))); 44 | 45 | //Delete selected range first 46 | if (tRange.Length > 0) 47 | DeleteRange(tRange, false); 48 | 49 | IEditable startInline = tRange.GetStartInline(); 50 | List splitInlines = SplitRunAtPos(tRange.Start, startInline, startInline.GetCharPosInInline(tRange.Start)); 51 | 52 | int insertionPt = startPar.Inlines.IndexOf(splitInlines[0]) + 1; 53 | 54 | int startInlineIndex = startPar.Inlines.IndexOf(splitInlines[0]) + 1; 55 | Paragraph addPar = startPar; 56 | int inlineno = 0; 57 | foreach (IEditable newinline in newInlines) 58 | { 59 | inlineno++; 60 | 61 | bool addnewpar = false; 62 | 63 | if (newinline.InlineText.EndsWith('\r')) 64 | { 65 | newinline.InlineText = newinline.InlineText[..^1]; 66 | addnewpar = inlineno > 1; 67 | //Debug.WriteLine("addnew par? " + addnewpar + " (" + newinline.InlineText + ")"); 68 | } 69 | 70 | if (addnewpar) 71 | { 72 | List moveInlines = addPar.Inlines.Take(new Range(0, startInlineIndex)).ToList(); // create an independent new list 73 | addPar.Inlines.RemoveMany(moveInlines); 74 | 75 | //Create new paragraph to insert 76 | addPar = new Paragraph(); 77 | addPar.Inlines.AddRange(moveInlines); 78 | startInlineIndex = addPar.Inlines.Count; 79 | Blocks.Insert(parIndex, addPar); 80 | addPar.CallRequestInlinesUpdate(); 81 | UpdateBlockAndInlineStarts(addPar); 82 | addedCharCount += 1; 83 | } 84 | 85 | addPar.Inlines.Insert(startInlineIndex, newinline); 86 | addPar.CallRequestInlinesUpdate(); 87 | UpdateBlockAndInlineStarts(addPar); 88 | addedCharCount += newinline.InlineLength; 89 | } 90 | 91 | if (splitInlines[0].InlineText == "") 92 | startPar.Inlines.Remove(splitInlines[0]); 93 | 94 | startPar.CallRequestInlinesUpdate(); 95 | UpdateBlockAndInlineStarts(startPar); 96 | 97 | return addedCharCount; 98 | 99 | } 100 | 101 | 102 | internal void SetRangeToText(TextRange tRange, string newText) 103 | { //The delete range and SetRangeToText should constitute one Undo operation 104 | 105 | Paragraph startPar = tRange.StartParagraph; 106 | int rangeStart = tRange.Start; 107 | int deleteRangeLength = tRange.Length; 108 | int parIndex = Blocks.IndexOf(startPar); 109 | 110 | Undos.Add(new PasteUndo(KeepParsAndInlines(tRange), parIndex, this, rangeStart, deleteRangeLength - newText.Length)); 111 | 112 | //Delete any selected text first 113 | if (tRange.Length > 0) 114 | { 115 | DeleteRange(tRange, false); 116 | tRange.CollapseToStart(); 117 | SelectionExtendMode = ExtendMode.ExtendModeNone; 118 | } 119 | 120 | IEditable startInline = tRange.GetStartInline(); 121 | List splitInlines = SplitRunAtPos(tRange.Start, startInline, startInline.GetCharPosInInline(tRange.Start)); 122 | 123 | int startInlineIndex = startPar.Inlines.IndexOf(splitInlines[0]) + 1; 124 | 125 | EditableRun? sRun = splitInlines[0] as EditableRun; 126 | EditableRun newEditableRun = new(newText) 127 | { 128 | FontFamily = sRun!.FontFamily, 129 | FontWeight = sRun.FontWeight, 130 | FontStyle = sRun.FontStyle, 131 | FontSize = sRun.FontSize, 132 | TextDecorations = sRun.TextDecorations, 133 | Background = sRun.Background 134 | }; 135 | 136 | startPar.Inlines.Insert(startInlineIndex, newEditableRun); 137 | 138 | if (splitInlines[0].InlineText == "") 139 | startPar.Inlines.Remove(splitInlines[0]); 140 | 141 | startPar.CallRequestInlinesUpdate(); 142 | UpdateBlockAndInlineStarts(startPar); 143 | 144 | } 145 | 146 | 147 | internal void Undo() 148 | { 149 | if (Undos.Count > 0) 150 | { 151 | Undos.Last().PerformUndo(); 152 | 153 | UpdateSelection(); 154 | 155 | if (Undos.Last().UpdateTextRanges) 156 | UpdateTextRanges(Selection.Start, Undos.Last().UndoEditOffset); 157 | 158 | Undos.RemoveAt(Undos.Count - 1); 159 | 160 | if (ShowDebugger) 161 | UpdateDebuggerSelectionParagraphs(); 162 | 163 | ScrollInDirection!(1); 164 | ScrollInDirection!(-1); 165 | } 166 | } 167 | 168 | internal void RestoreDeletedBlocks(Dictionary> parsAndInlines, int blockIndex) 169 | { 170 | //Reset all paragraphs with new inlines exactly as before 171 | 172 | foreach (KeyValuePair> restorePar in parsAndInlines) 173 | { 174 | if (restorePar.Key is Paragraph p) 175 | { 176 | p.Inlines.Clear(); 177 | p.CallRequestInlinesUpdate(); 178 | p.Inlines.AddRange(restorePar.Value); 179 | } 180 | } 181 | 182 | //Restore all of the previous paragraphs 183 | Blocks.RemoveAt(blockIndex); 184 | Blocks.AddOrInsertRange(parsAndInlines.ToList().ConvertAll(pil => pil.Key), blockIndex); 185 | 186 | foreach (KeyValuePair> restorePar in parsAndInlines) 187 | if (restorePar.Key.IsParagraph) 188 | { 189 | Paragraph? p = restorePar.Key as Paragraph; 190 | p!.CallRequestInlinesUpdate(); 191 | p.ClearSelection(); 192 | } 193 | 194 | UpdateBlockAndInlineStarts(blockIndex); 195 | 196 | } 197 | 198 | [GeneratedRegex(@"[\r\n]")] 199 | internal static partial Regex FindLineBreakCharsRegex(); 200 | 201 | 202 | internal IEditable GetStartInline(int charIndex) 203 | { 204 | 205 | Paragraph? startPar = Blocks.LastOrDefault(b => b.IsParagraph && (b.StartInDoc <= charIndex))! as Paragraph; 206 | 207 | if (startPar != null) 208 | { 209 | //Check if start at end of last paragraph (cannot span from end of a paragraph) 210 | if (startPar != Blocks.Where(b => b.IsParagraph).Last() && startPar!.EndInDoc == charIndex) 211 | startPar = Blocks.FirstOrDefault(b => b.IsParagraph && Blocks.IndexOf(b) > Blocks.IndexOf(startPar))! as Paragraph; 212 | } 213 | 214 | if (startPar == null) return null!; 215 | 216 | IEditable startInline = null!; 217 | bool IsAtLineBreak = false; 218 | 219 | IEditable startInlineReal = startPar.Inlines.LastOrDefault(ied => startPar.StartInDoc + ied.TextPositionOfInlineInParagraph <= charIndex)!; 220 | startInline = startPar.Inlines.LastOrDefault(ied => !ied.IsLineBreak && startPar.StartInDoc + ied.TextPositionOfInlineInParagraph <= charIndex)!; 221 | IsAtLineBreak = startInline != startInlineReal; 222 | 223 | return startInline!; 224 | 225 | } 226 | 227 | 228 | } -------------------------------------------------------------------------------- /AvRichTextBox/FlowDocument/FlowDocument_LoadSave.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml.Packaging; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Text.RegularExpressions; 6 | using static AvRichTextBox.WordConversions; 7 | using static AvRichTextBox.RtfConversions; 8 | using static AvRichTextBox.XamlConversions; 9 | using RtfDomParserAv; 10 | using System.Text; 11 | using HtmlAgilityPack; 12 | 13 | namespace AvRichTextBox; 14 | 15 | public partial class FlowDocument 16 | { 17 | internal void LoadRtf(string rtfContent) 18 | { 19 | RTFDomDocument rtfdom = new(); 20 | //rtfdom.Load(fileName); 21 | 22 | // Do this to fix malformed `\o "` and orphaned quotes 23 | if (rtfContent.Contains("\\o ")) 24 | { 25 | rtfContent = rtfContent.Replace("\\o \"}", "\\o \"\"}").Replace(" \"}", " }"); 26 | rtfContent = Regex.Replace(rtfContent, "\\\\o \".*?\"", "\\o\"\""); 27 | } 28 | using MemoryStream rtfStream = new(Encoding.UTF8.GetBytes(rtfContent)); 29 | using StreamReader streamReader = new(rtfStream); 30 | 31 | rtfdom.Load(streamReader.BaseStream); 32 | 33 | try 34 | { 35 | ClearDocument(); 36 | GetFlowDocumentFromRtf(rtfdom!, this); 37 | InitializeDocument(); 38 | } 39 | catch (Exception ex2) { Debug.WriteLine($"error getting flow doc:\n{ex2.Message}"); } 40 | } 41 | internal void LoadRtfFromFile(string fileName) 42 | { 43 | try 44 | { 45 | string rtfContent = File.ReadAllText(fileName); 46 | LoadRtf(rtfContent); 47 | } 48 | catch (Exception ex3) 49 | { 50 | if (ex3.HResult == -2147024864) 51 | throw new IOException($"The file:\n{fileName}\ncannot be opened because it is currently in use by another application.", ex3); 52 | else 53 | Debug.WriteLine($"Error trying to open file: {ex3.Message}"); 54 | } 55 | } 56 | 57 | internal void SaveRtfToFile(string fileName) 58 | { 59 | try 60 | { 61 | //string rtfText = GetRtfFromFlowDocumentBlocks(this.Blocks); 62 | string rtfText = SaveRtf(); 63 | File.WriteAllText(fileName, rtfText, Encoding.Default); 64 | //Debug.WriteLine(rtfText); 65 | } 66 | catch (Exception ex2) { Debug.WriteLine($"error getting flow doc:\n{ex2.Message}"); } 67 | } 68 | internal string SaveRtf() 69 | { 70 | return GetRtfFromFlowDocument(this); 71 | } 72 | 73 | 74 | internal void SaveXamlToFile(string fileName) 75 | { 76 | File.WriteAllText(fileName, SaveXaml()); 77 | } 78 | 79 | internal void LoadXamlFromFile(string fileName) 80 | { 81 | string xamlDocString = File.ReadAllText(fileName); 82 | LoadXaml(xamlDocString); 83 | } 84 | 85 | internal string SaveXaml() 86 | { 87 | return GetDocXaml(false, this); 88 | } 89 | 90 | internal void LoadXaml(string xamlContent) 91 | { 92 | ProcessXamlString(xamlContent, this); 93 | InitializeDocument(); 94 | } 95 | 96 | internal void SaveHtmlDocToFile(string fileName) 97 | { 98 | HtmlDocument hdoc = HtmlConversions.GetHtmlFromFlowDocument(this); 99 | hdoc.Save(fileName); 100 | } 101 | internal string SaveHtml() 102 | { 103 | HtmlDocument hdoc = HtmlConversions.GetHtmlFromFlowDocument(this); 104 | return hdoc.DocumentNode.OuterHtml; 105 | } 106 | 107 | internal void LoadHtmlDocFromFile(string fileName) 108 | { 109 | try 110 | { 111 | LoadHtml(File.ReadAllText(fileName)); 112 | } 113 | catch (Exception ex3) 114 | { 115 | if (ex3.HResult == -2147024864) 116 | throw new IOException($"The file:\n{fileName}\ncannot be opened because it is currently in use by another application.\n{ex3.Message}"); 117 | else 118 | Debug.WriteLine($"Error trying to open file: {ex3.Message}"); 119 | } 120 | 121 | } 122 | internal void LoadHtml(string htmlContent) 123 | { 124 | try 125 | { 126 | ClearDocument(); 127 | HtmlDocument hdoc = new(); 128 | hdoc.LoadHtml(htmlContent); 129 | HtmlConversions.GetFlowDocumentFromHtml(hdoc, this); 130 | InitializeDocument(); 131 | } 132 | catch (Exception ex2) { Debug.WriteLine("error getting flow doc:\n" + ex2.Message); } 133 | } 134 | 135 | 136 | internal void SaveWordDocToFile(string fileName) 137 | { 138 | WordConversions.SaveWordDoc(fileName, this); 139 | } 140 | 141 | internal void LoadWordDocFromFile(string fileName) 142 | { 143 | try 144 | { 145 | using WordprocessingDocument WordDoc = WordprocessingDocument.Open(fileName, false); 146 | try 147 | { 148 | ClearDocument(); 149 | GetFlowDocument(WordDoc.MainDocumentPart!, this); 150 | InitializeDocument(); 151 | } 152 | catch (Exception ex2) { Debug.WriteLine("error getting flow doc:\n" + ex2.Message); } 153 | } 154 | catch (Exception ex3) 155 | { 156 | if (ex3.HResult == -2147024864) 157 | throw new IOException($"The file:\n{fileName}\ncannot be opened because it is currently in use by another application.", ex3); 158 | else 159 | Debug.WriteLine($"Error trying to open file: {ex3.Message}"); 160 | } 161 | 162 | } 163 | 164 | internal void LoadXamlPackage(string fileName) 165 | { 166 | 167 | XamlConversions.LoadXamlPackage(fileName, this); 168 | 169 | InitializeDocument(); 170 | 171 | } 172 | 173 | 174 | internal void SaveXamlPackage(string fileName) 175 | { 176 | XamlConversions.SaveXamlPackage(fileName, this); 177 | } 178 | 179 | } 180 | 181 | 182 | -------------------------------------------------------------------------------- /AvRichTextBox/FlowDocument/FlowDocument_RangeXaml.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using static AvRichTextBox.XamlConversions; 3 | using System.Text; 4 | 5 | 6 | namespace AvRichTextBox; 7 | 8 | public partial class FlowDocument 9 | { 10 | public void SaveRangeToXamlStream(TextRange trange, Stream stream) 11 | { 12 | StringBuilder rangeXamlBuilder = new(SectionTextDefault); 13 | rangeXamlBuilder.Append(GetParagraphRunsXaml(CreateNewInlinesForRange(trange), false)); 14 | rangeXamlBuilder.Append(""); 15 | byte[] stringBytes = Encoding.UTF8.GetBytes(rangeXamlBuilder.ToString()); 16 | stream.Write(stringBytes, 0, stringBytes.Length); 17 | 18 | } 19 | 20 | internal void LoadXamlStreamIntoRange (Stream stream, TextRange trange) 21 | { 22 | byte[] streamBytes = new byte[stream.Length]; 23 | stream.Read(streamBytes, 0, streamBytes.Length); 24 | string xamlString = Encoding.UTF8.GetString(streamBytes, 0, streamBytes.Length); 25 | //ProcessXamlString(xamlString); 26 | 27 | } 28 | 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /AvRichTextBox/FlowDocument/FlowDocument_SelectionExtend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace AvRichTextBox; 5 | 6 | public partial class FlowDocument 7 | { 8 | 9 | internal void ExtendSelectionRight() 10 | { 11 | Selection.BiasForwardEnd = true; 12 | 13 | switch (SelectionExtendMode) 14 | { 15 | case ExtendMode.ExtendModeNone: 16 | case ExtendMode.ExtendModeRight: 17 | 18 | SelectionExtendMode = ExtendMode.ExtendModeRight; 19 | 20 | if (Selection.EndParagraph == Blocks[^1] && Selection.EndParagraph.SelectionEndInBlock == Selection.EndParagraph.Text.Length) 21 | return; // End of document 22 | 23 | Selection!.End += 1; 24 | 25 | break; 26 | 27 | case ExtendMode.ExtendModeLeft: 28 | 29 | Selection!.Start += 1; 30 | if (Selection.Start == Selection.End) 31 | SelectionExtendMode = ExtendMode.ExtendModeRight; 32 | 33 | break; 34 | } 35 | 36 | ScrollInDirection!(1); 37 | 38 | } 39 | 40 | internal void ExtendSelectionLeft() 41 | { 42 | Selection.BiasForwardEnd = false; 43 | 44 | switch (SelectionExtendMode) 45 | { 46 | case ExtendMode.ExtendModeNone: 47 | case ExtendMode.ExtendModeLeft: 48 | if (Selection!.Start == 0) return; 49 | Selection.Start -= 1; 50 | SelectionExtendMode = ExtendMode.ExtendModeLeft; 51 | break; 52 | 53 | case ExtendMode.ExtendModeRight: 54 | if (Selection!.End == 0) return; 55 | Selection!.End -= 1; 56 | if (Selection.Start == Selection.End) 57 | SelectionExtendMode = ExtendMode.ExtendModeLeft; 58 | break; 59 | } 60 | 61 | ScrollInDirection!(-1); 62 | } 63 | 64 | internal void ExtendSelectionDown() 65 | { 66 | Selection.BiasForwardEnd = true; 67 | 68 | switch (SelectionExtendMode) 69 | { 70 | case ExtendMode.ExtendModeNone: 71 | case ExtendMode.ExtendModeRight: 72 | 73 | SelectionExtendMode = ExtendMode.ExtendModeRight; 74 | 75 | if (Selection!.EndParagraph == Blocks[^1] && Selection.End == Text.Length) 76 | return; // last line of document 77 | 78 | Paragraph origEndPar = Selection.EndParagraph; 79 | 80 | int nextEnd = Selection.EndParagraph.StartInDoc + Selection.EndParagraph.CharNextLineEnd; 81 | 82 | if (Selection.EndParagraph.IsEndAtLastLine) 83 | { 84 | if (Selection.EndParagraph != Blocks[^1]) 85 | { 86 | int nextParIndex = Blocks.IndexOf(Selection.EndParagraph) + 1; 87 | Paragraph nextPar = (Paragraph)Blocks[nextParIndex]; 88 | int oldSE = Selection.End; 89 | Selection.End = Math.Min(nextPar.StartInDoc + nextPar.BlockLength - 1, nextEnd); 90 | } 91 | } 92 | else 93 | Selection.End = nextEnd; 94 | 95 | 96 | //for selection continuity 97 | if (Selection.EndParagraph != origEndPar) 98 | { 99 | origEndPar.SelectionEndInBlock = origEndPar.Text.Length; 100 | Selection.EndParagraph.SelectionStartInBlock = 0; 101 | } 102 | 103 | break; 104 | 105 | case ExtendMode.ExtendModeLeft: 106 | 107 | if (Selection!.StartParagraph == Blocks[^1] && Selection.StartParagraph.IsStartAtLastLine) 108 | return; // last line of document 109 | 110 | int newStart = Selection.StartParagraph.StartInDoc + Selection.StartParagraph.CharNextLineStart; 111 | 112 | if (Blocks.IndexOf(Selection!.StartParagraph) < Blocks.Count - 1) 113 | { 114 | Block nextBlock = Blocks[Blocks.IndexOf(Selection.StartParagraph) + 1]; 115 | int charsFromStart = Selection.StartParagraph.SelectionStartInBlock - Selection.StartParagraph.FirstIndexLastLine; 116 | if (Selection.StartParagraph.IsStartAtLastLine) 117 | { 118 | newStart = nextBlock.StartInDoc + charsFromStart; 119 | Selection.StartParagraph.CollapseToStart(); 120 | } 121 | 122 | } 123 | 124 | if (newStart > Selection.End) 125 | { 126 | int oldEnd = Selection.End; 127 | Selection.End = newStart; 128 | Selection.Start = oldEnd; 129 | SelectionExtendMode = ExtendMode.ExtendModeRight; 130 | } 131 | else 132 | Selection!.Start = newStart; 133 | 134 | break; 135 | } 136 | 137 | ScrollInDirection!(1); 138 | 139 | } 140 | 141 | internal void ExtendSelectionUp() 142 | { 143 | Paragraph? prevPar = null; 144 | Selection.BiasForwardEnd = false; 145 | 146 | switch (SelectionExtendMode) 147 | { 148 | case ExtendMode.ExtendModeNone: 149 | case ExtendMode.ExtendModeLeft: 150 | 151 | if (Selection!.StartParagraph == Blocks[0] && Selection.StartParagraph.IsStartAtFirstLine) 152 | return; // first line of document 153 | 154 | Paragraph origStartPar = Selection.StartParagraph; 155 | if (Selection.StartParagraph.IsStartAtFirstLine) 156 | { 157 | prevPar = (Paragraph)Blocks[Blocks.IndexOf(Selection.StartParagraph) - 1]; 158 | Selection.Start = Math.Min(prevPar.StartInDoc + prevPar.BlockLength - 2, prevPar.StartInDoc + prevPar.FirstIndexLastLine + Selection.StartParagraph.CharPrevLineStart); 159 | } 160 | else 161 | Selection.Start = Selection.StartParagraph.StartInDoc + Selection.StartParagraph.CharPrevLineStart; 162 | 163 | //for selection continuity 164 | SelectionExtendMode = ExtendMode.ExtendModeLeft; 165 | if (Selection.StartParagraph != origStartPar) 166 | { 167 | origStartPar.SelectionStartInBlock = 0; 168 | Selection.StartParagraph.SelectionEndInBlock = Selection.StartParagraph.Text.Length; 169 | } 170 | 171 | break; 172 | 173 | 174 | case ExtendMode.ExtendModeRight: 175 | 176 | int newEnd = Selection.EndParagraph.StartInDoc + Selection.EndParagraph.CharPrevLineEnd; 177 | 178 | if (Blocks.IndexOf(Selection!.EndParagraph) > 0) 179 | { 180 | prevPar = (Paragraph)Blocks[Blocks.IndexOf(Selection.EndParagraph) - 1]; 181 | int charsFromStart = Selection.EndParagraph.SelectionEndInBlock; 182 | if (Selection.EndParagraph.IsEndAtFirstLine) 183 | { 184 | newEnd = prevPar.StartInDoc + prevPar.FirstIndexLastLine + charsFromStart; 185 | Selection.EndParagraph.CollapseToStart(); 186 | } 187 | 188 | } 189 | 190 | if (newEnd < Selection.Start) 191 | { 192 | int oldStart = Selection.Start; 193 | Selection.Start = newEnd; 194 | Selection.End = oldStart; 195 | SelectionExtendMode = ExtendMode.ExtendModeLeft; 196 | } 197 | else 198 | Selection!.End = newEnd; 199 | 200 | break; 201 | } 202 | 203 | ScrollInDirection!(-1); 204 | 205 | 206 | } 207 | 208 | internal void EnsureSelectionContinuity() 209 | { 210 | 211 | foreach (Paragraph p in Blocks.Where(p => !SelectionParagraphs.Contains(p))) 212 | p.ClearSelection(); 213 | 214 | if (SelectionParagraphs.Count > 1) 215 | { 216 | SelectionParagraphs[0].SelectionEndInBlock = SelectionParagraphs[0].BlockLength; 217 | 218 | for (int i = Blocks.IndexOf(SelectionParagraphs[0]) + 1; i < Blocks.IndexOf(SelectionParagraphs[^1]); i++) 219 | { 220 | Blocks[i].SelectionStartInBlock = 0; 221 | Blocks[i].SelectionEndInBlock = Blocks[i].BlockLength; 222 | } 223 | 224 | SelectionParagraphs[^1].SelectionStartInBlock = 0; 225 | } 226 | 227 | 228 | ////Temp for debugging 229 | //foreach (Paragraph p in Blocks.Where(p => !SelectionParagraphs.Contains(p))) 230 | // p.Background = new SolidColorBrush(Colors.Transparent); 231 | //foreach (Paragraph p in Blocks.Where(p => SelectionParagraphs.Contains(p))) 232 | // p.Background = new SolidColorBrush(Colors.Red); 233 | 234 | } 235 | 236 | } 237 | 238 | 239 | -------------------------------------------------------------------------------- /AvRichTextBox/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Simplecto] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /AvRichTextBox/README.md: -------------------------------------------------------------------------------- 1 | # A RichTextBox control for Avalonia 2 | [![NuGet version](https://img.shields.io/nuget/v/Simplecto.Avalonia.RichTextBox.svg?cachebuster=1)](https://www.nuget.org/packages/Simplecto.Avalonia.RichTextBox/) 3 | 4 | As of 2024, Avalonia doesn't yet come with a RichTextBox, and since I needed one I created a "poor-man's version" based on the existing control `SelectableTextBlock`. 5 | 6 | Mirroring WPF, this `RichTextBox` control uses the concept of a `FlowDocument` (`FlowDoc`), which contains `Blocks` (at the current time, only `Paragraph` is available, although `Section` or `Table` could be added later). 7 | `Paragraph` contains `IEditable` objects (`EditableRun` (from `Avalonia.Run`) and `EditableInlineUIContainer` (from `Avalonia.InlineUIContainer`)) and it is bound to an `EditableParagraph` (inheriting from `SelectableTextBlock`). 8 | 9 | The `FlowDoc` is at heart merely an `ObservableCollection` of Blocks bound as the `ItemsSource` of an `ItemsControl` inside a `ScrollViewer`. Upon adding the appropriate key input handling, the control functions like a `RichTextBox`. 10 | 11 | (The hard part after that was implementing the selection logic, because `Selection` for the `RichTextBox` has to be able to move between and span multiple Paragraphs (SelectableTextBlocks), both with the keyboard and the mouse, and to allow editing functions that involve splitting or merging Paragraphs. And of course the Inline logic for spanning, inserting, splitting or deleting Inlines.) 12 | 13 | ```mermaid 14 | classDiagram 15 | class RichTextBox{ 16 | +FlowDocument FlowDoc 17 | } 18 | class FlowDocument{ 19 | +ObservableCollection 20 | } 21 | class FlowDoc{ 22 | -List TextRanges 23 | } 24 | class Blocks{ 25 | +Paragraph Paragraph 26 | } 27 | class Paragraph{ 28 | +IEditable Objects 29 | +EditableParagraph EditableParagraph 30 | } 31 | class IEditable{ 32 | +EditableRun EditableRun 33 | +EditableInlineUIContainer EditableInlineUIContainer 34 | } 35 | class Selection{ 36 | } 37 | class TextRange{ 38 | +int Start 39 | +int End 40 | +int Length 41 | +Delete() 42 | +string Text 43 | +ApplyFormatting(AvaloniaProperty, object) 44 | } 45 | 46 | RichTextBox --> FlowDocument : has 47 | FlowDocument --> Blocks : has 48 | FlowDoc --> TextRange : has 49 | Blocks --> Paragraph : contains 50 | Paragraph --> IEditable : has 51 | TextRange --> Selection : instance 52 | RichTextBox --> FlowDoc : has 53 | 54 | ``` 55 | 56 | **A Debugging panel can be displayed by setting "ShowDebuggerPanelInDebugMode" to True. The panel displays Inline debugging information - Inline starts, paragraph starts, inline texts, and indicates the inlines of the Selection start and end by background color coding. The Debugger panel is not shown in Release mode** 57 | 58 | The RichTextBox has the usual key functions: 59 | * Ctrl+B for **bold**/unbold 60 | * Ctrl-I for *italic*/unitalic 61 | * Ctrl-U for underline/remove underline 62 | * Ctrl-Z for undo 63 | * Ctrl-A for select all 64 | 65 | The `FlowDoc` has a `Selection` property, with `Start`, `End`, `Length`, `Select`, `Delete`, `Text`, etc. 66 | 67 | The `RichTextBox` also includes the concept of `TextRange` (of which `Selection` is merely a special case), which can be defined to format text from code independent from the current `FlowDoc.Selection`. A new `TextRange` is created with a `Start` and `End` (and its owning `FlowDoc`), whereby it is automatically added to the `FlowDoc's TextRanges List` so its Start and/or End can be updated whenever text changes in the `FlowDoc` require it. `TextRange` also has an `ApplyFormatting` property which allows any `AvaloniaProperty` to be applied that pertains to Inlines. 68 | 69 | The RichTextBox content can be saved/loaded either as straight Xaml or a XamlPackage (to preserve images), similar to the WPF RichTextBox. 70 | It can also save and load the FlowDoc content as a Word document (.docx), Rtf document (.rtf) or Html (.html), though only with a subset of attributes. This includes text, common text/paragraph formatting, images, highlighting, forecolor, justification, borders, etc. 71 | 72 | 73 | ## Various future to-do improvements include: 74 | * Word/Html/RTF export and import can be fleshed out (to support more attributes) 75 | * Save/Load Xaml, Rtf functionality (to/from a stream) for TextRanges 76 | * Adding Table support 77 | * Allow the Undo limit to be set, and create a Redo stack 78 | * Stress testing 79 | 80 | RtfDomParser used for parsing of rtf files can be found at https://github.com/SourceCodeBackup/RtfDomParser, but for this project I had to manually modify it to use Avalonia.Media instead of System.Drawing. Generation of .rtf is my own concoction with the bare minimum to produce a readable .rtf file/dataobject. 81 | 82 | **Added 2025/02/22: 83 | Internal binding was of the RTB itself to its viewmodel, which prevented external binding to UserControl properties (such as IsVisible). Internal binding is now to the immediate child (DockPanel "MainDP"), freeing up the properties of the UserControl itself. 84 | Also upgraded copy/paste to allow copying and pasting of paragraph breaks (\r), which were ignored before. 85 | 86 | **Added 2025/02/25** 87 | ver 1.0.16 now works with Avalonia 11.1.xx & 11.2.xx! Binding update issues resolved. Previous AvRichTextBox versions failed on Avalonia 11.1 and higher and have been deprecated. 88 | In addition, added IME support for Chinese/Japanese input. Kanji and Hanzi can now be directly inputted in the RichTextBox. 89 | 90 | **Added 2025/02/26** 91 | ver 1.0.17 improves IME popup location and behavior (Hides on Esc key, or after backspacing to null entry). 92 | In addition, the RichTextBox content can now be saved as .rtf (SaveRtfDoc(string fileName)). As of now, not all attributes are honored in the save (only bold, fontsize, italic and underline). 93 | 94 | **Added 2025/02/27** 95 | ver 1.2.0 - Copying of richtext content (rtf format) is now possible. Some navigation and pasting fixes/improvements. Also pasting of large-volume text is now much faster. Technically 1.0.16 should have been numbered 1.2.0 but hey. 96 | 97 | **Added 2025/02/28** 98 | ver. 1.2.1 - FontFamily now included in rtf copy/paste, fixed Word reading error due to fonts 99 | 100 | **Added 2025/04/05** 101 | ver. 1.2.6 - some minor fixes: run break errors, and better handling of Word colors. Also setting ShowDebuggerPanelInDebugMode at runtime will now dynamically show/hide the Debugger panel. 102 | 103 | **Added 2025/04/06** 104 | ver. 1.3.0 - Can save/load as Html. Rtf images/line spacing now saved. Paragraph borders, colors and backgrounds supported. 105 | 106 | **Added 2025/04/09** 107 | ver. 1.3.2 - Changed the underlying strategy for Undo. Undo now creates clones instead of retaining objects, which was causing problems during complex Undo sequences. 108 | Also made ShowDebuggerPanelInDebugMode default to False. 109 | -------------------------------------------------------------------------------- /AvRichTextBox/RequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using System; 4 | using System.Diagnostics; 5 | 6 | namespace AvRichTextBox; 7 | 8 | public static class RequestExtensions 9 | { 10 | //public static readonly AttachedProperty FlowDocumentProperty = AvaloniaProperty.RegisterAttached("FlowDocument", typeof(RequestExtensions)); 11 | //public static void SetFlowDocument(AvaloniaObject element, FlowDocument value) => element.SetValue(FlowDocumentProperty, value); 12 | //public static FlowDocument GetFlowDocument(AvaloniaObject element) => (FlowDocument)element.GetValue(FlowDocumentProperty); 13 | 14 | 15 | public static readonly AttachedProperty TextBoxFocusRequestedProperty = AvaloniaProperty.RegisterAttached("TextBoxFocusRequested", typeof(RequestExtensions)); 16 | public static void SetTextBoxFocusRequested(AvaloniaObject element, bool value) => element.SetValue(TextBoxFocusRequestedProperty, value); 17 | public static bool GetTextBoxFocusRequested(AvaloniaObject element) => (bool)element.GetValue(TextBoxFocusRequestedProperty); 18 | 19 | public static readonly AttachedProperty IsInlineUpdateRequestedProperty = AvaloniaProperty.RegisterAttached("IsInlineUpdateRequested", typeof(RequestExtensions)); 20 | public static void SetIsInlineUpdateRequested(AvaloniaObject element, bool value) => element.SetValue(IsInlineUpdateRequestedProperty, value); 21 | public static bool GetIsInlineUpdateRequested(AvaloniaObject element) => (bool)element.GetValue(IsInlineUpdateRequestedProperty); 22 | 23 | public static readonly AttachedProperty InvalidateVisualRequestedProperty = AvaloniaProperty.RegisterAttached("InvalidateVisualRequested", typeof(RequestExtensions)); 24 | public static void SetInvalidateVisualRequested(AvaloniaObject element, bool value) => element.SetValue(InvalidateVisualRequestedProperty, value); 25 | public static bool GetInvalidateVisualRequested(AvaloniaObject element) => (bool)element.GetValue(InvalidateVisualRequestedProperty); 26 | 27 | static RequestExtensions() 28 | { 29 | TextBoxFocusRequestedProperty.Changed.Subscribe(args => 30 | { 31 | if (args.Sender is EditableParagraph edPar && (bool)args.NewValue.Value) 32 | { 33 | edPar.Focus(); 34 | edPar.SetValue(TextBoxFocusRequestedProperty, false); 35 | } 36 | }); 37 | 38 | IsInlineUpdateRequestedProperty.Changed.Subscribe(args => 39 | { 40 | if (args.Sender is EditableParagraph edPar && (bool)args.NewValue.Value) 41 | { 42 | edPar.UpdateInlines(); 43 | edPar.SetValue(IsInlineUpdateRequestedProperty, false); 44 | } 45 | }); 46 | 47 | InvalidateVisualRequestedProperty.Changed.Subscribe(args => 48 | { 49 | if (args.Sender is EditableParagraph edPar && (bool)args.NewValue.Value) 50 | { 51 | edPar.UpdateLayout(); 52 | edPar.InvalidateVisual(); 53 | edPar.SetValue(InvalidateVisualRequestedProperty, false); 54 | } 55 | }); 56 | 57 | 58 | 59 | 60 | 61 | } 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 104 | 105 | 106 | 107 | 129 | 130 | 131 | 132 | 133 | 134 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBoxInputClient.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using System; 4 | using System.Diagnostics; 5 | using Avalonia.Input.TextInput; 6 | using Avalonia.Interactivity; 7 | using Avalonia.Media; 8 | using Avalonia.Platform; 9 | 10 | namespace AvRichTextBox; 11 | 12 | public partial class RichTextBox 13 | { 14 | 15 | public class RichTextBoxTextInputClient : TextInputMethodClient 16 | { 17 | private readonly RichTextBox _owner; 18 | 19 | public RichTextBoxTextInputClient(RichTextBox owner) 20 | { 21 | _owner = owner; 22 | //this.SelectionChanged += RichTextBoxTextInputClient_SelectionChanged; 23 | //this.TextViewVisualChanged += RichTextBoxTextInputClient_TextViewVisualChanged; 24 | //this.ShowInputPanel(); 25 | 26 | } 27 | 28 | private void RichTextBoxTextInputClient_TextViewVisualChanged(object? sender, EventArgs e) 29 | { 30 | //Debug.WriteLine("visual changed"); 31 | } 32 | 33 | private void RichTextBoxTextInputClient_SelectionChanged(object? sender, EventArgs e) 34 | { 35 | //Debug.WriteLine("selection changed"); 36 | } 37 | 38 | private double GetAdjustedCursorY (double yval) 39 | { 40 | //Debug.WriteLine("ownerbounds bottom=" + _owner.Bounds.Bottom + " // yval= " + yval); 41 | return (yval > _owner.Bounds.Bottom - 200) ? yval - 200 : yval + 22; 42 | } 43 | 44 | public override Rect CursorRectangle => new(_owner.CursorPosition.X + 12, GetAdjustedCursorY(_owner.CursorPosition.Y), 1, 0); 45 | 46 | public void UpdateCursorPosition() 47 | { 48 | RaiseCursorRectangleChanged(); 49 | } 50 | 51 | public override bool SupportsPreedit => true; 52 | 53 | public override bool SupportsSurroundingText => false; 54 | public override string SurroundingText => ""; 55 | 56 | 57 | public override TextSelection Selection 58 | { 59 | get => new (_owner.FlowDoc.Selection.Start, _owner.FlowDoc.Selection.End); 60 | set { } 61 | } 62 | 63 | public override Visual TextViewVisual => null!; 64 | 65 | public override void SetPreeditText(string? preeditText) 66 | { 67 | _owner.InsertPreeditText(preeditText!); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox_DocEditing.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Documents; 3 | using Avalonia.Media; 4 | using System.Diagnostics; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace AvRichTextBox; 8 | 9 | public partial class RichTextBox 10 | { 11 | 12 | private void RichTextBox_TextInput(object? sender, Avalonia.Input.TextInputEventArgs e) 13 | { 14 | FlowDoc.InsertText(e.Text); 15 | UpdateCurrentParagraphLayout(); 16 | 17 | if (PreeditOverlay.IsVisible) 18 | HideIMEOverlay(); 19 | 20 | } 21 | 22 | private void HideIMEOverlay() 23 | { 24 | _preeditText = ""; 25 | PreeditOverlay.IsVisible = false; 26 | 27 | } 28 | 29 | internal void UpdateCurrentParagraphLayout() 30 | { 31 | this.UpdateLayout(); 32 | rtbVM.UpdateCursorVisible(); 33 | } 34 | 35 | internal void InsertParagraph() 36 | { 37 | FlowDoc.InsertParagraph(true, FlowDoc.Selection.Start); 38 | UpdateCurrentParagraphLayout(); 39 | 40 | } 41 | 42 | internal void InsertLineBreak() 43 | { 44 | FlowDoc.InsertLineBreak(); 45 | UpdateCurrentParagraphLayout(); 46 | 47 | } 48 | 49 | public void SearchText(string searchText) 50 | { 51 | MatchCollection matches = Regex.Matches(FlowDoc.Text, searchText); 52 | 53 | if (matches.Count > 0) 54 | FlowDoc.Select(matches[0].Index, matches[0].Length); 55 | 56 | 57 | foreach (Match m in matches) 58 | { 59 | TextRange trange = new (FlowDoc, m.Index, m.Index + m.Length); 60 | FlowDoc.ApplyFormattingRange(Inline.FontStretchProperty, FontStretch.UltraCondensed, trange); 61 | FlowDoc.ApplyFormattingRange(Inline.ForegroundProperty, new SolidColorBrush(Colors.BlueViolet), trange); 62 | FlowDoc.ApplyFormattingRange(Inline.BackgroundProperty, new SolidColorBrush(Colors.Wheat), trange); 63 | } 64 | 65 | 66 | 67 | } 68 | 69 | 70 | private void PerformDelete(bool backspace) 71 | { 72 | 73 | if (FlowDoc.Selection!.Length > 0) 74 | FlowDoc.DeleteSelection(); 75 | else 76 | { 77 | if (backspace) 78 | if (FlowDoc.Selection.Start == 0) return; 79 | else 80 | if (FlowDoc.Selection.Start >= FlowDoc.Selection.StartParagraph.StartInDoc + FlowDoc.Selection.StartParagraph.BlockLength) 81 | return; 82 | 83 | FlowDoc.DeleteChar(backspace); 84 | } 85 | 86 | UpdateCurrentParagraphLayout(); 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox_KeyDown.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Input; 2 | using System.Diagnostics; 3 | 4 | 5 | namespace AvRichTextBox; 6 | 7 | public partial class RichTextBox 8 | { 9 | 10 | private void RichTextBox_KeyDown(object? sender, KeyEventArgs e) 11 | { 12 | 13 | if (e.KeyModifiers.HasFlag(KeyModifiers.Control)) 14 | { 15 | e.Handled = true; 16 | 17 | switch (e.Key) 18 | { 19 | case Key.I: 20 | ToggleItalics(); 21 | break; 22 | 23 | case Key.B: 24 | ToggleBold(); 25 | break; 26 | 27 | case Key.U: 28 | ToggleUnderlining(); 29 | break; 30 | 31 | case Key.C: 32 | CopyToClipboard(); 33 | break; 34 | 35 | case Key.V: 36 | PasteFromClipboard(); 37 | break; 38 | 39 | case Key.Home: // Ctrl-Home 40 | FlowDoc.MoveToDocStart(); 41 | FlowDocSV.ScrollToHome(); 42 | break; 43 | 44 | case Key.End: // Ctrl-End 45 | FlowDoc.MoveToDocEnd(); 46 | break; 47 | 48 | case Key.Z: 49 | FlowDoc.Undo(); 50 | break; 51 | 52 | case Key.A: 53 | FlowDoc.SelectAll(); 54 | break; 55 | 56 | case Key.Delete: 57 | FlowDoc.DeleteWord(false); 58 | break; 59 | 60 | case Key.Back: 61 | FlowDoc.DeleteWord(true); 62 | break; 63 | 64 | case Key.Right: 65 | FlowDoc.MoveRightWord(); 66 | break; 67 | 68 | case Key.Left: 69 | FlowDoc.MoveLeftWord(); 70 | break; 71 | } 72 | } 73 | else 74 | { 75 | 76 | switch (e.Key) 77 | { 78 | case Key.Escape: 79 | 80 | if (PreeditOverlay.IsVisible) 81 | HideIMEOverlay(); 82 | 83 | break; 84 | 85 | case Key.Enter: 86 | if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) 87 | InsertLineBreak(); 88 | else 89 | InsertParagraph(); 90 | break; 91 | case Key.Home: 92 | FlowDoc.MoveToStartOfLine(e.KeyModifiers.HasFlag(KeyModifiers.Shift)); 93 | break; 94 | 95 | case Key.End: 96 | FlowDoc.MoveToEndOfLine(e.KeyModifiers.HasFlag(KeyModifiers.Shift)); 97 | break; 98 | 99 | case Key.Right: 100 | if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) 101 | FlowDoc.ExtendSelectionRight(); 102 | else 103 | FlowDoc.MoveSelectionRight(false); 104 | FlowDoc.ResetInsertFormatting(); 105 | break; 106 | 107 | case Key.Left: 108 | if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) 109 | FlowDoc.ExtendSelectionLeft(); 110 | else 111 | FlowDoc.MoveSelectionLeft(false); 112 | FlowDoc.ResetInsertFormatting(); 113 | break; 114 | 115 | case Key.Up: 116 | if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) 117 | FlowDoc.ExtendSelectionUp(); 118 | else 119 | FlowDoc.MoveSelectionUp(false); 120 | break; 121 | 122 | case Key.Down: 123 | if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) 124 | FlowDoc.ExtendSelectionDown(); 125 | else 126 | FlowDoc.MoveSelectionDown(true); 127 | break; 128 | 129 | case Key.Back: 130 | PerformDelete(true); 131 | break; 132 | 133 | case Key.Delete: 134 | PerformDelete(false); 135 | break; 136 | 137 | case Key.PageDown: 138 | MovePage(1, e.KeyModifiers.HasFlag(KeyModifiers.Shift)); 139 | break; 140 | 141 | case Key.PageUp: 142 | 143 | MovePage(-1, e.KeyModifiers.HasFlag(KeyModifiers.Shift)); 144 | 145 | break; 146 | 147 | } 148 | 149 | rtbVM.CursorVisible = (rtbVM.FlowDoc.Selection.Length == 0); 150 | if (client != null) 151 | UpdatePreeditOverlay(); 152 | 153 | } 154 | 155 | 156 | } 157 | 158 | 159 | 160 | } 161 | 162 | 163 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox_PointerEvents.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Data.Core; 4 | using Avalonia.Input; 5 | using Avalonia.Media; 6 | using Avalonia.VisualTree; 7 | using DynamicData; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Linq; 11 | 12 | namespace AvRichTextBox; 13 | 14 | public partial class RichTextBox 15 | { 16 | 17 | EditableParagraph? currentMouseOverEP = null; 18 | 19 | internal void EditableParagraph_MouseMove(EditableParagraph edPar, int charIndex) 20 | { 21 | if (!PointerDownOverRTB) 22 | currentMouseOverEP = edPar; 23 | 24 | } 25 | 26 | 27 | private void EditableParagraph_LostFocus(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 28 | { 29 | this.Focus(); 30 | } 31 | 32 | 33 | internal int SelectionOrigin = 0; 34 | bool PointerDownOverRTB = false; 35 | 36 | private void FlowDocSV_PointerPressed(object? sender, PointerPressedEventArgs e) 37 | { 38 | if (currentMouseOverEP == null) return; 39 | 40 | PointerDownOverRTB = true; 41 | 42 | TextHitTestResult hitCarIndex = currentMouseOverEP.TextLayout.HitTestPoint(e.GetPosition(currentMouseOverEP)); 43 | Paragraph thisPar = (Paragraph)currentMouseOverEP.DataContext!; 44 | if (thisPar == null) return; 45 | SelectionOrigin = thisPar.StartInDoc + hitCarIndex.TextPosition; 46 | 47 | //Clear all selections in all paragraphs 48 | foreach (Paragraph p in FlowDoc.Blocks.Where(pp => pp.SelectionLength != 0)) { p.ClearSelection(); } 49 | 50 | FlowDoc.Selection.Start = SelectionOrigin; 51 | FlowDoc.Selection.CollapseToStart(); 52 | 53 | //e.Pointer.Capture(null); 54 | //e.Pointer.Capture(this); 55 | 56 | } 57 | 58 | private void FlowDocSV_PointerMoved(object? sender, PointerEventArgs e) 59 | { 60 | 61 | if (PointerDownOverRTB) 62 | { 63 | EditableParagraph overEP = null!; 64 | 65 | double RTBTransformedY = this.GetTransformedBounds()!.Value.Clip.Y; 66 | 67 | foreach (KeyValuePair kvp in VisualHelper.GetVisibleEditableParagraphs(FlowDocSV)) 68 | { //Debug.WriteLine("visiPar = " + kvp.Key.Text); 69 | 70 | Point ePoint = e.GetCurrentPoint(FlowDocSV).Position; 71 | Rect thisEPRect = new(kvp.Value.X - DocIC.Margin.Left, kvp.Value.Y, kvp.Value.Width, kvp.Value.Height); 72 | 73 | double adjustedMouseY = ePoint.Y + RTBTransformedY; 74 | bool epContainsPoint = thisEPRect.Top <= adjustedMouseY && thisEPRect.Bottom >= adjustedMouseY; 75 | 76 | if (epContainsPoint) 77 | { overEP = kvp.Key; break; } 78 | } 79 | 80 | if (overEP != null) 81 | { 82 | TextHitTestResult hitCharIndex = overEP.TextLayout.HitTestPoint(e.GetPosition(overEP)); 83 | int charIndex = hitCharIndex.TextPosition; 84 | 85 | Paragraph thisPar = (Paragraph)overEP.DataContext!; 86 | 87 | if (thisPar.StartInDoc + charIndex < SelectionOrigin) 88 | { //Debug.WriteLine("startindoc = " + thisPar.StartInDoc + " :::charindex = " + charIndex + " :::selectionorigin= " + SelectionOrigin); 89 | FlowDoc.SelectionExtendMode = FlowDocument.ExtendMode.ExtendModeLeft; 90 | FlowDoc.Selection.End = SelectionOrigin; 91 | FlowDoc.Selection.Start = thisPar.StartInDoc + charIndex; 92 | } 93 | else 94 | { 95 | FlowDoc.SelectionExtendMode = FlowDocument.ExtendMode.ExtendModeRight; 96 | FlowDoc.Selection.Start = SelectionOrigin; 97 | FlowDoc.Selection.End = thisPar.StartInDoc + charIndex; 98 | } 99 | 100 | FlowDoc.EnsureSelectionContinuity(); 101 | } 102 | } 103 | 104 | } 105 | 106 | private void RichTextBox_PointerReleased(object? sender, PointerReleasedEventArgs e) 107 | { 108 | PointerDownOverRTB = false; 109 | 110 | } 111 | 112 | private void FlowDocSV_PointerReleased(object? sender, PointerReleasedEventArgs e) 113 | { 114 | //e.Pointer.Capture(null); 115 | PointerDownOverRTB = false; 116 | 117 | } 118 | 119 | private void RichTextBox_PointerExited(object? sender, PointerEventArgs e) 120 | { 121 | //PointerDownOverRTB = false; 122 | 123 | } 124 | 125 | } 126 | 127 | 128 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox_RunFormatting.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using DocumentFormat.OpenXml.VariantTypes; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using RtfDomParserAv; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using static AvRichTextBox.FlowDocument; 11 | 12 | namespace AvRichTextBox; 13 | 14 | public partial class RichTextBox 15 | { 16 | 17 | private void ToggleItalics() 18 | { 19 | FlowDoc.ToggleItalic(); 20 | 21 | } 22 | 23 | private void ToggleBold() 24 | { 25 | FlowDoc.ToggleBold(); 26 | 27 | } 28 | 29 | private void ToggleUnderlining() 30 | { 31 | FlowDoc.ToggleUnderlining(); 32 | 33 | } 34 | 35 | private void CopyToClipboard() 36 | { 37 | 38 | var dataObject = new DataObject(); 39 | 40 | //create rtf string 41 | List newInlines = FlowDoc.GetRangeInlines(FlowDoc.Selection); 42 | string rtfString = RtfConversions.GetRtfFromInlines(newInlines); 43 | byte[] rtfbytes = System.Text.Encoding.Default.GetBytes(rtfString); 44 | 45 | dataObject.Set("Rich Text Format", rtfbytes); 46 | dataObject.Set("Text", FlowDoc.Selection.GetText()); 47 | 48 | TopLevel.GetTopLevel(this)!.Clipboard!.SetDataObjectAsync(dataObject); 49 | 50 | } 51 | 52 | 53 | private async void PasteFromClipboard() 54 | { 55 | bool TextPasted = false; 56 | int originalSelectionStart = FlowDoc.Selection.Start; 57 | int newSelPoint = originalSelectionStart; 58 | 59 | string[] formats = await TopLevel.GetTopLevel(this)!.Clipboard!.GetFormatsAsync(); 60 | if (formats.Contains ("Rich Text Format")) 61 | { 62 | object? rtfobj = await TopLevel.GetTopLevel(this)!.Clipboard!.GetDataAsync("Rich Text Format"); 63 | if (rtfobj != null) 64 | { 65 | byte[] rtfbytes = (byte[])rtfobj; 66 | string rtfstring = System.Text.Encoding.Default.GetString(rtfbytes!); 67 | 68 | RTFDomDocument dom = new(); 69 | dom.LoadRTFText(rtfstring); 70 | List insertInlines = RtfConversions.GetInlinesFromRtf(dom); 71 | insertInlines.Reverse(); 72 | int addedchars = FlowDoc.SetRangeToInlines(FlowDoc.Selection, insertInlines); 73 | 74 | newSelPoint = Math.Min(newSelPoint + addedchars, FlowDoc.DocEndPoint - 1); 75 | 76 | TextPasted = true; 77 | } 78 | } 79 | else if (formats.Contains("Text")) 80 | { 81 | object? textobj = await TopLevel.GetTopLevel(this)!.Clipboard!.GetDataAsync("Text"); 82 | 83 | if (textobj != null) 84 | { 85 | string pasteText = textobj.ToString()!; 86 | FlowDoc.SetRangeToText(FlowDoc.Selection, pasteText); 87 | 88 | newSelPoint = Math.Min(newSelPoint + pasteText.Length, FlowDoc.DocEndPoint - 1); 89 | 90 | TextPasted = true; 91 | } 92 | } 93 | 94 | if (TextPasted) 95 | { 96 | this.DocIC.UpdateLayout(); 97 | await Task.Delay(100); //necessary for following operations 98 | 99 | FlowDoc.Selection.EndParagraph.CallRequestInlinesUpdate(); // important 100 | FlowDoc.Selection.EndParagraph.UpdateEditableRunPositions(); 101 | 102 | FlowDoc.Select(newSelPoint, 0); 103 | FlowDoc.UpdateSelection(); 104 | 105 | FlowDoc.Selection.BiasForwardStart = false; 106 | FlowDoc.Selection.BiasForwardEnd = false; 107 | FlowDoc.SelectionExtendMode = ExtendMode.ExtendModeNone; 108 | 109 | CreateClient(); 110 | 111 | 112 | } 113 | 114 | } 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBox/RichTextBox_SelectionRect.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Media; 4 | using Avalonia.Media.TextFormatting; 5 | using System; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace AvRichTextBox; 11 | 12 | public partial class RichTextBox : UserControl 13 | { 14 | 15 | internal void SelectionStart_RectChanged(EditableParagraph edPar) 16 | { 17 | edPar.UpdateLayout(); 18 | 19 | TextLayout tlayout = edPar.TextLayout; 20 | 21 | Rect selStartRect = tlayout.HitTestTextPosition(edPar.SelectionStart); 22 | Rect prevCharRect = tlayout.HitTestTextPosition(edPar.SelectionStart - 1); 23 | 24 | Point? selStartPoint = edPar.TranslatePoint(selStartRect.Position, DocIC); 25 | Point? prevCharPoint = edPar.TranslatePoint(prevCharRect.Position, DocIC); 26 | 27 | if (selStartPoint == null || prevCharPoint == null) return; 28 | 29 | //Debug.WriteLine("selstartpoint= " + selStartPoint!.Value.Y); 30 | 31 | if (selStartPoint != null) 32 | FlowDoc.Selection.StartRect = new Rect((Point)selStartPoint!, selStartRect.Size); 33 | 34 | if (prevCharPoint != null) 35 | FlowDoc.Selection.PrevCharRect = new Rect((Point)prevCharPoint!, prevCharRect.Size); 36 | 37 | Paragraph thisPar = (Paragraph)edPar.DataContext!; 38 | //if (thisPar == null) return; 39 | 40 | thisPar.DistanceSelectionStartFromLeft = selStartRect.Left; 41 | 42 | int lineNo = tlayout.GetLineIndexFromCharacterIndex(edPar.SelectionStart, false); 43 | thisPar.IsStartAtFirstLine = lineNo == 0; 44 | thisPar.IsStartAtLastLine = (lineNo == tlayout.TextLines.Count - 1); 45 | 46 | if (thisPar.IsStartAtFirstLine) 47 | thisPar.CharPrevLineStart = edPar.SelectionStart; 48 | else 49 | { // get index of first char on previous line 50 | thisPar.CharPrevLineStart = GetClosestIndex(edPar, lineNo, thisPar.DistanceSelectionStartFromLeft, -1); 51 | //IEditable? nextInline = FlowDoc.GetNextInline(FlowDoc.Selection.GetStartInline()); 52 | //if (nextInline != null && nextInline.IsLineBreak) 53 | //{ 54 | // Debug.WriteLine("thisinlinetext=" + FlowDoc.Selection.GetStartInline().InlineText); 55 | // Debug.WriteLine("nextinlinetext=" + nextInline.InlineText); 56 | //} 57 | // //thisPar.CharPrevLineStart += 1; 58 | } 59 | 60 | 61 | if (thisPar.IsStartAtLastLine) 62 | thisPar.CharNextLineStart = edPar.SelectionEnd - tlayout.TextLines[lineNo].FirstTextSourceIndex; 63 | else 64 | thisPar.CharNextLineStart = GetClosestIndex(edPar, lineNo, thisPar.DistanceSelectionStartFromLeft, 1); 65 | 66 | thisPar.FirstIndexStartLine = tlayout.TextLines[lineNo].FirstTextSourceIndex; 67 | thisPar.FirstIndexLastLine = tlayout.TextLines[^1].FirstTextSourceIndex; 68 | 69 | 70 | //**********Fix cursor height and position*********: 71 | int lineIndex = tlayout.GetLineIndexFromCharacterIndex(edPar.SelectionStart, false); 72 | rtbVM.CursorHeight = tlayout.TextLines[lineIndex].Extent; 73 | if (rtbVM.CursorHeight == 0) 74 | rtbVM.CursorHeight = tlayout.TextLines[lineIndex].Height; 75 | rtbVM.CursorHeight += 5; // give it an extra bit 76 | 77 | 78 | double cursorML = selStartPoint!.Value.X; 79 | double cursorMT = tlayout.TextLines[lineIndex].Start; 80 | 81 | if (FlowDoc.Selection.IsAtEndOfLineSpace) 82 | { 83 | cursorML = FlowDoc.Selection!.PrevCharRect!.Right; 84 | cursorMT = FlowDoc.Selection!.PrevCharRect.Top + 1; 85 | } 86 | else 87 | cursorMT = selStartPoint.Value.Y; 88 | 89 | rtbVM.CursorMargin = new Thickness(cursorML, cursorMT, 0, 0); 90 | rtbVM.UpdateCursorVisible(); 91 | 92 | } 93 | 94 | internal void SelectionEnd_RectChanged(EditableParagraph edPar) 95 | { 96 | edPar.UpdateLayout(); 97 | 98 | Rect selEndRect = edPar.TextLayout.HitTestTextPosition(edPar.SelectionEnd); 99 | 100 | Point? selEndPoint = edPar.TranslatePoint(selEndRect.Position, DocIC); 101 | if (selEndPoint != null) 102 | FlowDoc.Selection.EndRect = new Rect((Point)selEndPoint!, selEndRect.Size); 103 | 104 | Paragraph thisPar = (Paragraph)edPar.DataContext!; 105 | //if (thisPar == null) return; 106 | 107 | thisPar.DistanceSelectionEndFromLeft = edPar.TextLayout.HitTestTextPosition(edPar.SelectionEnd).Left; 108 | int lineNo = edPar.TextLayout.GetLineIndexFromCharacterIndex(edPar.SelectionEnd, false); 109 | thisPar.IsEndAtLastLine = lineNo == edPar.TextLayout.TextLines.Count - 1; 110 | 111 | thisPar.IsEndAtFirstLine = (lineNo == 0); 112 | if (thisPar.IsEndAtLastLine) 113 | { 114 | thisPar.LastIndexEndLine = thisPar.BlockLength; 115 | thisPar.CharNextLineEnd = edPar.Text!.Length + 1 + edPar.SelectionEnd - edPar.TextLayout.TextLines[lineNo].FirstTextSourceIndex; 116 | } 117 | else 118 | { 119 | //Debug.WriteLine("partext = " + thisPar.Text); 120 | //Debug.WriteLine("partext = " + thisPar.Inlines.Count); 121 | 122 | //IEditable ied = FlowDoc.Selection.GetStartInline(); 123 | //Debug.WriteLine("ied.= " + ied.DisplayInlineText); 124 | 125 | //Debug.WriteLine("count = " + FlowDoc.GetRangeInlines(FlowDoc.Selection).Count); 126 | //if (FlowDoc.GetRangeInlines(FlowDoc.Selection).Count > 0) 127 | // Debug.WriteLine("count = *" + FlowDoc.GetRangeInlines(FlowDoc.Selection)[0].InlineText + "*"); 128 | 129 | TextLine tline = edPar.TextLayout.TextLines[lineNo]; 130 | //Debug.WriteLine("textstring =" + string.Join("\r", (tline.TextRuns.Select(tr=>tr.Text)))); 131 | //Debug.WriteLine("lastis rn? " + (tline.TextRuns.Last().Text.ToString() == "\r\n")); 132 | 133 | int goBackNo = 1; 134 | if (tline.TextRuns.Count > 0) 135 | { 136 | if (tline.TextRuns[tline.TextRuns.Count - 1].Text.ToString() == "\r\n") 137 | goBackNo++; 138 | 139 | } 140 | 141 | thisPar.LastIndexEndLine = edPar.TextLayout.TextLines[lineNo + 1].FirstTextSourceIndex - goBackNo; 142 | thisPar.CharNextLineEnd = GetClosestIndex(edPar, lineNo, thisPar.DistanceSelectionEndFromLeft, 1); 143 | } 144 | 145 | if (!thisPar.IsEndAtFirstLine) 146 | thisPar.CharPrevLineEnd = GetClosestIndex(edPar, lineNo, thisPar.DistanceSelectionEndFromLeft, -1); 147 | 148 | 149 | thisPar.FirstIndexLastLine = edPar.TextLayout.TextLines[^1].FirstTextSourceIndex; 150 | 151 | rtbVM.UpdateCursorVisible(); 152 | 153 | } 154 | 155 | 156 | private static int GetClosestIndex(EditableParagraph edPar, int lineNo, double distanceFromLeft, int direction) 157 | { 158 | CharacterHit chit = edPar.TextLayout.TextLines[lineNo + direction].GetCharacterHitFromDistance(distanceFromLeft); 159 | 160 | double CharDistanceDiffThis = Math.Abs(distanceFromLeft - edPar.TextLayout.HitTestTextPosition(chit.FirstCharacterIndex).Left); 161 | double CharDistanceDiffNext = Math.Abs(distanceFromLeft - edPar.TextLayout.HitTestTextPosition(chit.FirstCharacterIndex + 1).Left); 162 | 163 | if (CharDistanceDiffThis > CharDistanceDiffNext) 164 | return chit.FirstCharacterIndex + 1; 165 | else 166 | return chit.FirstCharacterIndex; 167 | 168 | 169 | } 170 | 171 | private void EditableParagraph_CharIndexRect_Notified(EditableParagraph edPar, Rect selStartRect) 172 | { 173 | //Point? selStartPoint = edPar.TranslatePoint(selStartRect.Position, DocIC); 174 | //if (selStartPoint != null) 175 | // FlowDoc.Selection.StartRect = new Rect((Point)selStartPoint!, selStartRect.Size); 176 | 177 | } 178 | 179 | 180 | } 181 | 182 | 183 | -------------------------------------------------------------------------------- /AvRichTextBox/RichTextBoxViewModel/RichTextBoxViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Threading; 3 | using System; 4 | using System.ComponentModel; 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using static AvRichTextBox.FlowDocument; 8 | 9 | namespace AvRichTextBox; 10 | 11 | public class RichTextBoxViewModel : INotifyPropertyChanged 12 | { 13 | public event PropertyChangedEventHandler? PropertyChanged; 14 | private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 15 | 16 | public delegate void FlowDocChanged_Handler(); 17 | internal event FlowDocChanged_Handler? FlowDocChanged; 18 | 19 | private Vector _RTBScrollOffset = new (0, 0); 20 | public Vector RTBScrollOffset { get => _RTBScrollOffset; set { if (_RTBScrollOffset != value) _RTBScrollOffset = value; NotifyPropertyChanged(nameof(RTBScrollOffset)); } } 21 | 22 | public double MinWidth => RunDebuggerVisible ? 500 : 100; 23 | 24 | private FlowDocument _FlowDoc = null!; 25 | //public FlowDocument FlowDoc { get => _FlowDoc; set { _FlowDoc = value; NotifyPropertyChanged(nameof(FlowDoc)); } } 26 | public FlowDocument FlowDoc { get => _FlowDoc; set { _FlowDoc = value; NotifyPropertyChanged(nameof(FlowDoc)); FlowDocChanged?.Invoke(); } } 27 | 28 | private bool _RunDebuggerVisible = false; 29 | public bool RunDebuggerVisible { get => _RunDebuggerVisible; set { _RunDebuggerVisible = value; NotifyPropertyChanged(nameof(RunDebuggerVisible)); } } 30 | 31 | public RichTextBoxViewModel() 32 | { 33 | //FlowDoc.ScrollInDirection += FlowDoc_ScrollInDirection; 34 | //FlowDoc.UpdateRTBCursor += FlowDoc_UpdateRTBCursor; 35 | } 36 | 37 | internal void FlowDoc_UpdateRTBCursor() 38 | { 39 | UpdateCursorVisible(); 40 | } 41 | 42 | internal double ScrollViewerHeight = 10; 43 | 44 | private double _CursorHeight = 5; 45 | public double CursorHeight { get => _CursorHeight; set { _CursorHeight = value; NotifyPropertyChanged(nameof(CursorHeight)); } } 46 | 47 | private Thickness _CursorMargin = new (0); 48 | public Thickness CursorMargin { get => _CursorMargin; set { _CursorMargin = value; NotifyPropertyChanged(nameof(CursorMargin)); } } 49 | 50 | private bool _CursorVisible = true; 51 | public bool CursorVisible { get => _CursorVisible; set { _CursorVisible = value; NotifyPropertyChanged(nameof(CursorVisible)); } } 52 | 53 | internal void UpdateCursorVisible() 54 | { 55 | 56 | FlowDoc.Selection.StartParagraph?.CallRequestInvalidateVisual(); 57 | 58 | CursorVisible = FlowDoc.Selection.Length == 0; 59 | 60 | } 61 | 62 | 63 | internal void FlowDoc_ScrollInDirection(int direction) 64 | { 65 | 66 | double scrollPadding = 30; 67 | if (direction == 1) 68 | { 69 | double checkPointY = FlowDoc.Selection!.EndRect!.Y; 70 | 71 | if (FlowDoc.SelectionExtendMode == ExtendMode.ExtendModeLeft) 72 | checkPointY = FlowDoc.Selection!.StartRect!.Y; 73 | 74 | if (checkPointY > RTBScrollOffset.Y + ScrollViewerHeight - scrollPadding) 75 | RTBScrollOffset = RTBScrollOffset.WithY(checkPointY - ScrollViewerHeight + scrollPadding); 76 | //RTBScrollOffset = RTBScrollOffset.WithY(checkPointY + scrollPadding); 77 | } 78 | else 79 | { 80 | double checkPointY = FlowDoc.Selection!.StartRect!.Y; 81 | if (FlowDoc.SelectionExtendMode == ExtendMode.ExtendModeRight) 82 | checkPointY = FlowDoc.Selection!.EndRect!.Y; 83 | 84 | 85 | if (checkPointY < RTBScrollOffset.Y) 86 | RTBScrollOffset = RTBScrollOffset.WithY(checkPointY); 87 | } 88 | 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /AvRichTextBox/RtfDomParserAv.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuikp/AvRichTextBox/145e48b7aecf1b896dc4a5fd573061b5bcd449d9/AvRichTextBox/RtfDomParserAv.dll -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/HtmlConversions/ContentToHtml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using Avalonia.Media.Imaging; 3 | using HtmlAgilityPack; 4 | using System.IO; 5 | using System; 6 | using System.Net; 7 | using System.Text; 8 | using Avalonia.Controls; 9 | using System.Linq; 10 | 11 | namespace AvRichTextBox; 12 | 13 | internal static partial class HtmlConversions 14 | { 15 | internal static HtmlDocument GetHtmlFromFlowDocument(FlowDocument fdoc) 16 | { 17 | 18 | HtmlDocument hdoc = new(); 19 | 20 | HtmlNode html = hdoc.CreateElement("html"); 21 | HtmlNode head = hdoc.CreateElement("head"); 22 | HtmlNode body = hdoc.CreateElement("body"); 23 | 24 | html.AppendChild(head); 25 | html.AppendChild(body); 26 | hdoc.DocumentNode.AppendChild(html); 27 | 28 | 29 | if (fdoc.PagePadding != default) 30 | { 31 | var pad = fdoc.PagePadding; 32 | var padStyle = $"padding:{pad.Top}px {pad.Right}px {pad.Bottom}px {pad.Left}px;"; 33 | body.SetAttributeValue("style", padStyle); 34 | } 35 | 36 | foreach (Block b in fdoc.Blocks){ 37 | 38 | if (b is Paragraph p) 39 | { 40 | HtmlNode parnode = hdoc.CreateElement("p"); 41 | //parnode.SetAttributeValue("style", GetParStyle(p)); 42 | 43 | foreach (IEditable ied in p.Inlines) 44 | { 45 | HtmlNode spanNode = hdoc.CreateElement("span"); 46 | 47 | switch (ied) 48 | { 49 | case EditableRun erun: 50 | spanNode.InnerHtml = WebUtility.HtmlEncode(erun.Text ?? ""); 51 | spanNode.SetAttributeValue("style", GetInlineStyle(erun)); 52 | break; 53 | 54 | case EditableLineBreak elbreak: 55 | spanNode = hdoc.CreateElement("br"); 56 | //parnode.AppendChild(br); 57 | break; 58 | 59 | case EditableInlineUIContainer eUIC: 60 | 61 | if (eUIC.Child is Image img && img.Source is Bitmap bmp) 62 | { 63 | using var memStream = new MemoryStream(); 64 | bmp.Save(memStream); 65 | var base64 = Convert.ToBase64String(memStream.ToArray()); 66 | 67 | var imgNode = hdoc.CreateElement("img"); 68 | imgNode.SetAttributeValue("src", $"data:image/png;base64,{base64}"); 69 | imgNode.SetAttributeValue("width", img.Width.ToString()); 70 | imgNode.SetAttributeValue("height", img.Height.ToString()); 71 | 72 | parnode.AppendChild(imgNode); 73 | } 74 | break; 75 | 76 | } 77 | 78 | parnode.AppendChild(spanNode); 79 | 80 | } 81 | 82 | parnode.SetAttributeValue("style", GetParStyle(p)); 83 | 84 | body.AppendChild(parnode); 85 | } 86 | } 87 | 88 | 89 | return hdoc; 90 | 91 | } 92 | 93 | private static string GetParStyle(Paragraph p) 94 | { 95 | var parStyle = new StringBuilder(); 96 | 97 | if (p.LineSpacing > 0) 98 | parStyle.Append($"line-height:{p.LineHeight}px;"); 99 | 100 | switch (p.TextAlignment) 101 | { 102 | case TextAlignment.Center: 103 | parStyle.Append("text-align:center;"); 104 | break; 105 | case TextAlignment.Right: 106 | parStyle.Append("text-align:right;"); 107 | break; 108 | case TextAlignment.Left: 109 | parStyle.Append("text-align:left;"); 110 | break; 111 | case TextAlignment.Justify: 112 | parStyle.Append("text-align:justify;"); 113 | break; 114 | } 115 | 116 | if (p.Margin != default) 117 | parStyle.Append($"margin:{p.Margin.Top}px {p.Margin.Right}px {p.Margin.Bottom}px {p.Margin.Left}px;"); 118 | 119 | string? parBackgroundColor = GetCssColor(p.Background); 120 | if (parBackgroundColor != null) 121 | parStyle.Append($"background-color:{parBackgroundColor};"); 122 | 123 | var borderColor = GetCssColor(p.BorderBrush); 124 | if (borderColor != null) 125 | parStyle.Append($"border-color:{borderColor};"); 126 | 127 | if (p.BorderThickness != default) 128 | parStyle.Append($"border-style:solid;border-width:{p.BorderThickness.Top}px {p.BorderThickness.Right}px {p.BorderThickness.Bottom}px {p.BorderThickness.Left}px;"); 129 | 130 | return parStyle.ToString(); 131 | } 132 | 133 | 134 | private static string GetInlineStyle(EditableRun run) 135 | { 136 | var sb = new StringBuilder(); 137 | 138 | if (!string.IsNullOrEmpty(run.FontFamily.ToString())) 139 | sb.Append($"font-family:{run.FontFamily};"); 140 | 141 | if (run.FontWeight == FontWeight.Bold) 142 | sb.Append("font-weight:bold;"); 143 | 144 | if (run.FontStyle == FontStyle.Italic) 145 | sb.Append("font-style:italic;"); 146 | 147 | if (run.FontSize > 0) 148 | sb.Append($"font-size:{run.FontSize}px;"); 149 | 150 | string? foregroundColor = GetCssColor(run.Foreground); 151 | if (foregroundColor != null) 152 | sb.Append($"color:{foregroundColor};"); 153 | 154 | string? backgroundColor = GetCssColor(run.Background); 155 | if (backgroundColor != null) 156 | sb.Append($"background-color:{backgroundColor};"); 157 | 158 | 159 | if (run.TextDecorations != null) 160 | { 161 | foreach (var td in run.TextDecorations) 162 | { 163 | switch (td.Location) 164 | { 165 | case TextDecorationLocation.Underline: 166 | sb.Append("text-decoration:underline;"); 167 | break; 168 | case TextDecorationLocation.Strikethrough: 169 | sb.Append("text-decoration:line-through;"); 170 | break; 171 | } 172 | } 173 | } 174 | 175 | if (run.BaselineAlignment == BaselineAlignment.Superscript) 176 | sb.Append("vertical-align: super;"); 177 | if (run.BaselineAlignment == BaselineAlignment.Subscript) 178 | sb.Append("vertical-align: sub;"); 179 | 180 | return sb.ToString(); 181 | } 182 | 183 | 184 | private static string? GetCssColor(IBrush? brush) 185 | { 186 | if (brush is SolidColorBrush solid) 187 | { 188 | var color = solid.Color; 189 | if (color == Colors.Transparent) return null; 190 | return $"#{color.R:X2}{color.G:X2}{color.B:X2}"; 191 | //return $"rgba({c.R},{c.G},{c.B},{c.A / 255.0:F2})"; 192 | } 193 | 194 | return null; 195 | } 196 | 197 | 198 | 199 | } -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateFlowDocument/GetFlowDocument.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Media; 3 | using DocumentFormat.OpenXml; 4 | using DocumentFormat.OpenXml.Packaging; 5 | using System; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using static AvRichTextBox.HelperMethods; 9 | 10 | namespace AvRichTextBox; 11 | 12 | internal static partial class WordConversions 13 | { 14 | internal static MainDocumentPart? mainDocPart; 15 | internal static string DefaultEastAsiaFont = ""; 16 | internal static string DefaultAsciiFont = ""; 17 | 18 | internal static void GetFlowDocument(MainDocumentPart mDocPart, FlowDocument fdoc) 19 | { 20 | 21 | try 22 | { 23 | 24 | mainDocPart = mDocPart; 25 | 26 | //Implement doc-wide properties someday: 27 | 28 | // Dim docVariablesList As List(Of DocumentVariables) = mainDocPart.DocumentSettingsPart.Settings.Descendants(Of DocumentVariables)().ToList() 29 | // 'foreach (DocumentVariables docVars in docVariablesList) 30 | // ' foreach (DocumentVariable docVar in docVars) 31 | //mainDocPart.DocumentSettingsPart.Settings 32 | 33 | //StyleDefinitionsPart? styles = mainDocPart.StyleDefinitionsPart!; 34 | //if (styles != null) 35 | //{ 36 | // var defParProps = styles.Styles!.DocDefaults!.ParagraphPropertiesDefault; 37 | 38 | // //if (defParProps.ParagraphPropertiesBaseStyle != null) 39 | // //{ 40 | // // if (defParProps.ParagraphPropertiesBaseStyle.SpacingBetweenLines == null) 41 | // // fdoc.LineHeight = 50; 42 | // // else 43 | // // fdoc.LineHeight = TwipToPix(Convert.ToDouble(defParProps.ParagraphPropertiesBaseStyle.SpacingBetweenLines.Line.Value)); 44 | // //} 45 | 46 | // //var runFonts = styles.Styles.DocDefaults.RunPropertiesDefault.RunPropertiesBaseStyle.RunFonts; 47 | 48 | //if (runFonts != null) 49 | //{ 50 | // DefaultAsciiFont = (runFonts.Ascii == null) ? "Times New Roman" : runFonts.Ascii.Value; 51 | // DefaultEastAsiaFont = (runFonts.EastAsia == null) ? "MS 明朝" : runFonts.EastAsia.Value; 52 | // if (DefaultEastAsiaFont == "Mincho") DefaultEastAsiaFont = "MS 明朝"; 53 | // fdoc.FontFamily = new FontFamily(DefaultAsciiFont + ", " + DefaultEastAsiaFont); 54 | //} 55 | 56 | // //foreach (DocumentFormat.OpenXml.Wordprocessing.Style dStyle in styles.Styles.Descendants()) 57 | // //{ 58 | // // if (dStyle.StyleName.Val == "Normal") 59 | // // fdoc.FontSize = (dStyle.StyleRunProperties == null || dStyle.StyleRunProperties.FontSize == null) ? 18 : PointsToPixels(Convert.ToDouble(dStyle.StyleRunProperties.FontSize.Val)) / 2; 60 | // //} 61 | //} 62 | 63 | fdoc.PagePadding = new Thickness(50); //set a small default padding 64 | 65 | DocumentFormat.OpenXml.Wordprocessing.PageMargin? pMarg = mainDocPart.Document.Descendants().FirstOrDefault()!; 66 | 67 | if (pMarg != null) 68 | { 69 | double docmargT = TwipToPix(Convert.ToDouble((int)pMarg.Top!)); 70 | double docmargR = TwipToPix(Convert.ToDouble((uint)pMarg.Right!)); 71 | double docmargB = TwipToPix(Convert.ToDouble((int)pMarg.Bottom!)); 72 | double docmargL = TwipToPix(Convert.ToDouble((uint)pMarg.Left!)); 73 | fdoc.PagePadding = new Thickness(docmargL, docmargT, docmargR, docmargB); 74 | } 75 | 76 | //DocumentFormat.OpenXml.Wordprocessing.PageSize pSize = mainDocPart.Document.Descendants().FirstOrDefault(); 77 | //if (pSize != null) 78 | //{ 79 | // fdoc.PageWidth = TwipToPix(Convert.ToDouble((uint)pSize.Width)); 80 | // fdoc.PageHeight = TwipToPix(Convert.ToDouble((uint)pSize.Height)); 81 | //} 82 | 83 | 84 | OpenXmlElement? docBody = mainDocPart.Document.Body!; 85 | 86 | if (docBody != null) 87 | { 88 | foreach (OpenXmlElement section in docBody.Elements()) 89 | { //MessageBox.Show(section.LocalName); 90 | 91 | switch (section.LocalName) 92 | { 93 | case "sectPr": 94 | 95 | //DocumentFormat.OpenXml.Wordprocessing.PageMargin? spMarg = section.Descendants().FirstOrDefault(); 96 | if (pMarg != null) 97 | { 98 | double docmargT = TwipToPix(Convert.ToDouble((int)pMarg.Top!)); 99 | double docmargR = TwipToPix(Convert.ToDouble((uint)pMarg.Right!)); 100 | double docmargB = TwipToPix(Convert.ToDouble((int)pMarg.Bottom!)); 101 | double docmargL = TwipToPix(Convert.ToDouble((uint)pMarg.Left!)); 102 | fdoc.PagePadding = new Thickness(docmargL, docmargT, docmargR, docmargB); 103 | } 104 | 105 | //DocumentFormat.OpenXml.Wordprocessing.PageSize spSize = section.Descendants().FirstOrDefault(); 106 | //if (pSize != null) 107 | //{ 108 | // fdoc.PageWidth = TwipToPix(Convert.ToDouble((uint)pSize.Width)); 109 | // fdoc.PageHeight = TwipToPix(Convert.ToDouble((uint)pSize.Height)); 110 | //} 111 | 112 | //DocumentFormat.OpenXml.Wordprocessing.SpacingBetweenLines spLineSpacing = section.Descendants().FirstOrDefault(); 113 | //// var linespacingRuleelm As OpenXmlElement = section.ChildElements.Where(Function(ce) ce.LocalName = "lineRule")(0) 114 | 115 | //if (spLineSpacing != null) 116 | //{ 117 | // //MessageBox.Show("section linespacing not null"); 118 | // fdoc.LineHeight = TwipToPix(Convert.ToDouble(spLineSpacing.Line)); 119 | //} 120 | 121 | break; 122 | 123 | 124 | case "tbl": 125 | 126 | //DocumentFormat.OpenXml.Wordprocessing.Table wtable = (DocumentFormat.OpenXml.Wordprocessing.Table)section; 127 | 128 | //System.Windows.Documents.Table newTable = GetTable(wtable); 129 | ////System.Windows.Documents.Table newTable = GetTable(section); 130 | 131 | //foreach (TableRow tr in newTable.RowGroups[0].Rows) 132 | //{ 133 | // foreach (TableCell cel in tr.Cells) 134 | // { 135 | // cel.BorderBrush = Brushes.Black; 136 | // cel.BorderThickness = new Thickness(0.3); 137 | // cel.Padding = new Thickness(10); 138 | // cel.LineHeight = fdoc.LineHeight; 139 | // } 140 | //} 141 | 142 | //fdoc.Blocks.Add(newTable); 143 | break; 144 | 145 | case "p": 146 | 147 | try 148 | { 149 | Paragraph para = GetParagraph(section); 150 | //para.FontFamily = fdoc.FontFamily; 151 | para.Margin = new Thickness(0); 152 | if (para.Inlines.Count == 0) 153 | para.Inlines.Add(new EditableRun("")); 154 | fdoc.Blocks.Add(para); 155 | } 156 | catch (Exception paraEx) { Debug.WriteLine($"Could not get paragraph:\n{paraEx.Message}"); } 157 | 158 | break; 159 | 160 | default: 161 | Debug.WriteLine("no section name"); 162 | break; 163 | 164 | } 165 | } 166 | 167 | } 168 | 169 | } 170 | catch (Exception ex) { Debug.WriteLine($"Error getting flow doc: \n{ex.Message}"); } 171 | 172 | } 173 | 174 | 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateFlowDocument/GetTable.cs: -------------------------------------------------------------------------------- 1 | //using DocumentFormat.OpenXml; 2 | //using DocumentFormat.OpenXml.Wordprocessing; 3 | //using System; 4 | //using System.Collections.Generic; 5 | //using System.Linq; 6 | 7 | //namespace AvRichTextBox; 8 | 9 | //internal static partial class WordConversions 10 | //{ 11 | 12 | // internal static System.Windows.Documents.Table GetTable(DocumentFormat.OpenXml.Wordprocessing.Table wordTable) 13 | // { 14 | 15 | // var thisTable = new System.Windows.Documents.Table(); 16 | // thisTable.RowGroups.Add(new TableRowGroup()); 17 | // thisTable.BorderThickness = new Thickness(0.5); // initial default 18 | // thisTable.BorderBrush = Brushes.Black; 19 | // thisTable.CellSpacing = 0; // default is "2" 20 | // thisTable.Margin = new Thickness(0, 0, 0, 0); // just in case 21 | // thisTable.Padding = new Thickness(0, 0, 0, 0); 22 | 23 | 24 | // var ColumnWidths = new List(); 25 | // TableGrid tgrid = wordTable.Descendants().FirstOrDefault(); 26 | // int nocols = tgrid.Descendants().Count(); 27 | 28 | // var LastVmergedHeadCells = new System.Windows.Documents.TableCell[nocols + 1]; 29 | 30 | // thisTable.BorderBrush = Brushes.Black; 31 | // thisTable.BorderThickness = new Thickness(1); 32 | 33 | // //MessageBox.Show(wordTable.Descendants().Count().ToString()); 34 | 35 | // foreach (OpenXmlElement telm in wordTable.ChildElements) 36 | // { 37 | // switch (telm.LocalName) 38 | // { 39 | // case "tblGrid": 40 | 41 | // foreach (OpenXmlElement gridelm in telm.ChildElements) 42 | // { 43 | // switch (gridelm.LocalName) 44 | // { 45 | // case "gridCol": 46 | // var newtabcol = new TableColumn(); 47 | // var glc = new GridLengthConverter(); 48 | // newtabcol.Width = new GridLength(TwipToPix(Convert.ToDouble(gridelm.GetAttributes()[0].Value))); 49 | // thisTable.Columns.Add(newtabcol); 50 | // break; 51 | // } 52 | // } 53 | 54 | // break; 55 | 56 | // case "tr": 57 | 58 | // var newrow = new System.Windows.Documents.TableRow(); 59 | // thisTable.RowGroups[0].Rows.Add(newrow); 60 | // int colno = 0; 61 | 62 | // foreach (OpenXmlElement cn in telm.ChildElements) 63 | // { 64 | // switch (cn.LocalName) 65 | // { 66 | // case "tc": 67 | 68 | // var thiscell = new System.Windows.Documents.TableCell(); 69 | 70 | // thiscell.Tag = new List(new string[] { "", "" }); 71 | 72 | // foreach (OpenXmlElement CellParNode in cn.ChildElements) 73 | // { 74 | // switch (CellParNode.LocalName) 75 | // { 76 | // case "tcPr": 77 | // // Get cell properties 78 | // foreach (OpenXmlElement CellPropNode in CellParNode.ChildElements) 79 | // { 80 | // switch (CellPropNode.LocalName) 81 | // { 82 | // case "tcW": 83 | // break; 84 | 85 | // case "gridSpan": 86 | // thiscell.ColumnSpan = Convert.ToInt32(CellPropNode.GetAttributes()[0].Value); 87 | // colno += thiscell.ColumnSpan - 1; 88 | // break; 89 | 90 | // case "vAlign": 91 | // if ((CellPropNode.GetAttributes()[0].Value ?? "") == "center") 92 | // thiscell.Focusable = true; 93 | // else 94 | // thiscell.Focusable = false; 95 | // break; 96 | 97 | // case "vmerge": 98 | // case "vMerge": 99 | // if (CellPropNode.GetAttributes().Count == 0 || (CellPropNode.GetAttributes()[0].Value ?? "") == "continue") 100 | // { 101 | // LastVmergedHeadCells[colno].RowSpan += 1; 102 | // ((List)thiscell.Tag)[0] = "merged"; 103 | // } 104 | // else 105 | // { 106 | // // restart 107 | // ((List)thiscell.Tag)[0] = ""; 108 | // LastVmergedHeadCells[colno] = thiscell; 109 | // LastVmergedHeadCells[colno].RowSpan = 1; 110 | // } 111 | 112 | // break; 113 | 114 | // case "tcBorders": 115 | // break; 116 | 117 | // case "tcMar": 118 | // break; 119 | // } 120 | // } 121 | 122 | // break; 123 | 124 | // case "p": 125 | // thiscell.Blocks.Add(GetParagraph(CellParNode)); 126 | // break; 127 | // } 128 | // } 129 | 130 | // // Add cell to current row 131 | // if (((List)thiscell.Tag)[0] != "merged") 132 | // { 133 | // newrow.Cells.Add(thiscell); 134 | // if (colno < LastVmergedHeadCells.Count()) 135 | // { 136 | // LastVmergedHeadCells[colno] = thiscell; 137 | // LastVmergedHeadCells[colno].RowSpan = 1; 138 | // } 139 | // } 140 | 141 | // colno += 1; 142 | // break; 143 | // } 144 | // } 145 | 146 | // break; 147 | 148 | // } 149 | // } 150 | 151 | // return thisTable; 152 | 153 | // } 154 | 155 | //} 156 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateWordDocument/CreateWordDrawing.cs: -------------------------------------------------------------------------------- 1 | using A = DocumentFormat.OpenXml.Drawing; 2 | using DOW = DocumentFormat.OpenXml.Wordprocessing; 3 | using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing; 4 | using PIC = DocumentFormat.OpenXml.Drawing.Pictures; 5 | using static AvRichTextBox.HelperMethods; 6 | 7 | namespace AvRichTextBox; 8 | 9 | internal static partial class WordConversions 10 | { 11 | internal static DOW.Drawing CreateWordDocDrawing(string relationshipID, double pixelWidth, double pixelHeight, string extension) 12 | { 13 | double emuWidth = PixToEMU(pixelWidth); 14 | double emuHeight = PixToEMU(pixelHeight); 15 | 16 | var drawingElement = new DOW.Drawing(new DW.Inline(new DW.Extent() 17 | { 18 | Cx = (int)emuWidth, 19 | Cy = (int)emuHeight 20 | }, new DW.EffectExtent() 21 | { 22 | LeftEdge = 0L, 23 | TopEdge = 0L, 24 | RightEdge = 0L, 25 | BottomEdge = 0L, 26 | }, 27 | new DW.DocProperties() { Id = 1U, Name = "Picture1" }, 28 | new DW.NonVisualGraphicFrameDrawingProperties(new A.GraphicFrameLocks() { NoChangeAspect = true }), 29 | new A.Graphic( 30 | new A.GraphicData( 31 | new PIC.Picture( 32 | new PIC.NonVisualPictureProperties(new PIC.NonVisualDrawingProperties() 33 | { 34 | Id = 0U, 35 | Name = "wordDrawing" + extension 36 | }, 37 | new PIC.NonVisualPictureDrawingProperties()), 38 | new PIC.BlipFill(new A.Blip(new A.BlipExtensionList(new A.BlipExtension() { Uri = "{28A0092B-C50C-407E-A947-70E740481C1C}" })) 39 | { 40 | Embed = relationshipID, 41 | CompressionState = A.BlipCompressionValues.Print 42 | }, new A.Stretch(new A.FillRectangle()) 43 | ), 44 | new PIC.ShapeProperties( 45 | new A.Transform2D( 46 | new A.Offset() { X = 0L, Y = 0L }, 47 | new A.Extents() { Cx = (int)emuWidth, Cy = (int)emuHeight } 48 | ), 49 | new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle } 50 | ))) 51 | { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" } 52 | )) 53 | { 54 | DistanceFromTop = 0U, 55 | DistanceFromBottom = 0U, 56 | DistanceFromLeft = 0U, 57 | DistanceFromRight = 0U 58 | } 59 | ); 60 | 61 | return drawingElement; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateWordDocument/CreateWordParagraph.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Documents; 4 | using Avalonia.Media.Imaging; 5 | using DocumentFormat.OpenXml; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Wordprocessing; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using DOW = DocumentFormat.OpenXml.Wordprocessing; 11 | using static AvRichTextBox.HelperMethods; 12 | 13 | namespace AvRichTextBox; 14 | 15 | internal static partial class WordConversions 16 | { 17 | internal static DOW.Paragraph CreateWordDocParagraph(Block b) 18 | { 19 | 20 | var parg = new DOW.Paragraph(); 21 | var pPr = new ParagraphProperties(); 22 | Paragraph? p = b as Paragraph; 23 | 24 | pPr.Justification = new() 25 | { 26 | Val = p!.TextAlignment switch 27 | { 28 | Avalonia.Media.TextAlignment.Left => JustificationValues.Left, 29 | Avalonia.Media.TextAlignment.Center => JustificationValues.Center, 30 | Avalonia.Media.TextAlignment.Right => JustificationValues.Right, 31 | Avalonia.Media.TextAlignment.Justify => JustificationValues.Both, 32 | _ => JustificationValues.Left 33 | } 34 | }; 35 | 36 | if (p.Background!= null && p.Background.Color != Avalonia.Media.Colors.Transparent) 37 | pPr.Shading = new() { Val = ShadingPatternValues.Clear, Color = "auto", Fill = ToOpenXmlColor(p.Background.Color) }; 38 | 39 | if (p.BorderBrush != null && p.BorderBrush.Color != Avalonia.Media.Colors.Transparent) 40 | { 41 | pPr.ParagraphBorders = new() 42 | { 43 | LeftBorder = new() { Val = BorderValues.Single, Color = ToOpenXmlColor(p.BorderBrush.Color), Size = (uint)(p.BorderThickness.Left * 6), Space = 0 }, 44 | TopBorder = new() { Val = BorderValues.Single, Color = ToOpenXmlColor(p.BorderBrush.Color), Size = (uint)(p.BorderThickness.Top * 6), Space = 0 }, 45 | RightBorder = new() { Val = BorderValues.Single, Color = ToOpenXmlColor(p.BorderBrush.Color), Size = (uint)(p.BorderThickness.Right * 6), Space = 0 }, 46 | BottomBorder = new() { Val = BorderValues.Single, Color = ToOpenXmlColor(p.BorderBrush.Color), Size = (uint)(p.BorderThickness.Bottom * 6), Space = 0 }, 47 | }; 48 | } 49 | 50 | pPr.SpacingBetweenLines = new SpacingBetweenLines() { Before = "0", After = "0", LineRule = LineSpacingRuleValues.Auto, Line = "240" }; 51 | pPr.SnapToGrid = new SnapToGrid() { Val = new OnOffValue(false) }; 52 | parg.AppendChild(pPr); 53 | 54 | foreach (IEditable iline in p.Inlines) 55 | { 56 | switch (iline) 57 | { 58 | //case var @case when @case == typeof(EditableLineBreak): 59 | case EditableLineBreak elbreak: 60 | parg.AppendChild(new Break()); 61 | break; 62 | 63 | case EditableInlineUIContainer edUIC: 64 | 65 | if (edUIC.Child.GetType() == typeof(Image)) 66 | { 67 | //string fontFamily = edUIC.FontFamily.ToString(); 68 | 69 | Image img = (Image)edUIC.Child; 70 | img.Width = img.Bounds.Width; 71 | img.Height = img.Bounds.Height; 72 | Bitmap? imgbitmap = (Bitmap)img.Source!; 73 | 74 | string extension = ""; 75 | ImagePart imagePart = mainPart!.AddImagePart(ImagePartType.Jpeg); 76 | 77 | //Debug.WriteLine("Imagesource is null ? : " + (thisImg.Source == null)); 78 | if (imgbitmap != null) 79 | { 80 | using (var memStream = new MemoryStream()) 81 | { 82 | ResizeAndSaveBitmap(imgbitmap, (int)imgbitmap.Size.Width, (int)imgbitmap.Size.Height, memStream); 83 | memStream.Position = 0; 84 | imagePart.FeedData(memStream); 85 | extension = ".jpg"; 86 | } 87 | 88 | parg.AppendChild(new DOW.Run(CreateWordDocDrawing(mainPart!.GetIdOfPart(imagePart), img.Width, img.Height, extension))); 89 | 90 | } 91 | } 92 | 93 | break; 94 | 95 | case EditableRun erun: 96 | DOW.Run dRun = GetWordDocRun(erun); 97 | //if (dRun.InnerText == "\v") 98 | // parg.AppendChild(new Break()); 99 | //else 100 | parg.AppendChild(dRun); 101 | break; 102 | } 103 | } 104 | 105 | return parg; 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateWordDocument/CreateWordRun.cs: -------------------------------------------------------------------------------- 1 | using DocumentFormat.OpenXml; 2 | using DocumentFormat.OpenXml.Wordprocessing; 3 | using DOW = DocumentFormat.OpenXml.Wordprocessing; 4 | using Avalonia.Media; 5 | using System; 6 | using System.Diagnostics; 7 | using static AvRichTextBox.HelperMethods; 8 | using System.Text; 9 | 10 | namespace AvRichTextBox; 11 | 12 | internal static partial class WordConversions 13 | { 14 | internal static DOW.Run GetWordDocRun(EditableRun edRun) 15 | { 16 | string? thisRunText = ""; 17 | thisRunText = edRun.Text; 18 | //Debug.WriteLine("thisRuntext= " + thisRunText); 19 | 20 | var newrun = new DOW.Run(); 21 | 22 | try 23 | { 24 | 25 | var runtext = new Text(thisRunText!) // convert text to "wordprocessing.text" form 26 | { 27 | Space = SpaceProcessingModeValues.Preserve 28 | }; 29 | 30 | var RunProp = new RunProperties(); 31 | 32 | if (edRun.TextDecorations != null) 33 | { 34 | foreach (TextDecoration td in edRun.TextDecorations!) 35 | { 36 | switch (td.Location) 37 | { 38 | case TextDecorationLocation.Underline: 39 | RunProp.AppendChild(new DOW.Underline() { Val = UnderlineValues.Single, Color = "Black" }); 40 | break; 41 | case TextDecorationLocation.Overline: { break; } 42 | case TextDecorationLocation.Baseline: { break; } 43 | case TextDecorationLocation.Strikethrough: 44 | RunProp.AppendChild(new DOW.Strike()); 45 | break; 46 | } 47 | } 48 | } 49 | 50 | if (edRun.FontWeight == FontWeight.Bold) 51 | RunProp.AppendChild(new DOW.Bold()); 52 | 53 | if (edRun.FontStyle == FontStyle.Italic) 54 | RunProp.AppendChild(new DOW.Italic()); 55 | 56 | if (edRun.Background != null) 57 | { 58 | var Hlight = new Highlight() { Val = BrushToHighlightColorValue(edRun.Background) }; 59 | RunProp.AppendChild(Hlight); 60 | } 61 | 62 | if (edRun.Foreground != null) 63 | { 64 | var foreColor = new DOW.Color() { Val = edRun.Foreground.ToString() }; 65 | RunProp.AppendChild(foreColor); 66 | } 67 | 68 | switch (edRun.BaselineAlignment) 69 | { 70 | case BaselineAlignment.Baseline: RunProp.AppendChild(new VerticalTextAlignment() { Val = VerticalPositionValues.Baseline }); break; 71 | case BaselineAlignment.TextTop: RunProp.AppendChild(new VerticalTextAlignment() { Val = VerticalPositionValues.Superscript }); break; 72 | case BaselineAlignment.Bottom: RunProp.AppendChild(new VerticalTextAlignment() { Val = VerticalPositionValues.Subscript }); break; 73 | } 74 | 75 | // Get font 76 | var rFont = new RunFonts(); 77 | 78 | 79 | 80 | var fntSize = default(double); 81 | rFont.Ascii = edRun.FontFamily.ToString(); 82 | rFont.HighAnsi = edRun.FontFamily.ToString(); 83 | //rFont.EastAsia = edRun.FontFamily.ToString(); 84 | rFont.EastAsia = "Meiryo UI"; // temporary default 85 | 86 | fntSize = edRun.FontSize * 0.75 * 2; // converts pixels to points 87 | 88 | RunProp.AppendChild(rFont); 89 | RunProp.AppendChild(new FontSize() { Val = fntSize.ToString() }); 90 | 91 | //Attach run properties 92 | newrun.AppendChild(RunProp); 93 | 94 | // Must parse line breaks 95 | if (!string.IsNullOrEmpty(runtext.Text)) 96 | { 97 | //if (runtext.Text.Contains(Constants.vbLf)) 98 | if (runtext.Text.Contains('\n')) 99 | ParseRunText(ref newrun, runtext.Text); 100 | else 101 | 102 | newrun.AppendChild(runtext); 103 | } 104 | 105 | } 106 | catch (Exception ex) { Debug.WriteLine($"Failed to create run: {edRun.Text}\nexception: {ex.Message}"); } 107 | 108 | return newrun; 109 | } 110 | 111 | public static void ParseRunText(ref DOW.Run r, string tData) 112 | { 113 | //var newLineArray = new[] { Constants.vbLf }; 114 | //var textArray = tData.Split(newLineArray, StringSplitOptions.None); 115 | var textArray = tData.Split("\n".ToCharArray(), StringSplitOptions.None); 116 | 117 | foreach (string line in textArray) 118 | { 119 | if (string.IsNullOrEmpty(line)) 120 | { 121 | if (r.LastChild is not Break) 122 | r.AppendChild(new Break()); 123 | } 124 | else 125 | { 126 | var txt = new Text 127 | { 128 | Text = line 129 | }; 130 | r.AppendChild(txt); 131 | } 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateWordDocument/CreateWordTable.cs: -------------------------------------------------------------------------------- 1 | //using DocumentFormat.OpenXml; 2 | //using DocumentFormat.OpenXml.Packaging; 3 | //using DocumentFormat.OpenXml.Wordprocessing; 4 | //using System.Diagnostics; 5 | //using System.Windows.Media.Imaging; 6 | //using System.Xml; 7 | //using DOW = DocumentFormat.OpenXml.Wordprocessing; 8 | 9 | //namespace AvRichTextBox; 10 | 11 | //internal static partial class WordConversions 12 | //{ 13 | // static bool firstTable = true; 14 | 15 | // internal static DOW.Table CreateWordDocTable(System.Windows.Documents.Table t) 16 | // { 17 | 18 | // var tabl = new DOW.Table(); 19 | 20 | // List VMergeCount = new List(); 21 | // for (int i = 0; i < 100; i++) VMergeCount.Add(new Point(1, 0)); 22 | 23 | // // Add column definitions 24 | // var tblGrid = new TableGrid(); 25 | // for (int colno = 0; colno < t.Columns.Count; colno++) 26 | // tblGrid.Append(new GridColumn() { Width = PixToTwip(t.Columns[colno].Width.Value).ToString() }); 27 | // tabl.Append(tblGrid); 28 | 29 | // int coloffset = 0; 30 | 31 | // foreach (TableRowGroup rgroup in t.RowGroups) 32 | // { 33 | // foreach (System.Windows.Documents.TableRow r in rgroup.Rows) 34 | // { 35 | // var trow = new DOW.TableRow(); 36 | // tabl.AppendChild(trow); 37 | // coloffset = 0; 38 | 39 | // for (int colno = 0; colno < t.Columns.Count; colno++) 40 | // { 41 | 42 | // var cel = new DOW.TableCell(); 43 | // trow.Append(cel); 44 | // var tcprop = new TableCellProperties(); 45 | // cel.Append(tcprop); 46 | 47 | 48 | // var tcwid = new TableCellWidth() { Width = PixToTwip(t.Columns[colno].Width.Value).ToString() }; 49 | // tcprop.Append(tcwid); 50 | 51 | // var tcbor = new TableCellBorders(); 52 | // tcprop.Append(tcbor); 53 | 54 | // if (VMergeCount[colno].X > 1) 55 | // { 56 | // tcprop.Append(new VerticalMerge() { Val = new EnumValue(MergedCellValues.Continue) }); 57 | // tcprop.Append(new GridSpan() { Val = (Int32Value)(VMergeCount[colno].Y + 1) }); 58 | 59 | // VMergeCount[colno] = new Point(VMergeCount[colno].X - 1, VMergeCount[colno].Y); 60 | 61 | // colno += (int)VMergeCount[colno].Y; 62 | // coloffset += (int)VMergeCount[colno].Y + 1; 63 | 64 | // var pprop = new ParagraphProperties() { SpacingBetweenLines = new SpacingBetweenLines() { Line = "240", LineRule = LineSpacingRuleValues.Auto, Before = "0", After = "0" } }; 65 | // var newpar = new DOW.Paragraph(); 66 | // newpar.Append(pprop); 67 | 68 | // cel.Append(newpar); // necessary to add paragraph to vmerged cell 69 | // } 70 | // else 71 | // { 72 | // tcprop.Append(new VerticalMerge() { Val = new EnumValue(MergedCellValues.Restart) }); 73 | 74 | // foreach (Block b in r.Cells[colno - coloffset].Blocks) 75 | // cel.Append(CreateWordDocParagraph((System.Windows.Documents.Paragraph)b)); 76 | 77 | // if (r.Cells[colno - coloffset].RowSpan > 1) 78 | // VMergeCount[colno] = new Point(r.Cells[colno - coloffset].RowSpan, r.Cells[colno - coloffset].ColumnSpan - 1); 79 | 80 | // if (r.Cells[colno - coloffset].ColumnSpan > 1) 81 | // tcprop.GridSpan = new GridSpan() { Val = r.Cells[colno - coloffset].ColumnSpan }; 82 | // else 83 | // VMergeCount[colno] = new Point(VMergeCount[colno].X, 0); 84 | 85 | // var thisCellVertAlign = new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Top }; 86 | // switch (r.Cells[colno - coloffset].BorderThickness.Left) 87 | // { 88 | // case 0.49: 89 | // thisCellVertAlign.Val = TableVerticalAlignmentValues.Center; 90 | // break; 91 | // case 0.48: 92 | // thisCellVertAlign.Val = TableVerticalAlignmentValues.Bottom; 93 | // break; 94 | // } 95 | // tcprop.Append(thisCellVertAlign); 96 | 97 | 98 | // var lmar = new LeftMargin() { Width = PixToTwip(r.Cells[colno - coloffset].Padding.Left).ToString() }; 99 | // var tmar = new TopMargin() { Width = PixToTwip(r.Cells[colno - coloffset].Padding.Top).ToString() }; 100 | // var rmar = new RightMargin() { Width = PixToTwip(r.Cells[colno - coloffset].Padding.Right).ToString() }; 101 | // var bmar = new BottomMargin() { Width = PixToTwip(r.Cells[colno - coloffset].Padding.Bottom).ToString() }; 102 | // var tcmar = new TableCellMargin() { LeftMargin = lmar, TopMargin = tmar, RightMargin = rmar, BottomMargin = bmar }; 103 | // tcprop.Append(tcmar); 104 | 105 | // int thiscolspan = r.Cells[colno - coloffset].ColumnSpan; 106 | // VMergeCount[colno] = new Point(VMergeCount[colno].X, thiscolspan - 1); 107 | // colno += thiscolspan - 1; 108 | // coloffset += thiscolspan - 1; 109 | // } 110 | 111 | // tcbor.Append(new LeftBorder() { Size = 7, Color = "#000000", Val = BorderValues.Single }); 112 | // tcbor.Append(new RightBorder() { Size = 7, Color = "#000000", Val = BorderValues.Single }); 113 | // tcbor.Append(new TopBorder() { Size = 7, Color = "#000000", Val = BorderValues.Single }); 114 | // tcbor.Append(new BottomBorder() { Size = 7, Color = "#000000", Val = BorderValues.Single}); 115 | // } 116 | // } 117 | // } 118 | 119 | // return tabl; 120 | // } 121 | 122 | 123 | //} 124 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/CreateWordDocument/WordDocumentCreator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media; 2 | using DocumentFormat.OpenXml; 3 | using DocumentFormat.OpenXml.Packaging; 4 | using DocumentFormat.OpenXml.Wordprocessing; 5 | using System; 6 | using System.Diagnostics; 7 | using DOW = DocumentFormat.OpenXml.Wordprocessing; 8 | using AvColor = Avalonia.Media.Color; 9 | using static AvRichTextBox.HelperMethods; 10 | 11 | namespace AvRichTextBox; 12 | 13 | internal static partial class WordConversions 14 | { 15 | private static MainDocumentPart? mainPart; 16 | 17 | internal static void SaveWordDoc(string saveWordFileName, FlowDocument fdoc) 18 | { 19 | try 20 | { 21 | using var WPDoc = WordprocessingDocument.Create(saveWordFileName, WordprocessingDocumentType.Document); 22 | 23 | mainPart = WPDoc.AddMainDocumentPart(); 24 | mainPart.Document = new Document(); 25 | 26 | 27 | //Set page and margin size according to Client 28 | int pmarLeft = default, pmarTop = default, pmarRight = default, pmarBottom = default; 29 | pmarLeft = Convert.ToInt32(PixToTwip(fdoc.PagePadding.Left)); 30 | pmarTop = Convert.ToInt32(PixToTwip(fdoc.PagePadding.Top)); 31 | pmarRight = Convert.ToInt32(PixToTwip(fdoc.PagePadding.Right)); 32 | pmarBottom = Convert.ToInt32(PixToTwip(fdoc.PagePadding.Bottom)); 33 | var newpageMargin = new PageMargin() { Left = Convert.ToUInt32(pmarLeft), Top = pmarTop, Bottom = pmarBottom, Right = Convert.ToUInt32(pmarRight), Header = 720, Footer = 720 }; 34 | 35 | var newcol = new Columns() { Space = new StringValue("720") }; // (StringValue)720 }; 36 | var newpageSize = new PageSize() { Width = 21 * 567, Height = Convert.ToUInt32(29.7 * 567) }; // A4 size 37 | 38 | 39 | //Start creating word body 40 | var body = new Body(); 41 | var sectp = new SectionProperties(); 42 | sectp.Append(newpageMargin); 43 | sectp.Append(newpageSize); 44 | body.Append(sectp); 45 | 46 | 47 | mainPart.Document.AppendChild(body); 48 | 49 | foreach (Block b in fdoc.Blocks) 50 | { 51 | switch (b.GetType()) 52 | { 53 | case Type t when t == typeof(Paragraph): 54 | 55 | Paragraph p = (Paragraph)b; 56 | DOW.Paragraph dowP = CreateWordDocParagraph(p); 57 | body.AppendChild(dowP); 58 | 59 | //Debug.WriteLine("\npPR.spacing=" + dowP.ParagraphProperties.SpacingBetweenLines.Line.ToString() + " (" + dowP.ParagraphProperties.SpacingBetweenLines.LineRule.ToString() + ")"); 60 | 61 | break; 62 | 63 | //case Type t when t == typeof(Table): 64 | // body.AppendChild(CreateWordDocTable((Table)b)); 65 | // break; 66 | } 67 | } 68 | 69 | WPDoc.Save(); 70 | 71 | 72 | } 73 | catch (Exception ex) { Debug.WriteLine("Could not save: " + ex.GetType() + " /// " + ex.Message); } 74 | 75 | 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/WordImportExport/HelperMethods.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Media; 3 | using Avalonia.Media.Imaging; 4 | using Avalonia.Media.Immutable; 5 | using DocumentFormat.OpenXml.Wordprocessing; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text.RegularExpressions; 12 | using AvColor = Avalonia.Media.Color; 13 | 14 | namespace AvRichTextBox; 15 | 16 | internal static partial class HelperMethods 17 | { 18 | internal static AvColor ColorFromHex(string hex) => AvColor.Parse(hex); 19 | internal static double TwipToPix(double unitTwip) => 96.0 / 1440D * unitTwip; 20 | internal static double PixToTwip(double unitPix) => 15D * unitPix; 21 | internal static double EMUToPix(double unitTwip) => 96 / (double)914400 * unitTwip; 22 | internal static double PixToEMU(double unitPix) => 914400 / (double)96 * unitPix; 23 | internal static double PointsToPixels(double pt) => pt * 96D / 72D; 24 | internal static double InchesToPixels(double inch) => inch * 96D; 25 | internal static double PixelsToPoints(double px) => px * 72 / 96; 26 | internal static double TwipToDip(double twips) => twips * (96.0 / 1440.0); 27 | internal static double DipToTwip(double dips) => dips * (1440.0 / 96.0); 28 | 29 | internal static void ResizeAndSaveBitmap(Bitmap originalBitmap, int newWidth, int newHeight, Stream memoryStream) 30 | { 31 | var renderTarget = new RenderTargetBitmap(new PixelSize(newWidth, newHeight)); 32 | 33 | using (var context = renderTarget.CreateDrawingContext()) 34 | { 35 | context.DrawImage(originalBitmap, new Rect(0, 0, newWidth, newHeight)); 36 | } 37 | 38 | renderTarget.Save(memoryStream); // png by default 39 | } 40 | 41 | internal static string WordColorValueToHexString(string hcv, bool isBackground) 42 | { 43 | string hex = hcv.ToLower(); 44 | 45 | string returnString = hex switch 46 | { 47 | "darkyellow" => "#FF9ACD32", 48 | "darkred" => "#FF8B000000", 49 | "blueviolet" => "#FF931FDF", 50 | "cyan" => "#FFE8EBF9", 51 | "green" => "#FF98FB98", 52 | "yellow" => "#FFFFE0C0", 53 | "red" => "#FFFF4500", 54 | "blue" => "#FFE3BFFF", 55 | "black" or "ck" => "#FF000000", 56 | "lightgray" => "#FFCCCCCC", 57 | "magenta" => "#FFFF00FF", 58 | "white" => "#FFFFFFFF", 59 | "none" => "#00FFFFFF", 60 | _ => string.Empty 61 | }; 62 | 63 | if (returnString != string.Empty) return returnString; 64 | //return hexColorRegex().Replace(hex, "") == "" ? ("#" + hex) : (isBackground ? "#FFFFFFFF" : "#FF000000"); // default white or black 65 | return hexColorRegex().Replace(hex, "") == "" ? (hex) : (isBackground ? "#FFFFFFFF" : "#FF000000"); // default white or black 66 | 67 | } 68 | 69 | internal static HighlightColorValues BrushToHighlightColorValue(IBrush br) 70 | { 71 | if (br is not SolidColorBrush solidColorBrush || br == Brushes.Transparent) 72 | return HighlightColorValues.None; 73 | 74 | var inputColor = solidColorBrush.Color; 75 | 76 | // Check for direct matches first 77 | var predefinedMatch = HighlightColors.FirstOrDefault(kv => kv.Value == inputColor); 78 | if (HighlightColors.ContainsKey(predefinedMatch.Key)) 79 | { 80 | Debug.WriteLine("key = " + predefinedMatch.Key.ToString()); 81 | return predefinedMatch.Key; 82 | } 83 | 84 | 85 | return FindClosestHighlightColor(inputColor); 86 | 87 | } 88 | 89 | private static readonly Dictionary HighlightColors = new() 90 | { 91 | { HighlightColorValues.Yellow, Colors.Yellow }, 92 | { HighlightColorValues.Red, Colors.Red }, 93 | { HighlightColorValues.Cyan, Colors.Cyan }, 94 | { HighlightColorValues.Green, Colors.Green }, 95 | { HighlightColorValues.Blue, Colors.Blue }, 96 | { HighlightColorValues.DarkYellow, Colors.YellowGreen }, 97 | { HighlightColorValues.White, Colors.White }, 98 | { HighlightColorValues.Magenta, Colors.Magenta }, 99 | { HighlightColorValues.LightGray, Colors.LightGray }, 100 | { HighlightColorValues.DarkRed, Colors.DarkRed }, 101 | { HighlightColorValues.DarkMagenta, Colors.DarkMagenta }, 102 | { HighlightColorValues.DarkGreen, Colors.DarkGreen }, 103 | { HighlightColorValues.DarkGray, Colors.DarkGray }, 104 | { HighlightColorValues.DarkCyan, Colors.DarkCyan }, 105 | { HighlightColorValues.DarkBlue, Colors.DarkBlue }, 106 | { HighlightColorValues.Black, Colors.Black }, 107 | { HighlightColorValues.None, Colors.Transparent } 108 | }; 109 | 110 | internal static string ToOpenXmlColor(AvColor color) => $"{color.R:X2}{color.G:X2}{color.B:X2}"; 111 | 112 | internal static SolidColorBrush FromOpenXmlColor(string openxmlhex) 113 | { 114 | BrushConverter BConverter = new(); 115 | ImmutableSolidColorBrush? iSCB = (ImmutableSolidColorBrush)BConverter.ConvertFromString("#" + openxmlhex)!; 116 | return new SolidColorBrush(iSCB.Color); 117 | 118 | } 119 | 120 | private static HighlightColorValues FindClosestHighlightColor(AvColor color) 121 | { 122 | return HighlightColors.OrderBy(kv => ColorDistance(color, kv.Value)).First().Key; 123 | } 124 | 125 | private static double ColorDistance(AvColor c1, AvColor c2) 126 | { 127 | int rDiff = c1.R - c2.R; 128 | int gDiff = c1.G - c2.G; 129 | int bDiff = c1.B - c2.B; 130 | return Math.Sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff); 131 | } 132 | 133 | internal static int GetLanguageForChar(char c) 134 | { 135 | if (c is >= (char)0x4E00 and <= (char)0x9FFF or // CJK Unified Ideographs (Common Chinese, Japanese, Korean) 136 | >= (char)0x3400 and <= (char)0x4DBF) // CJK Extension A (Rare Chinese characters) 137 | return 2052; // Simplified Chinese (zh-CN) 138 | else if (c is >= (char)0x3040 and <= (char)0x30FF) // Hiragana & Katakana (Japanese) 139 | return 1041; // Japanese (ja-JP) 140 | else if (c is >= (char)0xAC00 and <= (char)0xD7AF) // Hangul Syllables (Korean) 141 | return 1042; // Korean (ko-KR) 142 | else if (c is >= (char)0x3100 and <= (char)0x312F) // Bopomofo (Traditional Chinese phonetic) 143 | return 1028; // Traditional Chinese (zh-TW) 144 | else 145 | return 1033; // Default to English (en-US) 146 | } 147 | 148 | internal static bool IsCJKChar(char c) => GetLanguageForChar(c) is 2052 or 1041 or 1042 or 1028; 149 | [GeneratedRegex("[#0-9a-f]")] 150 | private static partial Regex hexColorRegex(); 151 | } 152 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/Xaml/XamlPackageData/.rels: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/Xaml/XamlPackageData/[Content_Types].xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AvRichTextBox/SaveAndLoadFormats/Xaml/XamlStringFromFlowDoc.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuikp/AvRichTextBox/145e48b7aecf1b896dc4a5fd573061b5bcd449d9/AvRichTextBox/SaveAndLoadFormats/Xaml/XamlStringFromFlowDoc.cs -------------------------------------------------------------------------------- /AvRichTextBox/TextRange/TextRange.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Input; 3 | using DocumentFormat.OpenXml.Drawing.Charts; 4 | using System; 5 | using System.ComponentModel; 6 | using System.Data.SqlTypes; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Runtime.CompilerServices; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace AvRichTextBox; 15 | 16 | public class TextRange : INotifyPropertyChanged, IDisposable 17 | { 18 | public event PropertyChangedEventHandler? PropertyChanged; 19 | private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 20 | 21 | internal delegate void Start_ChangedHandler(TextRange sender, int newStart); 22 | internal event Start_ChangedHandler? Start_Changed; 23 | internal delegate void End_ChangedHandler(TextRange sender, int newEnd); 24 | internal event End_ChangedHandler? End_Changed; 25 | 26 | public override string ToString() => this.Start + " → " + this.End; 27 | 28 | public TextRange(FlowDocument flowdoc, int start, int end) 29 | { 30 | if (end < start) throw new AvaloniaInternalException("TextRange not valid (start must be less than end)"); 31 | 32 | this.Start = start; 33 | this.End = end; 34 | myFlowDoc = flowdoc; 35 | myFlowDoc.TextRanges.Add(this); 36 | 37 | } 38 | 39 | internal FlowDocument myFlowDoc; 40 | public int Length => End - Start; 41 | private int _Start; 42 | public int Start { get => _Start; set { if (_Start != value) { _Start = value; Start_Changed?.Invoke(this, value); NotifyPropertyChanged(nameof(Start)); } } } 43 | 44 | private int _End; 45 | public int End { get => _End; set { if (_End != value) { _End = value; End_Changed?.Invoke(this, value); NotifyPropertyChanged(nameof(End)); } } } 46 | 47 | internal void UpdateStart() { NotifyPropertyChanged(nameof(Start)); } 48 | internal void UpdateEnd() { NotifyPropertyChanged(nameof(End)); } 49 | 50 | internal Paragraph StartParagraph = null!; 51 | internal Paragraph EndParagraph = null!; 52 | 53 | internal Rect PrevCharRect; 54 | internal Rect StartRect { get; set; } 55 | internal Rect EndRect { get; set; } 56 | internal bool IsAtEndOfLineSpace = false; 57 | internal bool IsAtEndOfLine = false; 58 | internal bool IsAtLineBreak = false; 59 | 60 | internal bool BiasForwardStart = true; 61 | internal bool BiasForwardEnd = true; 62 | public void CollapseToStart() { End = Start; } 63 | public void CollapseToEnd() { Start = End ; } 64 | 65 | 66 | internal IEditable GetStartInline() 67 | { 68 | 69 | Paragraph? startPar = GetStartPar(); 70 | if (startPar == null) return null!; 71 | IEditable startInline = null!; 72 | IsAtLineBreak = false; 73 | 74 | if (BiasForwardStart) 75 | { 76 | IEditable startInlineReal = startPar.Inlines.LastOrDefault(ied => startPar.StartInDoc + ied.TextPositionOfInlineInParagraph <= Start)!; 77 | startInline = startPar.Inlines.LastOrDefault(ied => !ied.IsLineBreak && startPar.StartInDoc + ied.TextPositionOfInlineInParagraph <= Start)!; 78 | IsAtLineBreak = startInline != startInlineReal; 79 | //Debug.WriteLine("calculating isatlinebreak biasforwardstart"); 80 | } 81 | else 82 | { 83 | if (Start - startPar.StartInDoc == 0) 84 | startInline = startPar.Inlines.FirstOrDefault()!; 85 | else 86 | { 87 | //Debug.WriteLine("calculating isatlinebreak - OTHER"); 88 | startInline = startPar.Inlines.LastOrDefault(ied => startPar.StartInDoc + ied.TextPositionOfInlineInParagraph < Start)!; 89 | IEditable startInlineUpToLineBreak = startPar.Inlines.LastOrDefault(ied => !ied.IsLineBreak && startPar.StartInDoc + ied.TextPositionOfInlineInParagraph < Start)!; 90 | if (startInline.IsLineBreak) 91 | startInline = myFlowDoc.GetNextInline(startInline) ?? startInline; 92 | IsAtLineBreak = startInline != startInlineUpToLineBreak; 93 | } 94 | } 95 | 96 | return startInline!; 97 | 98 | } 99 | 100 | 101 | internal IEditable GetEndInline() 102 | { 103 | Paragraph? endPar = GetEndPar(); 104 | if (endPar == null) return null!; 105 | 106 | IEditable endInline = null!; 107 | 108 | //if (trange.BiasForwardStart && trange.Length == 0) 109 | if (BiasForwardStart) 110 | endInline = endPar.Inlines.LastOrDefault(ied => endPar.StartInDoc + ied.TextPositionOfInlineInParagraph <= End)!; 111 | else 112 | endInline = endPar.Inlines.LastOrDefault(ied => endPar.StartInDoc + ied.TextPositionOfInlineInParagraph < End)!; 113 | 114 | return endInline!; 115 | } 116 | 117 | 118 | public Paragraph? GetStartPar() 119 | { 120 | Paragraph? startPar = myFlowDoc.Blocks.LastOrDefault(b => b.IsParagraph && (b.StartInDoc <= Start))! as Paragraph; 121 | 122 | if (startPar != null) 123 | { 124 | //Check if start at end of last paragraph (cannot span from end of a paragraph) 125 | if (startPar != myFlowDoc.Blocks.Where(b => b.IsParagraph).Last() && startPar!.EndInDoc == Start) 126 | startPar = myFlowDoc.Blocks.FirstOrDefault(b => b.IsParagraph && myFlowDoc.Blocks.IndexOf(b) > myFlowDoc.Blocks.IndexOf(startPar))! as Paragraph; 127 | } 128 | 129 | return startPar; 130 | 131 | } 132 | 133 | public Paragraph? GetEndPar() 134 | { 135 | return myFlowDoc.Blocks.LastOrDefault(b => b.IsParagraph && b.StartInDoc < End)! as Paragraph; // less than to keep within emd of paragraph 136 | 137 | } 138 | 139 | public object? GetFormatting(AvaloniaProperty avProp) 140 | { 141 | object? formatting = null!; 142 | if (myFlowDoc == null) return null!; 143 | IEditable currentInline = GetStartInline(); 144 | if (currentInline != null) 145 | formatting = myFlowDoc.GetFormattingInline(avProp, currentInline); 146 | 147 | return formatting; 148 | } 149 | 150 | bool isFormatting = false; 151 | public void ApplyFormatting(AvaloniaProperty avProp, object value) 152 | { 153 | if (myFlowDoc == null) return; 154 | if (Length < 1) return; 155 | 156 | //try 157 | //{ 158 | // Debug.WriteLine("\napplying: " + (this.Text ?? "null")); 159 | //} 160 | //catch (Exception ex) { Debug.WriteLine("exception, length = " + this.Length + " :::start = " + this.Start + " ::: end= " + this.End + " :::" + ex.Message); } 161 | if (this.Text == "") return; 162 | 163 | myFlowDoc.ApplyFormattingRange(avProp, value, this); 164 | 165 | BiasForwardStart = false; 166 | BiasForwardEnd = false; 167 | 168 | } 169 | 170 | internal string GetText() 171 | { 172 | if (myFlowDoc == null) return ""; 173 | return myFlowDoc.GetText(this); 174 | } 175 | 176 | public string Text 177 | { 178 | get => GetText(); 179 | set => myFlowDoc.SetRangeToText(this, value); 180 | } 181 | 182 | public void Dispose() 183 | { 184 | Dispose(true); 185 | GC.SuppressFinalize(this); 186 | 187 | } 188 | 189 | private bool _disposed = false; 190 | protected virtual void Dispose(bool disposing) 191 | { 192 | if (_disposed) 193 | return; 194 | 195 | if (disposing) 196 | { 197 | StartParagraph = null!; 198 | EndParagraph = null!; 199 | 200 | Start_Changed = null; 201 | End_Changed = null; 202 | this.Start = 0; this.End = 0; 203 | myFlowDoc.TextRanges.Remove(this); 204 | myFlowDoc = null!; 205 | 206 | } 207 | _disposed = true; 208 | } 209 | 210 | 211 | 212 | } 213 | 214 | -------------------------------------------------------------------------------- /AvRichTextBox/Undo/IUndo.cs: -------------------------------------------------------------------------------- 1 | namespace AvRichTextBox; 2 | 3 | public interface IUndo 4 | { 5 | public void PerformUndo(); 6 | public int UndoEditOffset { get; } 7 | public bool UpdateTextRanges { get; } 8 | } 9 | 10 | internal class IEditablePropertyAssociation 11 | { 12 | internal IEditable InlineItem { get; set; } 13 | internal object PropertyValue { get; set; } 14 | internal FlowDocument.FormatRun? FormatRun { get; set; } 15 | 16 | internal IEditablePropertyAssociation(IEditable inlineItem, FlowDocument.FormatRun formatRun, object propertyValue) 17 | { 18 | InlineItem = inlineItem; 19 | FormatRun = formatRun; 20 | PropertyValue = propertyValue; 21 | } 22 | } -------------------------------------------------------------------------------- /AvRichTextBox/Undo/Undos.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls.Documents; 2 | using DynamicData; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | 7 | namespace AvRichTextBox; 8 | 9 | internal class InsertCharUndo (int insertParIndex, int insertInlineIdx, int insertPos, FlowDocument flowDoc, int origSelectionStart) : IUndo 10 | { 11 | public int UndoEditOffset => -1; 12 | public bool UpdateTextRanges => true; 13 | 14 | public void PerformUndo() 15 | { 16 | try 17 | { 18 | Paragraph thisPar = (Paragraph)flowDoc.Blocks[insertParIndex]; 19 | Run? thisRun = thisPar.Inlines[insertInlineIdx] as Run; 20 | thisRun!.Text = thisRun.Text!.Remove(insertPos, 1); 21 | thisPar.CallRequestInlinesUpdate(); 22 | flowDoc.UpdateBlockAndInlineStarts(insertParIndex); 23 | flowDoc.Selection.Start = origSelectionStart; 24 | flowDoc.Selection.End = flowDoc.Selection.Start; 25 | } 26 | catch { Debug.WriteLine("Failed InsertCharUndo at inline idx: " + insertInlineIdx); } 27 | 28 | } 29 | } 30 | 31 | internal class DeleteCharUndo(int deleteParIndex, int deleteInlineIdx, IEditable deletedRun, string deleteChar, int deletePos, FlowDocument flowDoc, int origSelectionStart) : IUndo 32 | { 33 | public int UndoEditOffset => 1; 34 | public bool UpdateTextRanges => true; 35 | internal int DeleteInlineIdx => deleteInlineIdx; 36 | 37 | public void PerformUndo() 38 | { 39 | try 40 | { 41 | Paragraph thisPar = (Paragraph)flowDoc.Blocks[deleteParIndex]; 42 | if (deletedRun != null) 43 | thisPar.Inlines.Insert(deleteInlineIdx, deletedRun); 44 | else 45 | { 46 | Run? thisRun = thisPar.Inlines[deleteInlineIdx] as Run; 47 | thisRun!.Text = thisRun.Text!.Insert(deletePos, deleteChar); 48 | } 49 | 50 | thisPar.CallRequestInlinesUpdate(); 51 | flowDoc.UpdateBlockAndInlineStarts(deleteParIndex); 52 | flowDoc.Selection.Start = origSelectionStart; 53 | flowDoc.Selection.End = flowDoc.Selection.Start; 54 | } 55 | catch { Debug.WriteLine("Failed DeleteCharUndo at delete pos: " + deletePos); } 56 | } 57 | 58 | } 59 | 60 | internal class DeleteImageUndo(int deleteParIndex, IEditable deletedIUC, int deletedInlineIdx, FlowDocument flowDoc, int origSelectionStart, bool emptyRunAdded) : IUndo 61 | { 62 | public int UndoEditOffset => 1; 63 | public bool UpdateTextRanges => true; 64 | 65 | public void PerformUndo() 66 | { 67 | try 68 | { 69 | Paragraph thisPar = (Paragraph)flowDoc.Blocks[deleteParIndex]; 70 | if (emptyRunAdded) 71 | thisPar.Inlines.RemoveAt(deletedInlineIdx); 72 | thisPar.Inlines.Insert(deletedInlineIdx, deletedIUC); 73 | thisPar.CallRequestInlinesUpdate(); 74 | flowDoc.UpdateBlockAndInlineStarts(deleteParIndex); 75 | flowDoc.Selection.Start = origSelectionStart; 76 | flowDoc.Selection.End = flowDoc.Selection.Start; 77 | } 78 | catch { Debug.WriteLine("Failed DeleteImageUndo at delete pos: " + origSelectionStart); } 79 | } 80 | 81 | } 82 | 83 | internal class PasteUndo(Dictionary> keptParsAndInlines, int parIndex, FlowDocument flowDoc, int origSelectionStart, int undoEditOffset) : IUndo 84 | { 85 | public int UndoEditOffset => undoEditOffset; 86 | public bool UpdateTextRanges => true; 87 | 88 | public void PerformUndo() 89 | { 90 | try 91 | { 92 | flowDoc.RestoreDeletedBlocks(keptParsAndInlines, parIndex); 93 | 94 | flowDoc.Selection.Start = 0; //??? why necessary for cursor? 95 | flowDoc.Selection.End = 0; 96 | flowDoc.Selection.Start = origSelectionStart; 97 | flowDoc.Selection.End = origSelectionStart; 98 | flowDoc.UpdateSelection(); 99 | } 100 | catch { Debug.WriteLine("Failed PasteUndo at OrigSelectionStart: " + origSelectionStart); } 101 | } 102 | } 103 | 104 | internal class DeleteRangeUndo (Dictionary> keptParsAndInlines, int parIndex, FlowDocument flowDoc, int origSelectionStart, int undoEditOffset) : IUndo 105 | { //parInlines are cloned inlines 106 | 107 | public int UndoEditOffset => undoEditOffset; 108 | public bool UpdateTextRanges => true; 109 | 110 | public void PerformUndo() 111 | { 112 | try 113 | { 114 | flowDoc.RestoreDeletedBlocks(keptParsAndInlines, parIndex); 115 | 116 | flowDoc.Selection.Start = 0; //??? why necessary for cursor? 117 | flowDoc.Selection.End = 0; 118 | flowDoc.Selection.Start = origSelectionStart; 119 | flowDoc.Selection.End = origSelectionStart; 120 | 121 | flowDoc.UpdateSelection(); 122 | } 123 | catch { Debug.WriteLine("Failed DeleteRangeUndo at ParIndex: " + parIndex); } 124 | } 125 | 126 | } 127 | 128 | 129 | internal class InsertParagraphUndo (FlowDocument flowDoc, int insertedParIndex, List keepParInlines, int origSelectionStart, int undoEditOffset) : IUndo 130 | { 131 | public int UndoEditOffset => undoEditOffset; 132 | public bool UpdateTextRanges => true; 133 | 134 | public void PerformUndo() 135 | { 136 | try 137 | { 138 | Paragraph insertedPar = (Paragraph)flowDoc.Blocks[insertedParIndex]; 139 | insertedPar.Inlines.Clear(); 140 | insertedPar.Inlines.AddRange(keepParInlines); 141 | flowDoc.Blocks.RemoveAt(insertedParIndex + 1); 142 | flowDoc.UpdateBlockAndInlineStarts(insertedParIndex); 143 | //flowDoc.MergeParagraphForward(insertedIndex, false, origSelectionStart); 144 | flowDoc.Selection.Start = origSelectionStart; 145 | flowDoc.Selection.End = flowDoc.Selection.Start; 146 | } 147 | catch { Debug.WriteLine("Failed InsertParagraphUndo at InsertedIndex: " + insertedParIndex); } 148 | 149 | } 150 | } 151 | 152 | internal class MergeParagraphUndo (int origMergedParInlinesCount, int mergedParIndex, Paragraph removedPar, FlowDocument flowDoc, int originalSelectionStart) : IUndo 153 | { //removedPar is a clone 154 | 155 | public int UndoEditOffset => 1; 156 | public bool UpdateTextRanges => false; 157 | 158 | public void PerformUndo() 159 | { 160 | try 161 | { 162 | //flowDoc.InsertParagraph(false, mergedCharIndex); 163 | 164 | Paragraph mergedPar = (Paragraph)flowDoc.Blocks[mergedParIndex]; 165 | 166 | for (int rno = mergedPar.Inlines.Count - 1; rno >= origMergedParInlinesCount; rno--) 167 | mergedPar.Inlines.RemoveAt(rno); 168 | 169 | flowDoc.Blocks.Insert(mergedParIndex + 1, removedPar); 170 | 171 | flowDoc.UpdateBlockAndInlineStarts(mergedParIndex); 172 | flowDoc.Selection.End = originalSelectionStart; 173 | flowDoc.Selection.Start = originalSelectionStart; 174 | 175 | } 176 | catch { Debug.WriteLine("Failed MergeParagraphUndo at MergedParIndex: " + mergedParIndex); } 177 | } 178 | } 179 | 180 | 181 | internal class ApplyFormattingUndo (FlowDocument flowDoc, List propertyAssociations, int originalSelection, TextRange tRange) : IUndo 182 | { 183 | public int UndoEditOffset => 0; 184 | public bool UpdateTextRanges => false; 185 | 186 | public void PerformUndo() 187 | { 188 | 189 | foreach (IEditablePropertyAssociation propassoc in propertyAssociations) 190 | if (propassoc.FormatRun != null) 191 | flowDoc.ApplyFormattingInline(propassoc.FormatRun, propassoc.InlineItem, propassoc.PropertyValue); 192 | 193 | foreach (Paragraph p in flowDoc.GetRangeBlocks(tRange).Where(b=>b.IsParagraph)) 194 | p.CallRequestInlinesUpdate(); 195 | 196 | flowDoc.Selection.Start = originalSelection; 197 | flowDoc.Selection.End = originalSelection; 198 | 199 | } 200 | } 201 | 202 | 203 | -------------------------------------------------------------------------------- /AvRichTextBox/UniqueBitmap.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.Documents; 3 | using Avalonia.Media.Imaging; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace AvRichTextBox; 9 | 10 | public class UniqueBitmap(Bitmap ubmap, int w, int h, int cIndex) 11 | { 12 | internal Bitmap uBitmap = ubmap; 13 | internal int maxWidth = w; 14 | internal int maxHeight = h; 15 | internal int consecutiveIndex = cIndex; 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /AvRichTextBox/VisualHelper.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Styling; 3 | using Avalonia.VisualTree; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | 8 | namespace AvRichTextBox; 9 | 10 | public static class VisualHelper 11 | { 12 | internal static double GetUserY(this Visual? visual) 13 | { 14 | if (visual == null || !visual.IsVisible) 15 | return 0; 16 | 17 | Rect vRect = visual!.GetTransformedBounds()!.Value.Clip; 18 | 19 | return vRect.Y; 20 | 21 | } 22 | 23 | 24 | internal static Dictionary GetVisibleEditableParagraphs(this Visual? itemsControl) // where T : Visual 25 | { 26 | var items = new Dictionary(); 27 | 28 | if (itemsControl == null || !itemsControl.IsVisible) 29 | return items; 30 | 31 | var edPars = itemsControl.GetVisualDescendants().OfType(); 32 | foreach (var edpar in edPars) 33 | { 34 | Rect vRect = edpar.GetTransformedBounds()!.Value.Clip; 35 | //Rect vRect = edpar.Bounds; 36 | 37 | if (vRect.Y > 0) // Greater than zero means it's visible 38 | items.Add(edpar, edpar.GetTransformedBounds()!.Value.Clip); 39 | //items.Add(edpar, edpar.GetTransformedBounds()!.Value.Bounds); 40 | } 41 | 42 | 43 | return items; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Data.Core; 4 | using Avalonia.Data.Core.Plugins; 5 | using Avalonia.Markup.Xaml; 6 | using DemoApp_AvRichtextBox.ViewModels; 7 | using DemoApp_AvRichtextBox.Views; 8 | using System.Linq; 9 | 10 | namespace DemoApp_AvRichtextBox; 11 | 12 | public partial class App : Application 13 | { 14 | public override void Initialize() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | 19 | public override void OnFrameworkInitializationCompleted() 20 | { 21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 22 | { 23 | // Avoid duplicate validations from both Avalonia and the CommunityToolkit. 24 | // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins 25 | DisableAvaloniaDataAnnotationValidation(); 26 | desktop.MainWindow = new MainWindow 27 | { 28 | DataContext = new MainWindowViewModel(), 29 | }; 30 | } 31 | 32 | base.OnFrameworkInitializationCompleted(); 33 | } 34 | 35 | private void DisableAvaloniaDataAnnotationValidation() 36 | { 37 | // Get an array of plugins to remove 38 | var dataValidationPluginsToRemove = 39 | BindingPlugins.DataValidators.OfType().ToArray(); 40 | 41 | // remove each entry found 42 | foreach (var plugin in dataValidationPluginsToRemove) 43 | { 44 | BindingPlugins.DataValidators.Remove(plugin); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuikp/AvRichTextBox/145e48b7aecf1b896dc4a5fd573061b5bcd449d9/DemoApp_AvRichtextBox/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/DemoApp_AvRichTextBox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net8.0 5 | enable 6 | true 7 | app.manifest 8 | 9 | 10 | 11 | True 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | Never 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | None 51 | All 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/NumericSpinner/NumericSpinner.axaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 51 | 55 | 56 | 57 | 58 | 59 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/NumericSpinner/NumericSpinner.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Data; 4 | using Avalonia.Data.Converters; 5 | using Avalonia.Input; 6 | using Avalonia.Interactivity; 7 | using Avalonia.Media; 8 | using Avalonia.Threading; 9 | using AvRichTextBox; 10 | using System; 11 | using System.ComponentModel; 12 | using System.Globalization; 13 | using System.Runtime.CompilerServices; 14 | 15 | namespace CustomControls; 16 | 17 | public class NumericSpinnerViewModel 18 | { 19 | 20 | } 21 | 22 | public partial class NumericSpinner : UserControl 23 | { 24 | public delegate void ValueChangedHandler(double value); 25 | public event ValueChangedHandler? ValueChanged; 26 | 27 | public delegate void UserValueChangedHandler(double value); 28 | public event UserValueChangedHandler? UserValueChanged; 29 | 30 | public NumericSpinner() 31 | { 32 | InitializeComponent(); 33 | 34 | this.DataContext = this; 35 | 36 | Step = 1; 37 | 38 | DownKeyTimer.Tick += DownKeyTimer_Tick; 39 | DownKeyTimer.Interval = new TimeSpan(0, 0, 0, 0, 200); 40 | 41 | 42 | CmdUp.AddHandler(InputElement.PointerPressedEvent, CmdUp_PointerPressed, RoutingStrategies.Tunnel); 43 | CmdUp.AddHandler(InputElement.PointerReleasedEvent, CmdUp_PointerReleased, RoutingStrategies.Tunnel); 44 | CmdDown.AddHandler(InputElement.PointerPressedEvent, CmdDown_PointerPressed, RoutingStrategies.Tunnel); 45 | CmdDown.AddHandler(InputElement.PointerReleasedEvent, CmdDown_PointerReleased, RoutingStrategies.Tunnel); 46 | 47 | Value = 20; 48 | 49 | } 50 | 51 | private double _Step = 1; 52 | public double Step { get { return _Step; } set { _Step = value; } } 53 | 54 | private void CmdUp_Click(object? sender, RoutedEventArgs e) { Value += (ControlIsDown ? Step * 10 : Step); UserValueChanged?.Invoke(Value); } 55 | private void CmdDown_Click(object? sender, RoutedEventArgs e) { Value -= (ControlIsDown ? Step * 10 : Step); UserValueChanged?.Invoke(Value); } 56 | 57 | internal DispatcherTimer DownKeyTimer = new(); 58 | 59 | bool ControlIsDown = false; 60 | bool UpKeyDown = false; 61 | bool DownKeyDown = false; 62 | 63 | private void DownKeyTimer_Tick(object? sender, EventArgs e) 64 | { 65 | int fac = DownKeyDown ? -1 : 1; 66 | Value += (ControlIsDown ? Step * 10 : Step) * fac; 67 | UserValueChanged?.Invoke(Value); 68 | } 69 | 70 | private void CmdDown_PointerPressed(object? sender, PointerPressedEventArgs e) 71 | { 72 | DownKeyDown = true; 73 | ControlIsDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); 74 | DownKeyTimer.Start(); 75 | } 76 | 77 | private void CmdUp_PointerPressed(object? sender, PointerPressedEventArgs e) 78 | { 79 | UpKeyDown = true; 80 | ControlIsDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); 81 | DownKeyTimer.Start(); 82 | } 83 | 84 | private void CmdDown_PointerReleased(object? sender, PointerReleasedEventArgs e) { DownKeyDown = false; DownKeyTimer.Stop(); } 85 | private void CmdUp_PointerReleased(object? sender, PointerReleasedEventArgs e) { UpKeyDown = false; DownKeyTimer.Stop(); ; } 86 | 87 | public static readonly StyledProperty TextBackgroundProperty = AvaloniaProperty.Register(nameof(TextBackground)); 88 | 89 | public SolidColorBrush TextBackground 90 | { 91 | get => (SolidColorBrush)GetValue(TextBackgroundProperty); 92 | set => SetValue(TextBackgroundProperty, value); 93 | } 94 | 95 | public static readonly StyledProperty TextPaddingProperty = AvaloniaProperty.Register(nameof(TextPadding)); 96 | 97 | public Thickness TextPadding 98 | { 99 | get => (Thickness)GetValue(TextPaddingProperty); 100 | set => SetValue(TextPaddingProperty, value); 101 | } 102 | 103 | public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register(nameof(Value), 80, defaultBindingMode: BindingMode.TwoWay); 104 | 105 | public double Value 106 | { 107 | get => (double)GetValue(ValueProperty); 108 | set 109 | { 110 | if (value < MinValue) value = MinValue; 111 | if (value > MaxValue) value = MaxValue; 112 | 113 | if (Value != value) 114 | { 115 | SetValue(ValueProperty, value); 116 | ValueChanged?.Invoke(value); 117 | } 118 | 119 | } 120 | } 121 | 122 | protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) 123 | { 124 | base.OnPropertyChanged(change); 125 | 126 | //if (change.Property.Name == nameof(Value)) 127 | //{ 128 | // double Val = (double)change.GetNewValue(); 129 | // ValueChanged?.Invoke(Val); 130 | //} 131 | 132 | } 133 | 134 | public static readonly StyledProperty MaxValueProperty = AvaloniaProperty.Register(nameof(MaxValue)); 135 | 136 | public double MaxValue 137 | { 138 | get { return (double)GetValue(MaxValueProperty); } 139 | set { SetValue(MaxValueProperty, value); } 140 | } 141 | 142 | private void MaxValuePropertyChanged(double Val) { MaxValue = Val; } 143 | 144 | public static readonly StyledProperty MinValueProperty = AvaloniaProperty.Register(nameof(MinValue)); 145 | 146 | public double MinValue 147 | { 148 | get { return (double)GetValue(MinValueProperty); } 149 | set { SetValue(MinValueProperty, value); } 150 | } 151 | 152 | private void MinValuePropertyChanged(double Val) { MinValue = Val; } 153 | 154 | 155 | } 156 | 157 | public class DoubleToStringConverter : IValueConverter 158 | { 159 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 160 | { 161 | return value?.ToString() ?? ""; 162 | } 163 | 164 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 165 | { 166 | if (double.TryParse(value as string, out double result)) 167 | return result; 168 | return 0; // Default fallback 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace DemoApp_AvRichtextBox; 5 | 6 | internal sealed class Program 7 | { 8 | // Initialization code. Don't use any Avalonia, third-party APIs or any 9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 10 | // yet and stuff might break. 11 | [STAThread] 12 | public static void Main(string[] args) => BuildAvaloniaApp() 13 | .StartWithClassicDesktopLifetime(args); 14 | 15 | // Avalonia configuration, don't remove; also used by visual designer. 16 | public static AppBuilder BuildAvaloniaApp() 17 | => AppBuilder.Configure() 18 | .UsePlatformDetect() 19 | .WithInterFont() 20 | .LogToTrace(); 21 | } 22 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/TestFiles/TestDocumentWord.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuikp/AvRichTextBox/145e48b7aecf1b896dc4a5fd573061b5bcd449d9/DemoApp_AvRichtextBox/TestFiles/TestDocumentWord.docx -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/TestFiles/TestDocumentXamlPackage.xamlp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cuikp/AvRichTextBox/145e48b7aecf1b896dc4a5fd573061b5bcd449d9/DemoApp_AvRichtextBox/TestFiles/TestDocumentXamlPackage.xamlp -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using DemoApp_AvRichtextBox.ViewModels; 4 | using System; 5 | 6 | namespace DemoApp_AvRichtextBox; 7 | 8 | public class ViewLocator : IDataTemplate 9 | { 10 | 11 | public Control? Build(object? param) 12 | { 13 | if (param is null) 14 | return null; 15 | 16 | var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); 17 | var type = Type.GetType(name); 18 | 19 | if (type != null) 20 | { 21 | return (Control)Activator.CreateInstance(type)!; 22 | } 23 | 24 | return new TextBlock { Text = "Not Found: " + name }; 25 | } 26 | 27 | public bool Match(object? data) 28 | { 29 | return data is ViewModelBase; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 |  2 | using AvRichTextBox; 3 | using DocumentFormat.OpenXml.Spreadsheet; 4 | using System; 5 | using System.Collections.ObjectModel; 6 | using System.ComponentModel; 7 | using System.IO; 8 | using System.Runtime.CompilerServices; 9 | 10 | namespace DemoApp_AvRichtextBox.ViewModels; 11 | 12 | public partial class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged 13 | { 14 | public new event PropertyChangedEventHandler? PropertyChanged; 15 | public void NotifyPropertyChanged([CallerMemberName] String propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } 16 | 17 | 18 | private string _CurrentOpenFilePath = Path.Combine(AppContext.BaseDirectory, "TestFiles", "NewFile"); 19 | public string CurrentOpenFilePath { get => _CurrentOpenFilePath; set { _CurrentOpenFilePath = value; NotifyPropertyChanged(nameof(CurrentOpenFileNameExt)); } } 20 | 21 | internal string CurrentOpenFileName => Path.GetFileNameWithoutExtension(CurrentOpenFilePath); 22 | string CurrentExt => Path.GetExtension(CurrentOpenFilePath); 23 | public string CurrentOpenFileNameExt => "DemoApp_AvRichTextBox - " + CurrentOpenFileName + CurrentExt; 24 | 25 | public FlowDocument MyFlowDoc { get; set; } = new FlowDocument(); 26 | //public ObservableCollection MyBlocks { get; set; } = []; 27 | 28 | public MainWindowViewModel() 29 | { 30 | //Testing creation a flow doc for binding: 31 | //Paragraph p = new(); 32 | //p.Inlines.Add(new EditableRun("This is a fixxxxxrst inline")); 33 | 34 | //MyFlowDoc.Blocks.Clear(); 35 | //MyFlowDoc.Blocks.Add(p); 36 | 37 | //MyBlocks.Add(p); 38 | 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace DemoApp_AvRichtextBox.ViewModels; 4 | 5 | public class ViewModelBase : ObservableObject 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Media; 4 | using AvRichTextBox; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace DemoApp_AvRichtextBox.Views; 13 | 14 | public partial class MainWindow : Window 15 | { 16 | 17 | public static List GetAllFonts 18 | { 19 | get 20 | { 21 | List returnList = []; 22 | foreach (var font in FontManager.Current.SystemFonts) 23 | returnList.Add(font.Name); 24 | return returnList; 25 | } 26 | 27 | } 28 | 29 | public MainWindow() 30 | { 31 | InitializeComponent(); 32 | 33 | Loaded += MainWindow_Loaded; 34 | 35 | FontsCB.ItemsSource = GetAllFonts; 36 | 37 | } 38 | 39 | private void MainWindow_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 40 | { 41 | MainRTB.FlowDocument.Selection_Changed += FlowDocument_Selection_Changed; 42 | 43 | progChange = false; 44 | 45 | ////Temp debugging 46 | //string testLocation = Path.Combine(AppContext.BaseDirectory, "TestFiles"); 47 | //MainRTB.LoadWordDoc(Path.Combine(testLocation, "TestDocumentWord.docx")); 48 | 49 | } 50 | 51 | private void CreateNewDocumentMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 52 | { 53 | MainRTB.CloseDocument(); 54 | 55 | } 56 | 57 | private void ShowPagePaddingValue() 58 | { 59 | PagePaddingNSL.Value = MainRTB.FlowDocument.PagePadding.Left; 60 | PagePaddingNSR.Value = MainRTB.FlowDocument.PagePadding.Right; 61 | PagePaddingNST.Value = MainRTB.FlowDocument.PagePadding.Top; 62 | PagePaddingNSB.Value = MainRTB.FlowDocument.PagePadding.Bottom; 63 | } 64 | 65 | 66 | private void FindTextBox_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e) 67 | { 68 | FindTB.Background = Brushes.White; 69 | 70 | if (e.Key == Avalonia.Input.Key.Enter) 71 | { 72 | PerformFind(); 73 | e.Handled = true; 74 | } 75 | } 76 | 77 | private void FindTextBox_GotFocus(object? sender, Avalonia.Input.GotFocusEventArgs e) 78 | { 79 | FindTB.Background = Brushes.White; 80 | 81 | } 82 | 83 | private void FindButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 84 | { 85 | PerformFind(); 86 | 87 | } 88 | 89 | private void PerformFind() 90 | { 91 | FindTB.Background = Brushes.White; 92 | 93 | if (string.IsNullOrEmpty(FindTB.Text)) return; 94 | 95 | MatchCollection foundMatches = Regex.Matches(MainRTB.FlowDocument.Text, FindTB.Text); 96 | Match? firstMatch = foundMatches.FirstOrDefault(m => m.Index >= MainRTB.FlowDocument.Selection.End); 97 | if (firstMatch != null) 98 | { 99 | MainRTB.FlowDocument.Select(firstMatch.Index, FindTB.Text.Length); 100 | MainRTB.ScrollToSelection(); 101 | } 102 | else 103 | { 104 | FindTB.Background = Brushes.Coral; 105 | FindBut.Focus(); 106 | } 107 | 108 | 109 | } 110 | 111 | internal void PagePaddingNSL_ValueChanged(double value) 112 | { 113 | Thickness p = MainRTB.FlowDocument.PagePadding; 114 | MainRTB.FlowDocument.PagePadding = new Thickness(PagePaddingNSL.Value, p.Top, p.Right, p.Bottom); 115 | } 116 | 117 | internal void PagePaddingNST_ValueChanged(double value) 118 | { 119 | Thickness p = MainRTB.FlowDocument.PagePadding; 120 | MainRTB.FlowDocument.PagePadding = new Thickness(p.Left, PagePaddingNST.Value, p.Right, p.Bottom); 121 | } 122 | 123 | internal void PagePaddingNSR_ValueChanged(double value) 124 | { 125 | Thickness p = MainRTB.FlowDocument.PagePadding; 126 | MainRTB.FlowDocument.PagePadding = new Thickness(p.Left, p.Top, PagePaddingNSR.Value, p.Bottom); 127 | } 128 | 129 | internal void PagePaddingNSB_ValueChanged(double value) 130 | { 131 | Thickness p = MainRTB.FlowDocument.PagePadding; 132 | MainRTB.FlowDocument.PagePadding = new Thickness(p.Left, p.Top, p.Right, PagePaddingNSB.Value); 133 | } 134 | 135 | 136 | bool progChange = true; 137 | 138 | private void FlowDocument_Selection_Changed(TextRange selection) 139 | { 140 | FontSizeNS.Value = Math.Round((double)(selection.GetFormatting(FontSizeProperty) ?? 14D)); 141 | 142 | object? selFFP = selection.GetFormatting(FontFamilyProperty); 143 | if (selFFP != null) 144 | { 145 | FontFamily ffamily = (FontFamily)selFFP; 146 | FontsCB.SelectedItem = ffamily.ToString(); 147 | } 148 | 149 | Paragraph? selPar = selection.GetStartPar(); 150 | if (selPar != null && !progChange) 151 | { 152 | progChange = true; 153 | LineSpacingNS.Value = selPar.LineSpacing; 154 | ParagraphBorderNS.Value = selPar.BorderThickness.Left; 155 | ParBorderCP.Color = selPar.BorderBrush.Color; 156 | ParBackgroundCP.Color = selPar.Background.Color; 157 | progChange = false; 158 | } 159 | 160 | } 161 | 162 | internal void FontSizeNS_UserValueChanged(double value) 163 | { 164 | MainRTB.FlowDocument.Selection.ApplyFormatting(FontSizeProperty, value); 165 | 166 | } 167 | 168 | internal void LineSpacingNS_UserValueChanged(double value) 169 | { 170 | foreach (Paragraph p in MainRTB.FlowDocument.GetSelectedParagraphs) 171 | p.LineSpacing = value; 172 | } 173 | 174 | internal void ParagraphBorderNS_UserValueChanged(double value) 175 | { 176 | foreach (Paragraph p in MainRTB.FlowDocument.GetSelectedParagraphs) 177 | p.BorderThickness = new Thickness(value); 178 | 179 | } 180 | 181 | private void ParBorder_ColorChanged(object? sender, ColorChangedEventArgs e) 182 | { 183 | if (progChange) return; 184 | SolidColorBrush hBrush = new(e.NewColor); 185 | foreach (Paragraph p in MainRTB.FlowDocument.GetSelectedParagraphs) 186 | p.BorderBrush = hBrush; 187 | 188 | } 189 | 190 | private void ParBackground_ColorChanged(object? sender, ColorChangedEventArgs e) 191 | { 192 | if (progChange) return; 193 | SolidColorBrush hBrush = new(e.NewColor); 194 | foreach (Paragraph p in MainRTB.FlowDocument.GetSelectedParagraphs) 195 | p.Background = hBrush; 196 | 197 | } 198 | 199 | 200 | private void FontCP_ColorChanged(object? sender, ColorChangedEventArgs e) 201 | { 202 | SolidColorBrush hBrush = new (e.NewColor); 203 | MainRTB.FlowDocument.Selection.ApplyFormatting(ForegroundProperty, hBrush); 204 | } 205 | 206 | private void HighlightCP_ColorChanged(object? sender, ColorChangedEventArgs e) 207 | { 208 | SolidColorBrush hBrush = new (e.NewColor); 209 | MainRTB.FlowDocument.Selection.ApplyFormatting(BackgroundProperty, hBrush); 210 | } 211 | 212 | private void DebugPanelCB_CheckedUnchecked(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 213 | { 214 | CheckBox? thisCB = sender as CheckBox; 215 | if (thisCB != null && MainRTB != null) 216 | //MainRTB.ToggleDebuggerPanel((bool)thisCB.IsChecked!); 217 | MainRTB.ShowDebuggerPanelInDebugMode = (bool)thisCB.IsChecked!; 218 | } 219 | 220 | private void FontsComboBox_DropDownClosed(object? sender, System.EventArgs e) 221 | { 222 | ComboBox? comboBox = sender as ComboBox; 223 | if (comboBox != null && comboBox.SelectedItem != null) 224 | { 225 | string? newFont = comboBox.SelectedItem.ToString(); 226 | if (newFont != null) 227 | MainRTB.FlowDocument.Selection.ApplyFormatting(FontFamilyProperty, new FontFamily(newFont)); 228 | } 229 | 230 | } 231 | } -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/Views/MainWindow_File.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Platform.Storage; 3 | using DemoApp_AvRichtextBox.ViewModels; 4 | using System; 5 | using System.IO; 6 | 7 | namespace DemoApp_AvRichtextBox.Views; 8 | 9 | public partial class MainWindow : Window 10 | { 11 | string openFilePath 12 | { 13 | get { return ((MainWindowViewModel)DataContext!).CurrentOpenFilePath; } 14 | set { ((MainWindowViewModel)DataContext!).CurrentOpenFilePath = value; } 15 | } 16 | 17 | string openFileName => ((MainWindowViewModel)DataContext!).CurrentOpenFileName; 18 | 19 | private async void LoadXamlPackageMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 20 | { 21 | 22 | FilePickerOpenOptions filePickerOptions = new() 23 | { 24 | Title = "Open Xaml package file", 25 | SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Path.Combine(AppContext.BaseDirectory, "TestFiles")), 26 | FileTypeFilter = [new("Xaml package files") { Patterns = ["*.xamlp"] }], 27 | AllowMultiple = false 28 | }; 29 | 30 | var topLevel = TopLevel.GetTopLevel(this); 31 | var files = await topLevel!.StorageProvider.OpenFilePickerAsync(filePickerOptions); 32 | 33 | if (files.Count == 1) 34 | { 35 | string? f = files[0].TryGetLocalPath(); 36 | if (f != null) 37 | { 38 | openFilePath = f; 39 | MainRTB.LoadXamlPackage(f); 40 | ShowPagePaddingValue(); 41 | } 42 | 43 | 44 | } 45 | } 46 | 47 | 48 | private void SaveXamlPackageMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 49 | { 50 | MainRTB.SaveXamlPackage(Path.Combine(Path.GetDirectoryName(openFilePath)!, openFileName + ".xamlp")); 51 | } 52 | 53 | private async void LoadRtfFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 54 | { 55 | FilePickerOpenOptions filePickerOptions = new() 56 | { 57 | Title = "Open Rtf file", 58 | SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Path.Combine(AppContext.BaseDirectory, "TestFiles")), 59 | FileTypeFilter = [new("Rtf files") { Patterns = ["*.rtf"] }], 60 | AllowMultiple = false 61 | }; 62 | 63 | var topLevel = TopLevel.GetTopLevel(this); 64 | var files = await topLevel!.StorageProvider.OpenFilePickerAsync(filePickerOptions); 65 | 66 | if (files.Count == 1) 67 | { 68 | string? f = files[0].TryGetLocalPath(); 69 | if (f != null) 70 | { 71 | openFilePath = f; 72 | MainRTB.LoadRtfDoc(f); 73 | ShowPagePaddingValue(); 74 | } 75 | } 76 | } 77 | 78 | private void SaveRtfFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 79 | { 80 | MainRTB.SaveRtfDoc(Path.Combine(Path.GetDirectoryName(openFilePath)!, openFileName + ".rtf")); 81 | } 82 | 83 | 84 | private async void LoadWordFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 85 | { 86 | 87 | FilePickerOpenOptions filePickerOptions = new() 88 | { 89 | Title = "Open Word doc file", 90 | SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Path.Combine(AppContext.BaseDirectory, "TestFiles")), 91 | FileTypeFilter = [new("Word doc files") { Patterns = ["*.docx"], }], 92 | AllowMultiple = false 93 | }; 94 | 95 | 96 | var topLevel = TopLevel.GetTopLevel(this); 97 | var files = await topLevel!.StorageProvider.OpenFilePickerAsync(filePickerOptions); 98 | 99 | if (files.Count == 1) 100 | { 101 | string? f = files[0].TryGetLocalPath(); 102 | if (f != null) 103 | { 104 | openFilePath = f; 105 | MainRTB.LoadWordDoc(f); 106 | ShowPagePaddingValue(); 107 | } 108 | 109 | } 110 | 111 | } 112 | 113 | private void SaveWordFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 114 | { 115 | MainRTB.SaveWordDoc(Path.Combine(Path.GetDirectoryName(openFilePath)!, openFileName + ".docx")); 116 | } 117 | 118 | private void SaveHtmlFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 119 | { 120 | MainRTB.SaveHtmlDoc(Path.Combine(Path.GetDirectoryName(openFilePath)!, openFileName + ".html")); 121 | } 122 | 123 | private async void LoadHtmlFileMenuItem_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) 124 | { 125 | FilePickerOpenOptions filePickerOptions = new() 126 | { 127 | Title = "Open Html file", 128 | SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Path.Combine(AppContext.BaseDirectory, "TestFiles")), 129 | FileTypeFilter = [new("Html files") { Patterns = ["*.html"], }], 130 | AllowMultiple = false 131 | }; 132 | 133 | var topLevel = TopLevel.GetTopLevel(this); 134 | var files = await topLevel!.StorageProvider.OpenFilePickerAsync(filePickerOptions); 135 | 136 | if (files.Count == 1) 137 | { 138 | string? f = files[0].TryGetLocalPath(); 139 | if (f != null) 140 | { 141 | openFilePath = f; 142 | MainRTB.LoadHtmlDoc(f); 143 | ShowPagePaddingValue(); 144 | } 145 | 146 | } 147 | 148 | } 149 | 150 | } -------------------------------------------------------------------------------- /DemoApp_AvRichtextBox/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | enable 4 | 11.0.2 5 | 6 | 7 | --------------------------------------------------------------------------------