PageRenderTime 61ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/MarkPad/Document/Controls/MarkdownEditor.xaml.cs

https://github.com/bcott/DownmarkerWPF
C# | 377 lines | 299 code | 66 blank | 12 comment | 48 complexity | 8c38c03edb715364cf92dcdb207ba8e8 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Text.RegularExpressions;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Input;
  10. using System.Xml;
  11. using Caliburn.Micro;
  12. using ICSharpCode.AvalonEdit;
  13. using ICSharpCode.AvalonEdit.Document;
  14. using ICSharpCode.AvalonEdit.Highlighting;
  15. using ICSharpCode.AvalonEdit.Highlighting.Xshd;
  16. using ICSharpCode.AvalonEdit.Rendering;
  17. using MarkPad.Document.Commands;
  18. using MarkPad.Document.EditorBehaviours;
  19. using MarkPad.Document.Events;
  20. using MarkPad.Document.SpellCheck;
  21. using MarkPad.Framework;
  22. using MarkPad.Settings.Models;
  23. namespace MarkPad.Document.Controls
  24. {
  25. public partial class MarkdownEditor
  26. {
  27. const int NumSpaces = 4;
  28. const string Spaces = " ";
  29. readonly IEnumerable<IHandle<EditorPreviewKeyDownEvent>> editorPreviewKeyDownHandlers;
  30. readonly IEnumerable<IHandle<EditorTextEnteringEvent>> editorTextEnteringHandlers;
  31. public MarkdownEditor()
  32. {
  33. InitializeComponent();
  34. NameScope.SetNameScope(EditorContextMenu, NameScope.GetNameScope(this));
  35. Editor.TextArea.SelectionChanged += SelectionChanged;
  36. Editor.PreviewMouseLeftButtonUp += HandleMouseUp;
  37. Editor.MouseMove += HandleEditorMouseMove;
  38. Editor.PreviewMouseLeftButtonDown += HandleEditorPreviewMouseLeftButtonDown;
  39. Editor.MouseMove += (s, e) => e.Handled = true;
  40. Editor.TextArea.TextEntering += TextAreaTextEntering;
  41. CommandBindings.Add(new CommandBinding(FormattingCommands.ToggleBold, (x, y) => ToggleBold(), CanEditDocument));
  42. CommandBindings.Add(new CommandBinding(FormattingCommands.ToggleItalic, (x, y) => ToggleItalic(), CanEditDocument));
  43. CommandBindings.Add(new CommandBinding(FormattingCommands.ToggleCode, (x, y) => ToggleCode(), CanEditDocument));
  44. CommandBindings.Add(new CommandBinding(FormattingCommands.ToggleCodeBlock, (x, y) => ToggleCodeBlock(), CanEditDocument));
  45. CommandBindings.Add(new CommandBinding(FormattingCommands.SetHyperlink, (x, y) => SetHyperlink(), CanEditDocument));
  46. var overtypeMode = new OvertypeMode();
  47. editorPreviewKeyDownHandlers = new IHandle<EditorPreviewKeyDownEvent>[] {
  48. new CopyLeadingWhitespaceOnNewLine(),
  49. new PasteImageIntoDocument(),
  50. new PasteURLIntoDocument(),
  51. new ControlRightTweakedForMarkdown(),
  52. new HardLineBreak(),
  53. overtypeMode,
  54. new AutoContinueLists(),
  55. new IndentLists(()=>IndentType)
  56. };
  57. editorTextEnteringHandlers = new IHandle<EditorTextEnteringEvent>[] {
  58. overtypeMode
  59. };
  60. }
  61. #region public IndentType IndentType
  62. public static readonly DependencyProperty IndentTypeProperty =
  63. DependencyProperty.Register("IndentType", typeof (IndentType), typeof (MarkdownEditor), new PropertyMetadata(IndentType.Spaces));
  64. public IndentType IndentType
  65. {
  66. get { return (IndentType) GetValue(IndentTypeProperty); }
  67. set { SetValue(IndentTypeProperty, value); }
  68. }
  69. #endregion
  70. #region public TextDocument Document
  71. public static readonly DependencyProperty DocumentProperty =
  72. DependencyProperty.Register("Document", typeof(TextDocument), typeof(MarkdownEditor), new PropertyMetadata(default(TextDocument)));
  73. public TextDocument Document
  74. {
  75. get { return (TextDocument)GetValue(DocumentProperty); }
  76. set { SetValue(DocumentProperty, value); }
  77. }
  78. #endregion
  79. #region public bool FloatingToolbarEnabled
  80. public static readonly DependencyProperty FloatingToolbarEnabledProperty =
  81. DependencyProperty.Register("FloatingToolbarEnabled", typeof (bool), typeof (MarkdownEditor), new PropertyMetadata(default(bool)));
  82. public bool FloatingToolbarEnabled
  83. {
  84. get { return (bool)GetValue(FloatingToolbarEnabledProperty); }
  85. set { SetValue(FloatingToolbarEnabledProperty, value); }
  86. }
  87. #endregion
  88. #region public double EditorFontSize
  89. public static DependencyProperty EditorFontSizeProperty = DependencyProperty.Register("EditorFontSize", typeof (double), typeof (MarkdownEditor),
  90. new PropertyMetadata(default(double), EditorFontSizeChanged));
  91. public double EditorFontSize
  92. {
  93. get { return (double) GetValue(EditorFontSizeProperty); }
  94. set { SetValue(EditorFontSizeProperty, value); }
  95. }
  96. #endregion
  97. private static void EditorFontSizeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
  98. {
  99. ((MarkdownEditor) dependencyObject).Editor.FontSize = (double)dependencyPropertyChangedEventArgs.NewValue;
  100. }
  101. void EditorLoaded(object sender, RoutedEventArgs e)
  102. {
  103. if (DesignerProperties.GetIsInDesignMode(this)) return;
  104. using (var stream = Assembly.GetEntryAssembly().GetManifestResourceStream("MarkPad.Syntax.Markdown.xshd"))
  105. using (var reader = new XmlTextReader(stream))
  106. {
  107. Editor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
  108. }
  109. // AvalonEdit hijacks Ctrl+I. We need to free that mutha up
  110. var editCommandBindings = Editor.TextArea.DefaultInputHandler.Editing.CommandBindings;
  111. editCommandBindings
  112. .FirstOrDefault(b => b.Command == AvalonEditCommands.IndentSelection)
  113. .ExecuteSafely(b => editCommandBindings.Remove(b));
  114. Editor.Focus();
  115. }
  116. void SelectionChanged(object sender, EventArgs e)
  117. {
  118. if (!FloatingToolbarEnabled) return;
  119. if (Editor.TextArea.Selection.IsEmpty)
  120. floatingToolBar.Hide();
  121. else
  122. ShowFloatingToolBar();
  123. }
  124. private void HandleMouseUp(object sender, MouseButtonEventArgs e)
  125. {
  126. if (!FloatingToolbarEnabled)
  127. return;
  128. if (Editor.TextArea.Selection.IsEmpty)
  129. floatingToolBar.Hide();
  130. else
  131. ShowFloatingToolBar();
  132. }
  133. void HandleEditorMouseMove(object sender, MouseEventArgs e)
  134. {
  135. // Bail out if tool bar is disabled, if there is no selection, or if the toolbar is already open
  136. if (!FloatingToolbarEnabled) return;
  137. if (string.IsNullOrEmpty(Editor.SelectedText)) return;
  138. if (floatingToolBar.IsOpen) return;
  139. if (e.LeftButton == MouseButtonState.Pressed) return;
  140. // Bail out if the mouse isn't over the markdownEditor
  141. var editorPosition = Editor.GetPositionFromPoint(e.GetPosition(Editor));
  142. if (!editorPosition.HasValue) return;
  143. // Bail out if the mouse isn't over a selection
  144. var offset = Editor.Document.GetOffset(editorPosition.Value.Line, editorPosition.Value.Column);
  145. if (offset < Editor.SelectionStart) return;
  146. if (offset > Editor.SelectionStart + Editor.SelectionLength) return;
  147. ShowFloatingToolBar();
  148. }
  149. void HandleEditorPreviewMouseLeftButtonDown(object sender, MouseEventArgs e)
  150. {
  151. if (!floatingToolBar.IsOpen) return;
  152. floatingToolBar.Hide();
  153. }
  154. private void ShowFloatingToolBar()
  155. {
  156. // Find the screen position of the start of the selection
  157. var selectionStartLocation = Editor.Document.GetLocation(Editor.SelectionStart);
  158. var selectionStartPosition = new TextViewPosition(selectionStartLocation);
  159. var selectionStartPoint = Editor.TextArea.TextView.GetVisualPosition(selectionStartPosition, VisualYPosition.LineTop);
  160. var popupPoint = new Point(
  161. selectionStartPoint.X + 30,
  162. selectionStartPoint.Y - 35);
  163. floatingToolBar.Show(Editor, popupPoint);
  164. }
  165. void EditorPreviewKeyDown(object sender, KeyEventArgs e)
  166. {
  167. foreach (var handler in editorPreviewKeyDownHandlers)
  168. {
  169. handler.Handle(new EditorPreviewKeyDownEvent(DataContext as DocumentViewModel, Editor, e));
  170. }
  171. }
  172. void TextAreaTextEntering(object sender, TextCompositionEventArgs e)
  173. {
  174. foreach (var handler in editorTextEnteringHandlers)
  175. {
  176. handler.Handle(new EditorTextEnteringEvent(DataContext as DocumentViewModel, Editor, e));
  177. }
  178. }
  179. internal void ToggleBold()
  180. {
  181. var selectedText = GetSelectedText();
  182. if (string.IsNullOrWhiteSpace(selectedText)) return;
  183. Editor.SelectedText = selectedText.ToggleBold(!selectedText.IsBold());
  184. }
  185. internal void ToggleItalic()
  186. {
  187. var selectedText = GetSelectedText();
  188. if (string.IsNullOrWhiteSpace(selectedText)) return;
  189. Editor.SelectedText = selectedText.ToggleItalic(!selectedText.IsItalic());
  190. }
  191. internal void ToggleCode()
  192. {
  193. if (Editor.SelectedText.Contains(Environment.NewLine))
  194. ToggleCodeBlock();
  195. else
  196. {
  197. var selectedText = GetSelectedText();
  198. if (string.IsNullOrWhiteSpace(selectedText)) return;
  199. Editor.SelectedText = selectedText.ToggleCode(!selectedText.IsCode());
  200. }
  201. }
  202. private string GetSelectedText()
  203. {
  204. var textArea = Editor.TextArea;
  205. // What would you do if the selected text is empty? I vote: Nothing.
  206. if (textArea.Selection.IsEmpty)
  207. return null;
  208. return textArea.Selection.GetText();
  209. }
  210. private void ToggleCodeBlock()
  211. {
  212. var lines = Editor.SelectedText.Split(Environment.NewLine.ToCharArray());
  213. var firstLine = lines[0];
  214. if (firstLine.Length > 4 && firstLine.Substring(0, 4) == Spaces)
  215. RemoveCodeBlock(Spaces, NumSpaces);
  216. else if (firstLine.FirstOrDefault() == '\t')
  217. RemoveCodeBlock("\t", 1);
  218. else
  219. {
  220. var spacer = IndentType == IndentType.Spaces ? Spaces : "\t";
  221. Editor.SelectedText = spacer + Editor.SelectedText.Replace(Environment.NewLine, Environment.NewLine + spacer);
  222. }
  223. }
  224. private void RemoveCodeBlock(string replace, int numberSpaces)
  225. {
  226. Editor.SelectedText = Editor.SelectedText.Replace((Environment.NewLine + replace), Environment.NewLine);
  227. // remember the first line
  228. if (Editor.SelectedText.Length >= numberSpaces)
  229. {
  230. var firstFour = Editor.SelectedText.Substring(0, numberSpaces);
  231. var rest = Editor.SelectedText.Substring(numberSpaces);
  232. Editor.SelectedText = firstFour.Replace(replace, string.Empty) + rest;
  233. }
  234. }
  235. internal void SetHyperlink()
  236. {
  237. var textArea = Editor.TextArea;
  238. if (textArea.Selection.IsEmpty)
  239. return;
  240. var selectedText = textArea.Selection.GetText();
  241. // Check if the selected text already is a link...
  242. string text = selectedText, url = string.Empty;
  243. var match = Regex.Match(selectedText, @"\[(?<text>(?:[^\\]|\\.)+)\]\((?<url>[^)]+)\)");
  244. if (match.Success)
  245. {
  246. text = match.Groups["text"].Value;
  247. url = match.Groups["url"].Value;
  248. }
  249. var hyperlink = new MarkPadHyperlink(text, url);
  250. (DataContext as DocumentViewModel)
  251. .ExecuteSafely(vm =>
  252. {
  253. hyperlink = vm.GetHyperlink(hyperlink);
  254. if (hyperlink != null)
  255. {
  256. textArea.Selection.ReplaceSelectionWithText(string.Format("[{0}]({1})", hyperlink.Text, hyperlink.Url));
  257. }
  258. });
  259. }
  260. private void CanEditDocument(object sender, CanExecuteRoutedEventArgs e)
  261. {
  262. if (Editor != null && Editor.TextArea != null && Editor.TextArea.Selection != null)
  263. {
  264. e.CanExecute = !Editor.TextArea.Selection.IsEmpty;
  265. }
  266. }
  267. void EditorContextMenuOpening(object sender, ContextMenuEventArgs e)
  268. {
  269. // don't show menu if we bail out with return
  270. e.Handled = true;
  271. if (SpellCheckProvider == null) return;
  272. // Bail out if the mouse isn't over the markdownEditor
  273. var editorPosition = Editor.GetPositionFromPoint(Mouse.GetPosition(Editor));
  274. if (!editorPosition.HasValue) return;
  275. var offset = Editor.Document.GetOffset(editorPosition.Value.Line, editorPosition.Value.Column);
  276. var errorSegments = SpellCheckProvider.GetSpellCheckErrors();
  277. var misspelledSegment = errorSegments.FirstOrDefault(segment => segment.StartOffset <= offset && segment.EndOffset >= offset);
  278. if (misspelledSegment == null) return;
  279. // check if the clicked offset is the beginning or end of line to prevent snapping to it (like in text selection) with GetPositionFromPoint
  280. // in practice makes context menu not show when clicking on the first character of a line
  281. var currentLine = Document.GetLineByOffset(offset);
  282. if (offset == currentLine.Offset || offset == currentLine.EndOffset)
  283. {
  284. return;
  285. }
  286. EditorContextMenu.Tag = misspelledSegment;
  287. EditorContextMenu.ItemsSource = SpellCheckProvider.GetSpellcheckSuggestions(editor.Document.GetText(misspelledSegment));
  288. e.Handled = false;
  289. }
  290. void SpellcheckerWordClick(object sender, RoutedEventArgs e)
  291. {
  292. var word = (string)(e.OriginalSource as FrameworkElement).DataContext;
  293. var segment = (TextSegment)EditorContextMenu.Tag;
  294. Editor.Document.Replace(segment, word);
  295. }
  296. public static readonly DependencyProperty SpellcheckProviderProperty =
  297. DependencyProperty.Register("SpellCheckProvider", typeof (ISpellCheckProvider), typeof (MarkdownEditor), new PropertyMetadata(default(ISpellCheckProvider)));
  298. public ISpellCheckProvider SpellCheckProvider
  299. {
  300. get { return (ISpellCheckProvider) GetValue(SpellcheckProviderProperty); }
  301. set { SetValue(SpellcheckProviderProperty, value); }
  302. }
  303. public static readonly DependencyProperty IsColorsInvertedProperty =
  304. DependencyProperty.Register("IsColorsInverted", typeof(bool), typeof(MarkdownEditor));
  305. public bool IsColorsInverted
  306. {
  307. get { return (bool) GetValue(IsColorsInvertedProperty); }
  308. set { SetValue(IsColorsInvertedProperty, value);}
  309. }
  310. }
  311. }