Code:
/ Dotnetfx_Win7_3.5.1 / Dotnetfx_Win7_3.5.1 / 3.5.1 / DEVDIV / depot / DevDiv / releases / Orcas / NetFXw7 / wpf / src / Framework / System / Windows / Documents / TextEditorTyping.cs / 1 / TextEditorTyping.cs
//---------------------------------------------------------------------------- // // File: TextEditorTyping.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Text editing service for controls. // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using MS.Internal; using System.Globalization; using System.Threading; using System.ComponentModel; using System.Text; using System.Collections; // ArrayList using System.Runtime.InteropServices; using System.Windows.Threading; using System.Windows.Input; using System.Windows.Controls; // ScrollChangedEventArgs using System.Windows.Controls.Primitives; // CharacterCasing, TextBoxBase using System.Windows.Media; using System.Windows.Markup; using System.Security; using System.Security.Permissions; using System.Windows.Interop; using MS.Utility; using MS.Win32; using MS.Internal.Documents; using MS.Internal.Commands; // CommandHelpers ////// Subcomponent of TextEditor class - Support for Typing /// internal static class TextEditorTyping { //----------------------------------------------------- // // Class Internal Methods // //----------------------------------------------------- #region Class Internal Methods ////// Registes all handlers needed for text editing control functioning. /// /// /// A type of control for which typing component is registered /// /// /// If registerEventListeners is false, caller is responsible for calling OnXXXEvent methods on TextEditor from /// UIElement and FrameworkElement virtual overrides (piggy backing on the /// UIElement/FrameworkElement class listeners). If true, TextEditor will register /// its own class listener for events it needs. /// /// This method will always register private command listeners. /// internal static void _RegisterClassHandlers(Type controlType, bool registerEventListeners) { if (registerEventListeners) { EventManager.RegisterClassHandler(controlType, Keyboard.KeyDownEvent, new KeyEventHandler(OnKeyDown)); EventManager.RegisterClassHandler(controlType, Keyboard.KeyUpEvent, new KeyEventHandler(OnKeyUp)); EventManager.RegisterClassHandler(controlType, TextCompositionManager.TextInputEvent, new TextCompositionEventHandler(OnTextInput)); } var onEnterBreak = new ExecutedRoutedEventHandler(OnEnterBreak); var onSpace = new ExecutedRoutedEventHandler(OnSpace); var onQueryStatusNYI = new CanExecuteRoutedEventHandler(OnQueryStatusNYI); var onQueryStatusEnterBreak = new CanExecuteRoutedEventHandler(OnQueryStatusEnterBreak); EventManager.RegisterClassHandler(controlType, Mouse.MouseMoveEvent, new MouseEventHandler(OnMouseMove), true /* handledEventsToo */); EventManager.RegisterClassHandler(controlType, Mouse.MouseLeaveEvent, new MouseEventHandler(OnMouseLeave), true /* handledEventsToo */); CommandHelpers.RegisterCommandHandler(controlType, ApplicationCommands.CorrectionList , new ExecutedRoutedEventHandler(OnCorrectionList) , new CanExecuteRoutedEventHandler(OnQueryStatusCorrectionList) , SRID.KeyCorrectionList, SRID.KeyCorrectionListDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.ToggleInsert , new ExecutedRoutedEventHandler(OnToggleInsert) , onQueryStatusNYI , SRID.KeyToggleInsert, SRID.KeyToggleInsertDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Delete , new ExecutedRoutedEventHandler(OnDelete) , onQueryStatusNYI , SRID.KeyDelete, SRID.KeyDeleteDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.DeleteNextWord , new ExecutedRoutedEventHandler(OnDeleteNextWord) , onQueryStatusNYI , SRID.KeyDeleteNextWord, SRID.KeyDeleteNextWordDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.DeletePreviousWord , new ExecutedRoutedEventHandler(OnDeletePreviousWord) , onQueryStatusNYI , SRID.KeyDeletePreviousWord, SRID.KeyDeletePreviousWordDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.EnterParagraphBreak , onEnterBreak , onQueryStatusEnterBreak , SRID.KeyEnterParagraphBreak, SRID.KeyEnterParagraphBreakDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.EnterLineBreak , onEnterBreak , onQueryStatusEnterBreak , SRID.KeyEnterLineBreak, SRID.KeyEnterLineBreakDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.TabForward , new ExecutedRoutedEventHandler(OnTabForward) , new CanExecuteRoutedEventHandler(OnQueryStatusTabForward) , SRID.KeyTabForward, SRID.KeyTabForwardDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.TabBackward , new ExecutedRoutedEventHandler(OnTabBackward) , new CanExecuteRoutedEventHandler(OnQueryStatusTabBackward) , SRID.KeyTabBackward, SRID.KeyTabBackwardDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Space , onSpace , onQueryStatusNYI , SRID.KeySpace, SRID.KeySpaceDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.ShiftSpace , onSpace , onQueryStatusNYI , SRID.KeyShiftSpace, SRID.KeyShiftSpaceDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Backspace , new ExecutedRoutedEventHandler(OnBackspace) , onQueryStatusNYI , KeyGesture.CreateFromResourceStrings(SR.Get(SRID.KeyBackspace), SR.Get(SRID.KeyBackspaceDisplayString)), KeyGesture.CreateFromResourceStrings(SR.Get(SRID.KeyShiftBackspace), SR.Get(SRID.KeyShiftBackspaceDisplayString)) ); } ////// Add the input language changed event handler and save it /// into UIContext data slot. /// internal static void _AddInputLanguageChangedEventHandler(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; Invariant.Assert(This._dispatcher == null); This._dispatcher = Dispatcher.CurrentDispatcher; Invariant.Assert(This._dispatcher != null); threadLocalStore = TextEditor._ThreadLocalStore; // Only add the input language changed event handler once that safe per UIContext if (threadLocalStore.InputLanguageChangeEventHandlerCount == 0) { // Add input changed event handler into InputLanguageManager InputLanguageManager.Current.InputLanguageChanged += new InputLanguageEventHandler(OnInputLanguageChanged); // Add the dispatcher shutdown finished event handler to remove InputLanguageChangedEventHandler // before dispose the dispatcher. Dispatcher.CurrentDispatcher.ShutdownFinished += new EventHandler(OnDispatcherShutdownFinished); } threadLocalStore.InputLanguageChangeEventHandlerCount++; } ////// Remove the input language changed event handler from UIContext data slot. /// internal static void _RemoveInputLanguageChangedEventHandler(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; threadLocalStore = TextEditor._ThreadLocalStore; // Decrease the input language changed event handler reference count threadLocalStore.InputLanguageChangeEventHandlerCount--; // Remove the input language changed event handler when nobody reference it if (threadLocalStore.InputLanguageChangeEventHandlerCount == 0) { // Remove InputLanguageEventHandler InputLanguageManager.Current.InputLanguageChanged -= new InputLanguageEventHandler(OnInputLanguageChanged); // Remove the dispatcher shutdown finished event handler Dispatcher.CurrentDispatcher.ShutdownFinished -= new EventHandler(OnDispatcherShutdownFinished); } } ////// Discards previous typing undo unit, to prevent /// from merging it with the subsequent typing. /// internal static void _BreakTypingSequence(TextEditor This) { // Discard typing undo unit This._typingUndoUnit = null; } // Handles any pending input events. internal static void _FlushPendingInputItems(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } threadLocalStore = TextEditor._ThreadLocalStore; if (threadLocalStore.PendingInputItems != null) { try { for (int i = 0; i < threadLocalStore.PendingInputItems.Count; i++) { ((InputItem)threadLocalStore.PendingInputItems[i]).Do(); // After the first dequeue, clear the bit that tracks if // any events are handled after ctl+shift (change flow direction keyboard hotkey). threadLocalStore.PureControlShift = false; } } finally { threadLocalStore.PendingInputItems.Clear(); } } // Clear the bit that tracks if any events are handled after // ctl+shift (change flow direction keyboard hotkey) one last // time, in case the queue was empty. // // Because we only call this method in preparation for handling // a Command, we want this bit cleared. threadLocalStore.PureControlShift = false; } // Un-hides the mouse cursor. internal static void _ShowCursor() { if (TextEditor._ThreadLocalStore.HideCursor) { TextEditor._ThreadLocalStore.HideCursor = false; SafeNativeMethods.ShowCursor(true); } } // ................................................................ // // Event Handlers // // ................................................................ // KeyDownEvent handler - needed for handling FlowDirection commands on KeyUp internal static void OnKeyDown(object sender, KeyEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || (This.IsReadOnly && !This.IsReadOnlyCaretVisible) || !This._IsSourceInScope(e.OriginalSource)) { return; } // Ignore repeated events generated when the key is hold down for long time if (e.IsRepeat) { return; } // If UiScope has a ToolTip and it is open, any keyboard/mouse activity should close the tooltip. This.CloseToolTip(); TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; // Clear a flag indicating that Shift key was pressed without any following key // This flag is necessary for KeyUp(RightShift/LeftShift) processing. threadLocalStore.PureControlShift = false; // Shift+Ctrl combination must be executed only when it's "pure" - // no mouse dragging/movement, no other key downs involved in a gesture. if (This.TextView != null && !This.UiScope.IsMouseCaptured) { if ((e.Key == Key.RightShift || e.Key == Key.LeftShift) && // (e.KeyboardDevice.Modifiers & ModifierKeys.Control) != 0 && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { threadLocalStore.PureControlShift = true; // will be cleared by any other key down } else if ((e.Key == Key.RightCtrl || e.Key == Key.LeftCtrl) && // (e.KeyboardDevice.Modifiers & ModifierKeys.Shift) != 0 && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { threadLocalStore.PureControlShift = true; // will be cleared by any other key down } else if (e.Key == Key.RightCtrl || e.Key == Key.LeftCtrl) { UpdateHyperlinkCursor(This); } } } // Handler for KeyUp events internal static void OnKeyUp(object sender, KeyEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || (This.IsReadOnly && !This.IsReadOnlyCaretVisible) || !This._IsSourceInScope(e.OriginalSource)) { return; } // Delegate the work to specific handlers. switch (e.Key) { case Key.RightShift: case Key.LeftShift: if (TextEditor._ThreadLocalStore.PureControlShift && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { TextEditorTyping.ScheduleInput(This, new KeyUpInputItem(This, e.Key, e.KeyboardDevice.Modifiers)); } break; case Key.LeftCtrl: case Key.RightCtrl: UpdateHyperlinkCursor(This); break; } } // TextInputEvent handler. internal static void OnTextInput(object sender, TextCompositionEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(e.OriginalSource)) { return; } FrameworkTextComposition composition = e.TextComposition as FrameworkTextComposition; // Ignore any event with an empty Text property. // The public TextCompositionEventArgs ctor allows null Text values. // Also it's possible to have non-null ControlText or AltText with String.Empty Text values. if (composition == null && (e.Text == null || e.Text.Length == 0)) { return; } // Consider event handled e.Handled = true; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } // If this event is our Cicero TextStore composition, we always handles through ITextStore::SetText. if (composition != null) { if (composition.Owner == This.TextStore) { This.TextStore.UpdateCompositionText(composition); } else if (composition.Owner == This.ImmComposition) { This.ImmComposition.UpdateCompositionText(composition); } } else { // Input text (with springload formatting if any) // We'll delay the event handling, batching it up with other // input if layout is too slow to keep up with the input stream. KeyboardDevice keyboard = e.Device as KeyboardDevice; TextEditorTyping.ScheduleInput(This, new TextInputItem(This, e.Text, /*isInsertKeyToggled:*/keyboard != null ? keyboard.IsKeyToggled(Key.Insert) : false)); } } #endregion Class Internal Methods //------------------------------------------------------ // // Private Methods // //----------------------------------------------------- #region Private Methods // ................................................................ // // Command Handlers // // ................................................................ ////// CorrectionList command QueryStatus handler /// private static void OnQueryStatusCorrectionList(object target, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } if (This.TextStore != null) { // Don't do actual reconversion, it just checks if the current selection is reconvertable. args.CanExecute = This.TextStore.QueryRangeOrReconvertSelection( /*fDoReconvert:*/ false); } else { // If there is no textstore, this command is not enabled. args.CanExecute = false; } } ////// CorrectionList command event handler. /// private static void OnCorrectionList(object target, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } if (This.TextStore != null) { This.TextStore.QueryRangeOrReconvertSelection( /*fDoReconvert:*/ true); } } ////// ToggleInsert command handler /// ////// Critical:This code toggles the state of the insert key and in doing so touches Cicero code which is critical (StartTrasitoryExtension and StopTransitoryExtension) /// TreatAsSafe: This code can do no harm and any state change can be reversed by hitting toggle again /// [SecurityCritical,SecurityTreatAsSafe] private static void OnToggleInsert(object target, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } This._OvertypeMode = !This._OvertypeMode; // Use Cicero's transitory extension for OverTyping. if (TextServicesLoader.ServicesInstalled && (This.TextStore != null)) { TextServicesHost tsfHost = TextServicesHost.Current; if (tsfHost != null) { if (This._OvertypeMode) { TextServicesHost.StartTransitoryExtension(This.TextStore); } else { TextServicesHost.StopTransitoryExtension(This.TextStore); } } } } // ........................................................................... // // Delete Characters // // ........................................................................... private static void OnDelete(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(args.Source)) { return; } TextEditorTyping._FlushPendingInputItems(This); // Note, that Delete and Backspace keys behave differently. ((TextSelection)This.Selection).ClearSpringloadFormatting(); // Forget previously suggested horizontal position TextEditorSelection._ClearSuggestedX(This); using (This.Selection.DeclareChangeBlock()) { ITextPointer position = This.Selection.End; if (This.Selection.IsEmpty) { ITextPointer deletePosition = position.GetNextInsertionPosition(LogicalDirection.Forward); if (deletePosition == null) { // Nothing to delete. return; } if (TextPointerBase.IsAtRowEnd(deletePosition)) { // Backspace and delete are a no-op at row end positions. return; } if (position is TextPointer && !IsAtListItemStart(deletePosition) && HandleDeleteWhenStructuralBoundaryIsCrossed(This, (TextPointer)position, (TextPointer)deletePosition)) { // We are crossing structural boundary and // selection was updated in HandleDeleteWhenStructuralBoundaryIsCrossed. return; } // Selection is empty, extend selection forward to delete the following char. This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Forward); } // Delete selected text. This.Selection.Text = String.Empty; } } private static void OnBackspace(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(args.Source)) { return; } TextEditorTyping._FlushPendingInputItems(This); // Forget previously suggested horizontal position. TextEditorSelection._ClearSuggestedX(This); using (This.Selection.DeclareChangeBlock()) { ITextPointer position = This.Selection.Start; // Note that this is different than the previous insertion position in backward direction, // in case of combining characters and surrogates. ITextPointer backspacePosition = null; // In case when selection is empty we will need to expand // it backward. Check first whether we are crossing // any structural boundary - to disable the operation // in such case. if (This.Selection.IsEmpty) { // Identify a case for special actions in the beginning of paragraphs or list items if (This.AcceptsRichContent && IsAtListItemStart(position)) { // Remove a bullet from this list item. // Note that doing anything more aggressive like unindenting // would make backspace very inconvenient for merging two same-level list items. TextRangeEditLists.ConvertListItemsToParagraphs((TextRange)This.Selection); } else if (This.AcceptsRichContent && (IsAtListItemChildStart(position, false /* emptyChildOnly */) || IsAtIndentedParagraphOrBlockUIContainerStart(This.Selection.Start))) { // Unindent the list by one level. TextEditorLists.DecreaseIndentation(This); } else { // Find a preceding position. ITextPointer deletePosition = position.GetNextInsertionPosition(LogicalDirection.Backward); if (deletePosition == null) { // Nothing to delete. ((TextSelection)This.Selection).ClearSpringloadFormatting(); return; } if (TextPointerBase.IsAtRowEnd(deletePosition)) { // Backspace and delete are a no-op at row end positions. ((TextSelection)This.Selection).ClearSpringloadFormatting(); return; } if (position is TextPointer && HandleDeleteWhenStructuralBoundaryIsCrossed(This, (TextPointer)position, (TextPointer)deletePosition)) { // We are crossing structural boundary and // selection was updated in HandleDeleteWhenStructuralBoundaryIsCrossed. return; } // Normalize the current position backward. position = position.GetFrozenPointer(LogicalDirection.Backward); // If TextView is valid, we can get the backspace position from TextView and then // delete the content from the backspace position to the current position. // Otherwise, we move the selection to the previous insertion position then delete. if (This.TextView != null && position.HasValidLayout && position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text) { // Get the backspace caret unit position from TextView that support surrogate // and all internal characters backspacePosition = This.TextView.GetBackspaceCaretUnitPosition(position); Invariant.Assert(backspacePosition != null); // bug 1733868 // backspacePosition should always be less than position. // But backspacing before '\n' (no preceding '\r') exposes // this bug. if (backspacePosition.CompareTo(position) == 0) { // As of 6/30/2006 we're too close to ship to fix // this bug cleanly. Ideally, we would stop referencing // the position at the end-of-line (which mil text does not // consider a valid position), and instead reference the start // of the next line (flipping the original position's gravity). // // As a work-around, take the previous insertion position, // ignoring glyph level backspace positions. // This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Backward); backspacePosition = null; } // If there is no text preceding the backspacePosition, extend to the next // insertion position to make sure we cleanup any empty Inlines left // after the delete. We don't want a non-empty selection if there is // bordering text, because we might normalize outside of a run of combining // marks otherwise. else if (backspacePosition.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.Text) { This.Selection.Select(This.Selection.End, backspacePosition); backspacePosition = null; } } else { // Selection is empty, extend it backward to include the preceeding char. This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Backward); } } } // Save current formatting properties for springload formatting before backspace // Note, that Delete and Backspace keys behave differently: it's by design. if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); ((TextSelection)This.Selection).SpringloadCurrentFormatting(); } // If backspace position is available from TextView, we can delete it directly // without the normalization. Because we already normalized the backspace position. if (backspacePosition != null) { Invariant.Assert(backspacePosition.CompareTo(position) < 0); // Delete the content from the backspace to the current position backspacePosition.DeleteContentToPosition(position); } else { // Delete selected text This.Selection.Text = String.Empty; position = This.Selection.Start; } // Set the caret position with the Backward direction, // because we want to appear close to previous character. // However, we do not allow to stop at end of line. // We alow to stop next to space - to be consistent with typing behavior. This.Selection.SetCaretToPosition(position, LogicalDirection.Backward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/true); } } // Helper for OnDelete/OnBackspace, handles special case scenarios for delete when table or BlockUIContainer boundaries are crossed. // Returns true if passed positions were in this category and appropriate editing action was taken for handling delete operation. // Otherwise, returns false. private static bool HandleDeleteWhenStructuralBoundaryIsCrossed(TextEditor This, TextPointer position, TextPointer deletePosition) { if (!TextRangeEditTables.IsTableStructureCrossed(position, deletePosition) && !IsBlockUIContainerBoundaryCrossed(position, deletePosition) && !TextPointerBase.IsAtRowEnd(position)) { return false; } LogicalDirection directionOfDelete = position.CompareTo(deletePosition) < 0 ? LogicalDirection.Forward : LogicalDirection.Backward; Block paragraphOrBlockUIContainerToDelete = position.ParagraphOrBlockUIContainer; // Check if an empty paragraph or BlockUIContainer needs to be deleted. if (paragraphOrBlockUIContainerToDelete != null) { if (directionOfDelete == LogicalDirection.Forward) { // if (paragraphOrBlockUIContainerToDelete.NextBlock != null && paragraphOrBlockUIContainerToDelete is Paragraph && Paragraph.HasNoTextContent((Paragraph)paragraphOrBlockUIContainerToDelete) || // empty paragraph paragraphOrBlockUIContainerToDelete is BlockUIContainer && paragraphOrBlockUIContainerToDelete.IsEmpty) // empty BlockUIContainer { paragraphOrBlockUIContainerToDelete.RepositionWithContent(null); } } else { if (paragraphOrBlockUIContainerToDelete.PreviousBlock != null && paragraphOrBlockUIContainerToDelete is Paragraph && Paragraph.HasNoTextContent((Paragraph)paragraphOrBlockUIContainerToDelete) || // empty paragraph paragraphOrBlockUIContainerToDelete is BlockUIContainer && paragraphOrBlockUIContainerToDelete.IsEmpty) // empty BlockUIContainer { paragraphOrBlockUIContainerToDelete.RepositionWithContent(null); } } } // Set caret position. This.Selection.SetCaretToPosition(deletePosition, directionOfDelete, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/true); if (directionOfDelete == LogicalDirection.Backward) { // Clear springload formatting in case of backspace ((TextSelection)This.Selection).ClearSpringloadFormatting(); } return true; } // Tests if the position is at the beginning of indented paragraph - // to allow Backspace to decrease indentation private static bool IsAtIndentedParagraphOrBlockUIContainerStart(ITextPointer position) { if ((position is TextPointer) && TextPointerBase.IsAtParagraphOrBlockUIContainerStart(position)) { Block paragraphOrBlockUIContainer = ((TextPointer)position).ParagraphOrBlockUIContainer; if (paragraphOrBlockUIContainer != null) { FlowDirection flowDirection = paragraphOrBlockUIContainer.FlowDirection; Thickness margin = paragraphOrBlockUIContainer.Margin; return flowDirection == FlowDirection.LeftToRight && margin.Left > 0 || flowDirection == FlowDirection.RightToLeft && margin.Right > 0 || (paragraphOrBlockUIContainer is Paragraph && ((Paragraph)paragraphOrBlockUIContainer).TextIndent > 0); } } return false; } // Tests if the position is at the beginning of some list item - // to allow Backspace to delete the bullet. private static bool IsAtListItemStart(ITextPointer position) { // Check for empty ListItem case if (typeof(ListItem).IsAssignableFrom(position.ParentType) && position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { return true; } while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { Type parentType = position.ParentType; if (TextSchema.IsBlock(parentType)) { if (TextSchema.IsParagraphOrBlockUIContainer(parentType)) { position = position.GetNextContextPosition(LogicalDirection.Backward); if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && typeof(ListItem).IsAssignableFrom(position.ParentType)) { return true; } } return false; } position = position.GetNextContextPosition(LogicalDirection.Backward); } return false; } // Tests if a position is at the start of a Block // within a ListItem. // // position must be normalized at an insertion point. private static bool IsAtListItemChildStart(ITextPointer position, bool emptyChildOnly) { if (position.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart) { return false; } if (emptyChildOnly && position.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.ElementEnd) { return false; } ITextPointer navigator = position.CreatePointer(); // Cross inline opening tags. while (navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && typeof(Inline).IsAssignableFrom(navigator.ParentType)) { navigator.MoveToElementEdge(ElementEdge.BeforeStart); } // Check if navigator is at the start of a block. if (!(navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsParagraphOrBlockUIContainer(navigator.ParentType))) { return false; } // Move just past the block. navigator.MoveToElementEdge(ElementEdge.BeforeStart); return typeof(ListItem).IsAssignableFrom(navigator.ParentType); } // Tests if position1 and position2 cross a BlockUIContainer boundary. private static bool IsBlockUIContainerBoundaryCrossed(TextPointer position1, TextPointer position2) { return (position1.Parent is BlockUIContainer || position2.Parent is BlockUIContainer) && position1.Parent != position2.Parent; } // ........................................................................... // // Delete Words // // ........................................................................... private static void OnDeleteNextWord(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange) { return; } TextEditorTyping._FlushPendingInputItems(This); ITextPointer wordBoundary = This.Selection.End.CreatePointer(); // When selection is not empty the command deletes selected content // without extending it to the word bopundary. For empty selection // the command deletes a content from caret position to // nearest word boundary in a given direction if (This.Selection.IsEmpty) { TextPointerBase.MoveToNextWordBoundary(wordBoundary, LogicalDirection.Forward); } if (TextRangeEditTables.IsTableStructureCrossed(This.Selection.Start, wordBoundary)) { return; } ITextRange textRange = new TextRange(This.Selection.Start, wordBoundary); // When a range is TableCellRange we do not want to make deletions if (textRange.IsTableCellRange) { return; } if (!textRange.IsEmpty) { using (This.Selection.DeclareChangeBlock()) { // Note asymetry with Backspace: we do not load springload formatting here if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); } This.Selection.Select(textRange.Start, textRange.End); // Delete selected text This.Selection.Text = String.Empty; } } } private static void OnDeletePreviousWord(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange) { // return; } TextEditorTyping._FlushPendingInputItems(This); ITextPointer wordBoundary = This.Selection.Start.CreatePointer(); // When selection is not empty the command deletes selected content // without extending it to the word bopundary. For empty selection // the command deletes a content from caret position to // nearest word boundary in a given direction if (This.Selection.IsEmpty) { TextPointerBase.MoveToNextWordBoundary(wordBoundary, LogicalDirection.Backward); } // When the movement to word boundary crosses table structure, ignore the command if (TextRangeEditTables.IsTableStructureCrossed(wordBoundary, This.Selection.Start)) { return; } // Build a range from a start of a word preceding start of selection, ending at the end of whole selection // This range is supposed to be deleted by the operation. ITextRange textRange = new TextRange(wordBoundary, This.Selection.End); // When a range is TableCellRange we do not want to make deletions if (textRange.IsTableCellRange) { return; } if (!textRange.IsEmpty) { using (This.Selection.DeclareChangeBlock()) { // Note asymetry with Backspace: we DO load springload formatting here if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); This.Selection.Select(textRange.Start, textRange.End); ((TextSelection)This.Selection).SpringloadCurrentFormatting(); } else { This.Selection.Select(textRange.Start, textRange.End); } // Delete selected text This.Selection.Text = String.Empty; } } } // ........................................................................... // // Enter Breaks // // ........................................................................... ////// EnterParagraphBreak/EnterLineBreak command QueryStatus handler /// private static void OnQueryStatusEnterBreak(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { args.ContinueRouting = true; return; } if (This.Selection.IsTableCellRange || !This.AcceptsReturn) { args.ContinueRouting = true; return; } args.CanExecute = true; } // EnterParagraphBreak/EnterLineBreak command handler private static void OnEnterBreak(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange || !This.AcceptsReturn || !This.UiScope.IsKeyboardFocused) { return; } TextEditorTyping._FlushPendingInputItems(This); // We do not merge Enter typing with other typing - for better undo structuring using (This.Selection.DeclareChangeBlock()) { // Flag to indicate if selection was changed. It may be unaffected in following cases: // 1. In plain text case, Environment.NewLine can not fit in MaxLength // 2. In rich text case, we cannot split a hyperlink ancestor to insert a paragraph break bool wasSelectionChanged; if (This.AcceptsRichContent && This.Selection.Start is TextPointer) { // Paragraph insertion for the case of rich text wasSelectionChanged = HandleEnterBreakForRichText(This, args.Command); } else { // Newline insertion for plain text wasSelectionChanged = HandleEnterBreakForPlainText(This); } // Update caret and clear SuggestedX only when selection has changed. if (wasSelectionChanged) { // Position the caret. This.Selection.SetCaretToPosition(This.Selection.End, LogicalDirection.Forward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); // Forget previously suggested horizontal position TextEditorSelection._ClearSuggestedX(This); } } } // Helper for OnEnterBreak for rich text case private static bool HandleEnterBreakForRichText(TextEditor This, ICommand command) { bool wasSelectionChanged = true; // Save current inline settings to continue on the next paragraph ((TextSelection)This.Selection).SpringloadCurrentFormatting(); if (!This.Selection.IsEmpty) { // Delete selected content This.Selection.Text = String.Empty; } if (HandleEnterBreakWhenStructuralBoundaryIsCrossed(This, command)) { // We are crossing structural boundary and // selection was updated if HandleEnterBreakWhenStructuralBoundaryIsCrossed returned true } else { TextPointer newEnd = ((TextSelection)This.Selection).End; if (command == EditingCommands.EnterParagraphBreak) { if (newEnd.HasNonMergeableInlineAncestor && !TextPointerBase.IsPositionAtNonMergeableInlineBoundary(newEnd)) { // Selection end is in the middle of a hyperlink element, enter is a no-op. wasSelectionChanged = false; } else { newEnd = TextRangeEdit.InsertParagraphBreak(newEnd, /*moveIntoSecondParagraph*/true); } } else if (command == EditingCommands.EnterLineBreak) { newEnd = newEnd.InsertLineBreak(); } if (wasSelectionChanged) { This.Selection.Select(newEnd, newEnd); } } return wasSelectionChanged; } // Helper for OnEnterBreak for plain text case private static bool HandleEnterBreakForPlainText(TextEditor This) { bool wasSelectionChanged = true; // Filter Environment.NewLine based on TextBox.MaxLength string filteredText = This._FilterText(Environment.NewLine, This.Selection); if (filteredText != String.Empty) { This.Selection.Text = Environment.NewLine; } else { // Do not update selection if Environment.NewLine can not fit in. wasSelectionChanged = false; } return wasSelectionChanged; } // Helper for rich text OnEnterBreak case, handles special cases when a // structural boundary such as listitem, table, blockuicontainer is crossed. private static bool HandleEnterBreakWhenStructuralBoundaryIsCrossed(TextEditor This, ICommand command) { Invariant.Assert(This.Selection.Start is TextPointer); TextPointer position = (TextPointer)This.Selection.Start; bool structuralBoundaryIsCrossed = true; if (TextPointerBase.IsAtRowEnd(position)) { // For both ParagraphBreak and LineBreak commands, insert a new row after the current one TextRange range = ((TextSelection)This.Selection).InsertRows(+1); This.Selection.SetCaretToPosition(range.Start, LogicalDirection.Forward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); } else if (This.Selection.IsEmpty && (TextPointerBase.IsInEmptyListItem(position) || IsAtListItemChildStart(position, true /* emptyChildOnly */)) && command == EditingCommands.EnterParagraphBreak) { // Unindent the list by one level. TextEditorLists.DecreaseIndentation(This); } else if (TextPointerBase.IsBeforeFirstTable(position) || TextPointerBase.IsAtBlockUIContainerStart(position)) { // Calling EnsureInsertionPosition has the effect of inserting a paragraph BEFORE the table or BlockUIContainer/Table. // In this case, we do not want to move selection end to the paragraph just created. TextRangeEditTables.EnsureInsertionPosition(position); } else if (TextPointerBase.IsAtBlockUIContainerEnd(position)) { // Calling EnsureInsertionPosition has the effect of inserting a paragraph AFTER the BlockUIContainer. // Update selection end to position in the following paragraph. TextPointer newEnd = TextRangeEditTables.EnsureInsertionPosition(position); This.Selection.Select(newEnd, newEnd); } else { structuralBoundaryIsCrossed = false; } return structuralBoundaryIsCrossed; } // ........................................................................... // // Flow Direction // // ........................................................................... ////// LeftToRightFlowDirection command event handler. /// private static void OnFlowDirectionCommand(TextEditor This, Key key) { // using (This.Selection.DeclareChangeBlock()) { if (key == Key.LeftShift) { if (This.AcceptsRichContent && (This.Selection is TextSelection)) { // NOTE: We do not call OnApplyProperty to avoid recursion for FlushPendingInput ((TextSelection)This.Selection).ApplyPropertyValue(FlowDocument.FlowDirectionProperty, FlowDirection.LeftToRight, /*applyToParagraphs*/true); } else { Invariant.Assert(This.UiScope != null); UIElementPropertyUndoUnit.Add(This.TextContainer, This.UiScope, FrameworkElement.FlowDirectionProperty, FlowDirection.LeftToRight); This.UiScope.SetValue(FrameworkElement.FlowDirectionProperty, FlowDirection.LeftToRight); } } else { Invariant.Assert(key == Key.RightShift); if (This.AcceptsRichContent && (This.Selection is TextSelection)) { // NOTE: We do not call OnApplyProperty to avoid recursion for FlushPendingInput ((TextSelection)This.Selection).ApplyPropertyValue(FlowDocument.FlowDirectionProperty, FlowDirection.RightToLeft, /*applyToParagraphs*/true); } else { Invariant.Assert(This.UiScope != null); UIElementPropertyUndoUnit.Add(This.TextContainer, This.UiScope, FrameworkElement.FlowDirectionProperty, FlowDirection.RightToLeft); This.UiScope.SetValue(FrameworkElement.FlowDirectionProperty, FlowDirection.RightToLeft); } } ((TextSelection)This.Selection).UpdateCaretState(CaretScrollMethod.Simple); } } // ........................................................................... // // In some controls, Space and Shift+Space keys are mapped to // scroll down and scroll up commands respectively. // In TextEditor, we handle them as text input. // Using the command system allows controls to override the existing default behavior. // ........................................................................... // Space, Shift+Space handler private static void OnSpace(object sender, ExecutedRoutedEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(e.OriginalSource)) { return; } // If this event is our Cicero TextStore composition, we always handles through ITextStore::SetText. if (This.TextStore != null && This.TextStore.IsComposing) { return; } if (This.ImmComposition != null && This.ImmComposition.IsComposition) { return; } // Consider event handled e.Handled = true; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } ScheduleInput(This, new TextInputItem(This, " ", /*isInsertKeyToggled:*/!This._OvertypeMode)); } // ........................................................................... // // Tab and Back-Tab // // ........................................................................... ////// ForwardTabStop command QueryStatus handler /// private static void OnQueryStatusTabForward(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This != null && This.AcceptsTab) { args.CanExecute = true; } else { args.ContinueRouting = true; } } ////// BackwardTabStop command QueryStatus handler /// private static void OnQueryStatusTabBackward(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This != null && This.AcceptsTab) { args.CanExecute = true; } else { args.ContinueRouting = true; } } // Tab handler. private static void OnTabForward(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } TextEditorTyping._FlushPendingInputItems(This); if (HandleTabInTables(This, LogicalDirection.Forward)) { // All done on table level. return; } if (This.AcceptsRichContent && (!This.Selection.IsEmpty || TextPointerBase.IsAtParagraphOrBlockUIContainerStart(This.Selection.Start)) && EditingCommands.IncreaseIndentation.CanExecute(null, (IInputElement)sender)) { // In RichTextBox Tab/Shift+Tab keys work as paragraph/list indentation EditingCommands.IncreaseIndentation.Execute(null, (IInputElement)sender); } else { // In plain text we treat tab as a characters always DoTextInput(This, "\t", /*isInsertKeyToggled:*/!This._OvertypeMode, /*acceptControlCharacters:*/true); } } // Shift+Tab handler. private static void OnTabBackward(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } // TextEditorTyping._FlushPendingInputItems(This); if (HandleTabInTables(This, LogicalDirection.Backward)) { // All done on table level. return; } if (This.AcceptsRichContent && (!This.Selection.IsEmpty || TextPointerBase.IsAtParagraphOrBlockUIContainerStart(This.Selection.Start)) && EditingCommands.DecreaseIndentation.CanExecute(null, (IInputElement)sender)) { // In RichTextBox Tab/Shift+Tab keys work as paragraph/list indentation EditingCommands.DecreaseIndentation.Execute(null, (IInputElement)sender); } else { // In plain text we treat tab as a characters always DoTextInput(This, "\t", /*isInsertKeyToggled:*/!This._OvertypeMode, /*acceptControlCharacters:*/true); } } // Command handler for Tab and ShiftTab - moves caret between table cells // if the selection is within a table. Otherwise does nothing and returns false. private static bool HandleTabInTables(TextEditor This, LogicalDirection direction) { if (!This.AcceptsRichContent) { return false; } if (This.Selection.IsTableCellRange) { // When table cell range is selected, Tab simply collapses // a selection to a content of a first cell This.Selection.SetCaretToPosition(This.Selection.Start, LogicalDirection.Backward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); return true; } if (This.Selection.IsEmpty && TextPointerBase.IsAtRowEnd(This.Selection.End)) { // From the end of row we go to the first cell of a next row TableCell cell = null; TableRow row = ((TextPointer)This.Selection.End).Parent as TableRow; Invariant.Assert(row != null); TableRowGroup body = row.RowGroup; int rowIndex = body.Rows.IndexOf(row); if (direction == LogicalDirection.Forward) { if (rowIndex + 1 < body.Rows.Count) { cell = body.Rows[rowIndex + 1].Cells[0]; } } else { if (rowIndex > 0) { cell = body.Rows[rowIndex - 1].Cells[body.Rows[rowIndex - 1].Cells.Count - 1]; } } if (cell != null) { This.Selection.Select(cell.ContentStart, cell.ContentEnd); } return true; } // Check if selection is within a table TextElement parent = ((TextPointer)This.Selection.Start).Parent as TextElement; while (parent != null && !(parent is TableCell)) { parent = parent.Parent as TextElement; } if (parent is TableCell) { TableCell cell = (TableCell)parent; TableRow row = cell.Row; TableRowGroup body = row.RowGroup; int cellIndex = row.Cells.IndexOf(cell); int rowIndex = body.Rows.IndexOf(row); if (direction == LogicalDirection.Forward) { if (cellIndex + 1 < row.Cells.Count) { cell = row.Cells[cellIndex + 1]; } else if (rowIndex + 1 < body.Rows.Count) { cell = body.Rows[rowIndex + 1].Cells[0]; } else { // } } else { if (cellIndex > 0) { cell = row.Cells[cellIndex - 1]; } else if (rowIndex > 0) { cell = body.Rows[rowIndex - 1].Cells[body.Rows[rowIndex - 1].Cells.Count - 1]; } else { // } } Invariant.Assert(cell != null); This.Selection.Select(cell.ContentStart, cell.ContentEnd); return true; } return false; } // ...................................................... // // Handling Text Input // // ...................................................... ////// This is a single method used to insert user input characters. /// It takes care of typing undo, springload formatting, overtype mode etc. /// /// /// /// Text to insert. /// /// /// Reflects a state of Insert key at the moment of textData input. /// /// /// True indicates that control characters like '\t' or '\r' etc. can be inserted. /// False means that all control characters are filtered out. /// private static void DoTextInput(TextEditor This, string textData, bool isInsertKeyToggled, bool acceptControlCharacters) { // Hide the mouse cursor on user input. HideCursor(This); // Remove control characters. Note that this is not included into _FilterText, // because we want such kind of filtering only for real input, // not for copy/paste. if (!acceptControlCharacters) { for (int i = 0; i < textData.Length; i++) { if (Char.IsControl(textData[i])) { textData = textData.Remove(i--, 1); // decrement i to compensate for character removal } } } string filteredText = This._FilterText(textData, This.Selection); if (filteredText.Length == 0) { return; } TextEditorTyping.OpenTypingUndoUnit(This); UndoCloseAction closeAction = UndoCloseAction.Rollback; try { using (This.Selection.DeclareChangeBlock()) { This.Selection.ApplyTypingHeuristics(This.AllowOvertype && This._OvertypeMode && filteredText != "\t"); This.SetSelectedText(filteredText, InputLanguageManager.Current.CurrentInputLanguage); // Create caret position normalized backward to keep formatting of a character just typed ITextPointer caretPosition = This.Selection.End.CreatePointer(LogicalDirection.Backward); // Set selection at the end of input content This.Selection.SetCaretToPosition(caretPosition, LogicalDirection.Backward, /*allowStopAtLineEnd:*/true, /*allowStopNearSpace:*/true); // Note: Using explicit backward orientation we keep formatting with // a previous character during typing. closeAction = UndoCloseAction.Commit; } } finally { TextEditorTyping.CloseTypingUndoUnit(This, closeAction); } } // Takes state originating with a KeyDownEvent or TextInputEvent and // schedules it for eventual handling. // // Normally we delay handling until a Background priority event fires. // This has the effect of batching multiple input events when // layout cannot keep up with the input stream. // // However, if any mouse events are pending, we handle the event // immediately, since otherwise we risk the possibility of handling // the events out of order. private static void ScheduleInput(TextEditor This, InputItem item) { if (!This.AcceptsRichContent || IsMouseInputPending(This)) { // We have to do the work now, or we'll get out of synch. TextEditorTyping._FlushPendingInputItems(This); item.Do(); } else { TextEditorThreadLocalStore threadLocalStore; threadLocalStore = TextEditor._ThreadLocalStore; if (threadLocalStore.PendingInputItems == null) { threadLocalStore.PendingInputItems = new ArrayList(1); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(BackgroundInputCallback), This); } threadLocalStore.PendingInputItems.Add(item); } } // Returns true if any mouse input event is currently waiting in the // win32 message queue for processing. // Avalon doesn't keep a separate queue for input events. Instead // it interleaves work items with the win32 input queue. ////// Critical - Calls PeekMessage, and accesses the root window /// TreatAsSafe - The information it returns is safe to return. /// [SecurityCritical,SecurityTreatAsSafe] private static bool IsMouseInputPending(TextEditor This) { bool mouseInputPending = false; IWin32Window win32Window = PresentationSource.CriticalFromVisual(This.UiScope) as IWin32Window; if (win32Window != null) { IntPtr hwnd = IntPtr.Zero; new UIPermission(UIPermissionWindow.AllWindows).Assert(); // BlessedAssert try { hwnd = win32Window.Handle; } finally { UIPermission.RevertAssert(); } if (hwnd != (IntPtr)0) { System.Windows.Interop.MSG message = new System.Windows.Interop.MSG(); mouseInputPending = UnsafeNativeMethods.PeekMessage(ref message, new HandleRef(null, hwnd), NativeMethods.WM_MOUSEFIRST, NativeMethods.WM_MOUSELAST, NativeMethods.PM_NOREMOVE); } } return mouseInputPending; } // Background priority callback used to process keystrokes. private static object BackgroundInputCallback(object This) { TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; Invariant.Assert(This is TextEditor); Invariant.Assert(threadLocalStore.PendingInputItems != null); try { TextEditorTyping._FlushPendingInputItems((TextEditor)This); } finally { threadLocalStore.PendingInputItems = null; } return null; } ////// Callback for shutdown finished dispatcher. Before shutdown dispatcher, we should clean /// InputLanguageChangedEventHandler. /// private static void OnDispatcherShutdownFinished(object sender, EventArgs args) { // Remove the dispatcher shutdown finished event handler Dispatcher.CurrentDispatcher.ShutdownFinished -= new EventHandler(OnDispatcherShutdownFinished); // Remove the input language changed event handler InputLanguageManager.Current.InputLanguageChanged -= new InputLanguageEventHandler(OnInputLanguageChanged); TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; // Clear InputLanguageChangeEventHandler count threadLocalStore.InputLanguageChangeEventHandlerCount = 0; } // InputLanguageChanged handler. private static void OnInputLanguageChanged(object sender, InputLanguageEventArgs e) { TextSelection.OnInputLanguageChanged(e.NewLanguage); } // Base class for keyboard/text input items. // Individual keystroke/text events are batched and handled together // when layout cannot keep up with the input stream. private abstract class InputItem { // Ctor. internal InputItem(TextEditor textEditor) { _textEditor = textEditor; } // Handles the input event. internal abstract void Do(); // The TextEditor instance on which this input item applies. TextEditor _textEditor; protected TextEditor TextEditor { get { return _textEditor; } } } // Holds state originating from a single TextInputEvent. private class TextInputItem : InputItem { // Ctor. internal TextInputItem(TextEditor textEditor, string text, bool isInsertKeyToggled) : base (textEditor) { _text = text; _isInsertKeyToggled = isInsertKeyToggled; } // Inserts event content into the document. internal override void Do() { if (TextEditor.UiScope == null) { // We dont want to process the input item if the editor has already been detached from its UiScope. return; } DoTextInput(TextEditor, _text, _isInsertKeyToggled, /*acceptControlCharacters:*/false); } // Text to input. private readonly string _text; private readonly bool _isInsertKeyToggled; } // Holds state originating from a single KeyDownEvent. private class KeyUpInputItem : InputItem { // Ctor. internal KeyUpInputItem(TextEditor textEditor, Key key, ModifierKeys modifiers) : base(textEditor) { _key = key; _modifiers = modifiers; } // Fires the command associated with a keystroke. internal override void Do() { if (TextEditor.UiScope == null) { // We dont want to process the input item if the editor has already been detached from its UiScope. return; } // Delegate the work to specific handlers. switch (_key) { case Key.RightShift: // Only support RTL flow direction in case of having the installed // bidi input language. if (TextSelection.IsBidiInputLanguageInstalled() == true) { TextEditorTyping.OnFlowDirectionCommand(TextEditor, _key); } break; case Key.LeftShift: TextEditorTyping.OnFlowDirectionCommand(TextEditor, _key); break; default: Invariant.Assert(false, "Unexpected key value!"); break; } } // Key associated with the original event. private readonly Key _key; // Modifier state when the original event fired. private readonly ModifierKeys _modifiers; } // ---------------------------------------------------------- // // Merge Typing Undo Units // // ---------------------------------------------------------- #region Merge Typing Undo Units ////// The helper for typing undo unit merging. /// Supposed to be called in the beginning of typing block - /// before making any changes. /// Assumes that CloseTypingUndoUnit method will be called /// after the change is completed. /// private static void OpenTypingUndoUnit(TextEditor This) { UndoManager undoManager = This._GetUndoManager(); if (undoManager != null && undoManager.IsEnabled) { if (This._typingUndoUnit != null && undoManager.LastUnit == This._typingUndoUnit && !This._typingUndoUnit.Locked) { undoManager.Reopen(This._typingUndoUnit); } else { This._typingUndoUnit = new TextParentUndoUnit(This.Selection); undoManager.Open(This._typingUndoUnit); } } } ////// The helper for typing undo unit megring. /// Supposed to be called at the end of typing block - /// after all changes are done. /// Assumes that OpenTypingUndoUnit method was called /// in the beginning of this sequence. /// private static void CloseTypingUndoUnit(TextEditor This, UndoCloseAction closeAction) { UndoManager undoManager = This._GetUndoManager(); if (undoManager != null && undoManager.IsEnabled) { if (This._typingUndoUnit != null && undoManager.LastUnit == This._typingUndoUnit && !This._typingUndoUnit.Locked) { if (This._typingUndoUnit is TextParentUndoUnit) { ((TextParentUndoUnit)This._typingUndoUnit).RecordRedoSelectionState(); } undoManager.Close(This._typingUndoUnit, closeAction); } } else { This._typingUndoUnit = null; } } ////// StartInputCorrection command QueryStatus handler /// private static void OnQueryStatusNYI(object target, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } args.CanExecute = true; } #endregion Merge Typing Undo Units // MouseMoveEvent listener. private static void OnMouseMove(object sender, MouseEventArgs e) { // Un-vanish the cursor on any mouse move. _ShowCursor(); } // MouseMoveEvent listener. // We only need this event because of the edge case where // moving the mouse from the outermost pixel of the UiScope to // another UIElement's real estate doesn't raise a MouseMoveEvent. private static void OnMouseLeave(object sender, MouseEventArgs e) { // Un-vanish the cursor on any mouse leave. _ShowCursor(); } // Hides the mouse cursor when the user starts typing. private static void HideCursor(TextEditor This) { if (!TextEditor._ThreadLocalStore.HideCursor && SystemParameters.MouseVanish && This.UiScope.IsMouseOver) { TextEditor._ThreadLocalStore.HideCursor = true; SafeNativeMethods.ShowCursor(false); } } // When the mouse cursor is over a Hyperlink, force a cursor update // to display the "hand" cursor appropriately. private static void UpdateHyperlinkCursor(TextEditor This) { if (This.UiScope is RichTextBox && This.TextView != null && This.TextView.IsValid) { TextPointer pointer = (TextPointer)This.TextView.GetTextPositionFromPoint(Mouse.GetPosition(This.TextView.RenderScope), false); if (pointer != null && pointer.Parent is TextElement && TextSchema.HasHyperlinkAncestor((TextElement)pointer.Parent)) { Mouse.UpdateCursor(); } } } #endregion Private Methods } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------------------- // // File: TextEditorTyping.cs // // Copyright (C) Microsoft Corporation. All rights reserved. // // Description: Text editing service for controls. // //--------------------------------------------------------------------------- namespace System.Windows.Documents { using MS.Internal; using System.Globalization; using System.Threading; using System.ComponentModel; using System.Text; using System.Collections; // ArrayList using System.Runtime.InteropServices; using System.Windows.Threading; using System.Windows.Input; using System.Windows.Controls; // ScrollChangedEventArgs using System.Windows.Controls.Primitives; // CharacterCasing, TextBoxBase using System.Windows.Media; using System.Windows.Markup; using System.Security; using System.Security.Permissions; using System.Windows.Interop; using MS.Utility; using MS.Win32; using MS.Internal.Documents; using MS.Internal.Commands; // CommandHelpers ////// Subcomponent of TextEditor class - Support for Typing /// internal static class TextEditorTyping { //----------------------------------------------------- // // Class Internal Methods // //----------------------------------------------------- #region Class Internal Methods ////// Registes all handlers needed for text editing control functioning. /// /// /// A type of control for which typing component is registered /// /// /// If registerEventListeners is false, caller is responsible for calling OnXXXEvent methods on TextEditor from /// UIElement and FrameworkElement virtual overrides (piggy backing on the /// UIElement/FrameworkElement class listeners). If true, TextEditor will register /// its own class listener for events it needs. /// /// This method will always register private command listeners. /// internal static void _RegisterClassHandlers(Type controlType, bool registerEventListeners) { if (registerEventListeners) { EventManager.RegisterClassHandler(controlType, Keyboard.KeyDownEvent, new KeyEventHandler(OnKeyDown)); EventManager.RegisterClassHandler(controlType, Keyboard.KeyUpEvent, new KeyEventHandler(OnKeyUp)); EventManager.RegisterClassHandler(controlType, TextCompositionManager.TextInputEvent, new TextCompositionEventHandler(OnTextInput)); } var onEnterBreak = new ExecutedRoutedEventHandler(OnEnterBreak); var onSpace = new ExecutedRoutedEventHandler(OnSpace); var onQueryStatusNYI = new CanExecuteRoutedEventHandler(OnQueryStatusNYI); var onQueryStatusEnterBreak = new CanExecuteRoutedEventHandler(OnQueryStatusEnterBreak); EventManager.RegisterClassHandler(controlType, Mouse.MouseMoveEvent, new MouseEventHandler(OnMouseMove), true /* handledEventsToo */); EventManager.RegisterClassHandler(controlType, Mouse.MouseLeaveEvent, new MouseEventHandler(OnMouseLeave), true /* handledEventsToo */); CommandHelpers.RegisterCommandHandler(controlType, ApplicationCommands.CorrectionList , new ExecutedRoutedEventHandler(OnCorrectionList) , new CanExecuteRoutedEventHandler(OnQueryStatusCorrectionList) , SRID.KeyCorrectionList, SRID.KeyCorrectionListDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.ToggleInsert , new ExecutedRoutedEventHandler(OnToggleInsert) , onQueryStatusNYI , SRID.KeyToggleInsert, SRID.KeyToggleInsertDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Delete , new ExecutedRoutedEventHandler(OnDelete) , onQueryStatusNYI , SRID.KeyDelete, SRID.KeyDeleteDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.DeleteNextWord , new ExecutedRoutedEventHandler(OnDeleteNextWord) , onQueryStatusNYI , SRID.KeyDeleteNextWord, SRID.KeyDeleteNextWordDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.DeletePreviousWord , new ExecutedRoutedEventHandler(OnDeletePreviousWord) , onQueryStatusNYI , SRID.KeyDeletePreviousWord, SRID.KeyDeletePreviousWordDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.EnterParagraphBreak , onEnterBreak , onQueryStatusEnterBreak , SRID.KeyEnterParagraphBreak, SRID.KeyEnterParagraphBreakDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.EnterLineBreak , onEnterBreak , onQueryStatusEnterBreak , SRID.KeyEnterLineBreak, SRID.KeyEnterLineBreakDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.TabForward , new ExecutedRoutedEventHandler(OnTabForward) , new CanExecuteRoutedEventHandler(OnQueryStatusTabForward) , SRID.KeyTabForward, SRID.KeyTabForwardDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.TabBackward , new ExecutedRoutedEventHandler(OnTabBackward) , new CanExecuteRoutedEventHandler(OnQueryStatusTabBackward) , SRID.KeyTabBackward, SRID.KeyTabBackwardDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Space , onSpace , onQueryStatusNYI , SRID.KeySpace, SRID.KeySpaceDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.ShiftSpace , onSpace , onQueryStatusNYI , SRID.KeyShiftSpace, SRID.KeyShiftSpaceDisplayString ); CommandHelpers.RegisterCommandHandler(controlType, EditingCommands.Backspace , new ExecutedRoutedEventHandler(OnBackspace) , onQueryStatusNYI , KeyGesture.CreateFromResourceStrings(SR.Get(SRID.KeyBackspace), SR.Get(SRID.KeyBackspaceDisplayString)), KeyGesture.CreateFromResourceStrings(SR.Get(SRID.KeyShiftBackspace), SR.Get(SRID.KeyShiftBackspaceDisplayString)) ); } ////// Add the input language changed event handler and save it /// into UIContext data slot. /// internal static void _AddInputLanguageChangedEventHandler(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; Invariant.Assert(This._dispatcher == null); This._dispatcher = Dispatcher.CurrentDispatcher; Invariant.Assert(This._dispatcher != null); threadLocalStore = TextEditor._ThreadLocalStore; // Only add the input language changed event handler once that safe per UIContext if (threadLocalStore.InputLanguageChangeEventHandlerCount == 0) { // Add input changed event handler into InputLanguageManager InputLanguageManager.Current.InputLanguageChanged += new InputLanguageEventHandler(OnInputLanguageChanged); // Add the dispatcher shutdown finished event handler to remove InputLanguageChangedEventHandler // before dispose the dispatcher. Dispatcher.CurrentDispatcher.ShutdownFinished += new EventHandler(OnDispatcherShutdownFinished); } threadLocalStore.InputLanguageChangeEventHandlerCount++; } ////// Remove the input language changed event handler from UIContext data slot. /// internal static void _RemoveInputLanguageChangedEventHandler(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; threadLocalStore = TextEditor._ThreadLocalStore; // Decrease the input language changed event handler reference count threadLocalStore.InputLanguageChangeEventHandlerCount--; // Remove the input language changed event handler when nobody reference it if (threadLocalStore.InputLanguageChangeEventHandlerCount == 0) { // Remove InputLanguageEventHandler InputLanguageManager.Current.InputLanguageChanged -= new InputLanguageEventHandler(OnInputLanguageChanged); // Remove the dispatcher shutdown finished event handler Dispatcher.CurrentDispatcher.ShutdownFinished -= new EventHandler(OnDispatcherShutdownFinished); } } ////// Discards previous typing undo unit, to prevent /// from merging it with the subsequent typing. /// internal static void _BreakTypingSequence(TextEditor This) { // Discard typing undo unit This._typingUndoUnit = null; } // Handles any pending input events. internal static void _FlushPendingInputItems(TextEditor This) { TextEditorThreadLocalStore threadLocalStore; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } threadLocalStore = TextEditor._ThreadLocalStore; if (threadLocalStore.PendingInputItems != null) { try { for (int i = 0; i < threadLocalStore.PendingInputItems.Count; i++) { ((InputItem)threadLocalStore.PendingInputItems[i]).Do(); // After the first dequeue, clear the bit that tracks if // any events are handled after ctl+shift (change flow direction keyboard hotkey). threadLocalStore.PureControlShift = false; } } finally { threadLocalStore.PendingInputItems.Clear(); } } // Clear the bit that tracks if any events are handled after // ctl+shift (change flow direction keyboard hotkey) one last // time, in case the queue was empty. // // Because we only call this method in preparation for handling // a Command, we want this bit cleared. threadLocalStore.PureControlShift = false; } // Un-hides the mouse cursor. internal static void _ShowCursor() { if (TextEditor._ThreadLocalStore.HideCursor) { TextEditor._ThreadLocalStore.HideCursor = false; SafeNativeMethods.ShowCursor(true); } } // ................................................................ // // Event Handlers // // ................................................................ // KeyDownEvent handler - needed for handling FlowDirection commands on KeyUp internal static void OnKeyDown(object sender, KeyEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || (This.IsReadOnly && !This.IsReadOnlyCaretVisible) || !This._IsSourceInScope(e.OriginalSource)) { return; } // Ignore repeated events generated when the key is hold down for long time if (e.IsRepeat) { return; } // If UiScope has a ToolTip and it is open, any keyboard/mouse activity should close the tooltip. This.CloseToolTip(); TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; // Clear a flag indicating that Shift key was pressed without any following key // This flag is necessary for KeyUp(RightShift/LeftShift) processing. threadLocalStore.PureControlShift = false; // Shift+Ctrl combination must be executed only when it's "pure" - // no mouse dragging/movement, no other key downs involved in a gesture. if (This.TextView != null && !This.UiScope.IsMouseCaptured) { if ((e.Key == Key.RightShift || e.Key == Key.LeftShift) && // (e.KeyboardDevice.Modifiers & ModifierKeys.Control) != 0 && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { threadLocalStore.PureControlShift = true; // will be cleared by any other key down } else if ((e.Key == Key.RightCtrl || e.Key == Key.LeftCtrl) && // (e.KeyboardDevice.Modifiers & ModifierKeys.Shift) != 0 && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { threadLocalStore.PureControlShift = true; // will be cleared by any other key down } else if (e.Key == Key.RightCtrl || e.Key == Key.LeftCtrl) { UpdateHyperlinkCursor(This); } } } // Handler for KeyUp events internal static void OnKeyUp(object sender, KeyEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || (This.IsReadOnly && !This.IsReadOnlyCaretVisible) || !This._IsSourceInScope(e.OriginalSource)) { return; } // Delegate the work to specific handlers. switch (e.Key) { case Key.RightShift: case Key.LeftShift: if (TextEditor._ThreadLocalStore.PureControlShift && (e.KeyboardDevice.Modifiers & ModifierKeys.Alt) == 0) { TextEditorTyping.ScheduleInput(This, new KeyUpInputItem(This, e.Key, e.KeyboardDevice.Modifiers)); } break; case Key.LeftCtrl: case Key.RightCtrl: UpdateHyperlinkCursor(This); break; } } // TextInputEvent handler. internal static void OnTextInput(object sender, TextCompositionEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(e.OriginalSource)) { return; } FrameworkTextComposition composition = e.TextComposition as FrameworkTextComposition; // Ignore any event with an empty Text property. // The public TextCompositionEventArgs ctor allows null Text values. // Also it's possible to have non-null ControlText or AltText with String.Empty Text values. if (composition == null && (e.Text == null || e.Text.Length == 0)) { return; } // Consider event handled e.Handled = true; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } // If this event is our Cicero TextStore composition, we always handles through ITextStore::SetText. if (composition != null) { if (composition.Owner == This.TextStore) { This.TextStore.UpdateCompositionText(composition); } else if (composition.Owner == This.ImmComposition) { This.ImmComposition.UpdateCompositionText(composition); } } else { // Input text (with springload formatting if any) // We'll delay the event handling, batching it up with other // input if layout is too slow to keep up with the input stream. KeyboardDevice keyboard = e.Device as KeyboardDevice; TextEditorTyping.ScheduleInput(This, new TextInputItem(This, e.Text, /*isInsertKeyToggled:*/keyboard != null ? keyboard.IsKeyToggled(Key.Insert) : false)); } } #endregion Class Internal Methods //------------------------------------------------------ // // Private Methods // //----------------------------------------------------- #region Private Methods // ................................................................ // // Command Handlers // // ................................................................ ////// CorrectionList command QueryStatus handler /// private static void OnQueryStatusCorrectionList(object target, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } if (This.TextStore != null) { // Don't do actual reconversion, it just checks if the current selection is reconvertable. args.CanExecute = This.TextStore.QueryRangeOrReconvertSelection( /*fDoReconvert:*/ false); } else { // If there is no textstore, this command is not enabled. args.CanExecute = false; } } ////// CorrectionList command event handler. /// private static void OnCorrectionList(object target, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } if (This.TextStore != null) { This.TextStore.QueryRangeOrReconvertSelection( /*fDoReconvert:*/ true); } } ////// ToggleInsert command handler /// ////// Critical:This code toggles the state of the insert key and in doing so touches Cicero code which is critical (StartTrasitoryExtension and StopTransitoryExtension) /// TreatAsSafe: This code can do no harm and any state change can be reversed by hitting toggle again /// [SecurityCritical,SecurityTreatAsSafe] private static void OnToggleInsert(object target, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } This._OvertypeMode = !This._OvertypeMode; // Use Cicero's transitory extension for OverTyping. if (TextServicesLoader.ServicesInstalled && (This.TextStore != null)) { TextServicesHost tsfHost = TextServicesHost.Current; if (tsfHost != null) { if (This._OvertypeMode) { TextServicesHost.StartTransitoryExtension(This.TextStore); } else { TextServicesHost.StopTransitoryExtension(This.TextStore); } } } } // ........................................................................... // // Delete Characters // // ........................................................................... private static void OnDelete(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(args.Source)) { return; } TextEditorTyping._FlushPendingInputItems(This); // Note, that Delete and Backspace keys behave differently. ((TextSelection)This.Selection).ClearSpringloadFormatting(); // Forget previously suggested horizontal position TextEditorSelection._ClearSuggestedX(This); using (This.Selection.DeclareChangeBlock()) { ITextPointer position = This.Selection.End; if (This.Selection.IsEmpty) { ITextPointer deletePosition = position.GetNextInsertionPosition(LogicalDirection.Forward); if (deletePosition == null) { // Nothing to delete. return; } if (TextPointerBase.IsAtRowEnd(deletePosition)) { // Backspace and delete are a no-op at row end positions. return; } if (position is TextPointer && !IsAtListItemStart(deletePosition) && HandleDeleteWhenStructuralBoundaryIsCrossed(This, (TextPointer)position, (TextPointer)deletePosition)) { // We are crossing structural boundary and // selection was updated in HandleDeleteWhenStructuralBoundaryIsCrossed. return; } // Selection is empty, extend selection forward to delete the following char. This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Forward); } // Delete selected text. This.Selection.Text = String.Empty; } } private static void OnBackspace(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(args.Source)) { return; } TextEditorTyping._FlushPendingInputItems(This); // Forget previously suggested horizontal position. TextEditorSelection._ClearSuggestedX(This); using (This.Selection.DeclareChangeBlock()) { ITextPointer position = This.Selection.Start; // Note that this is different than the previous insertion position in backward direction, // in case of combining characters and surrogates. ITextPointer backspacePosition = null; // In case when selection is empty we will need to expand // it backward. Check first whether we are crossing // any structural boundary - to disable the operation // in such case. if (This.Selection.IsEmpty) { // Identify a case for special actions in the beginning of paragraphs or list items if (This.AcceptsRichContent && IsAtListItemStart(position)) { // Remove a bullet from this list item. // Note that doing anything more aggressive like unindenting // would make backspace very inconvenient for merging two same-level list items. TextRangeEditLists.ConvertListItemsToParagraphs((TextRange)This.Selection); } else if (This.AcceptsRichContent && (IsAtListItemChildStart(position, false /* emptyChildOnly */) || IsAtIndentedParagraphOrBlockUIContainerStart(This.Selection.Start))) { // Unindent the list by one level. TextEditorLists.DecreaseIndentation(This); } else { // Find a preceding position. ITextPointer deletePosition = position.GetNextInsertionPosition(LogicalDirection.Backward); if (deletePosition == null) { // Nothing to delete. ((TextSelection)This.Selection).ClearSpringloadFormatting(); return; } if (TextPointerBase.IsAtRowEnd(deletePosition)) { // Backspace and delete are a no-op at row end positions. ((TextSelection)This.Selection).ClearSpringloadFormatting(); return; } if (position is TextPointer && HandleDeleteWhenStructuralBoundaryIsCrossed(This, (TextPointer)position, (TextPointer)deletePosition)) { // We are crossing structural boundary and // selection was updated in HandleDeleteWhenStructuralBoundaryIsCrossed. return; } // Normalize the current position backward. position = position.GetFrozenPointer(LogicalDirection.Backward); // If TextView is valid, we can get the backspace position from TextView and then // delete the content from the backspace position to the current position. // Otherwise, we move the selection to the previous insertion position then delete. if (This.TextView != null && position.HasValidLayout && position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text) { // Get the backspace caret unit position from TextView that support surrogate // and all internal characters backspacePosition = This.TextView.GetBackspaceCaretUnitPosition(position); Invariant.Assert(backspacePosition != null); // bug 1733868 // backspacePosition should always be less than position. // But backspacing before '\n' (no preceding '\r') exposes // this bug. if (backspacePosition.CompareTo(position) == 0) { // As of 6/30/2006 we're too close to ship to fix // this bug cleanly. Ideally, we would stop referencing // the position at the end-of-line (which mil text does not // consider a valid position), and instead reference the start // of the next line (flipping the original position's gravity). // // As a work-around, take the previous insertion position, // ignoring glyph level backspace positions. // This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Backward); backspacePosition = null; } // If there is no text preceding the backspacePosition, extend to the next // insertion position to make sure we cleanup any empty Inlines left // after the delete. We don't want a non-empty selection if there is // bordering text, because we might normalize outside of a run of combining // marks otherwise. else if (backspacePosition.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.Text) { This.Selection.Select(This.Selection.End, backspacePosition); backspacePosition = null; } } else { // Selection is empty, extend it backward to include the preceeding char. This.Selection.ExtendToNextInsertionPosition(LogicalDirection.Backward); } } } // Save current formatting properties for springload formatting before backspace // Note, that Delete and Backspace keys behave differently: it's by design. if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); ((TextSelection)This.Selection).SpringloadCurrentFormatting(); } // If backspace position is available from TextView, we can delete it directly // without the normalization. Because we already normalized the backspace position. if (backspacePosition != null) { Invariant.Assert(backspacePosition.CompareTo(position) < 0); // Delete the content from the backspace to the current position backspacePosition.DeleteContentToPosition(position); } else { // Delete selected text This.Selection.Text = String.Empty; position = This.Selection.Start; } // Set the caret position with the Backward direction, // because we want to appear close to previous character. // However, we do not allow to stop at end of line. // We alow to stop next to space - to be consistent with typing behavior. This.Selection.SetCaretToPosition(position, LogicalDirection.Backward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/true); } } // Helper for OnDelete/OnBackspace, handles special case scenarios for delete when table or BlockUIContainer boundaries are crossed. // Returns true if passed positions were in this category and appropriate editing action was taken for handling delete operation. // Otherwise, returns false. private static bool HandleDeleteWhenStructuralBoundaryIsCrossed(TextEditor This, TextPointer position, TextPointer deletePosition) { if (!TextRangeEditTables.IsTableStructureCrossed(position, deletePosition) && !IsBlockUIContainerBoundaryCrossed(position, deletePosition) && !TextPointerBase.IsAtRowEnd(position)) { return false; } LogicalDirection directionOfDelete = position.CompareTo(deletePosition) < 0 ? LogicalDirection.Forward : LogicalDirection.Backward; Block paragraphOrBlockUIContainerToDelete = position.ParagraphOrBlockUIContainer; // Check if an empty paragraph or BlockUIContainer needs to be deleted. if (paragraphOrBlockUIContainerToDelete != null) { if (directionOfDelete == LogicalDirection.Forward) { // if (paragraphOrBlockUIContainerToDelete.NextBlock != null && paragraphOrBlockUIContainerToDelete is Paragraph && Paragraph.HasNoTextContent((Paragraph)paragraphOrBlockUIContainerToDelete) || // empty paragraph paragraphOrBlockUIContainerToDelete is BlockUIContainer && paragraphOrBlockUIContainerToDelete.IsEmpty) // empty BlockUIContainer { paragraphOrBlockUIContainerToDelete.RepositionWithContent(null); } } else { if (paragraphOrBlockUIContainerToDelete.PreviousBlock != null && paragraphOrBlockUIContainerToDelete is Paragraph && Paragraph.HasNoTextContent((Paragraph)paragraphOrBlockUIContainerToDelete) || // empty paragraph paragraphOrBlockUIContainerToDelete is BlockUIContainer && paragraphOrBlockUIContainerToDelete.IsEmpty) // empty BlockUIContainer { paragraphOrBlockUIContainerToDelete.RepositionWithContent(null); } } } // Set caret position. This.Selection.SetCaretToPosition(deletePosition, directionOfDelete, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/true); if (directionOfDelete == LogicalDirection.Backward) { // Clear springload formatting in case of backspace ((TextSelection)This.Selection).ClearSpringloadFormatting(); } return true; } // Tests if the position is at the beginning of indented paragraph - // to allow Backspace to decrease indentation private static bool IsAtIndentedParagraphOrBlockUIContainerStart(ITextPointer position) { if ((position is TextPointer) && TextPointerBase.IsAtParagraphOrBlockUIContainerStart(position)) { Block paragraphOrBlockUIContainer = ((TextPointer)position).ParagraphOrBlockUIContainer; if (paragraphOrBlockUIContainer != null) { FlowDirection flowDirection = paragraphOrBlockUIContainer.FlowDirection; Thickness margin = paragraphOrBlockUIContainer.Margin; return flowDirection == FlowDirection.LeftToRight && margin.Left > 0 || flowDirection == FlowDirection.RightToLeft && margin.Right > 0 || (paragraphOrBlockUIContainer is Paragraph && ((Paragraph)paragraphOrBlockUIContainer).TextIndent > 0); } } return false; } // Tests if the position is at the beginning of some list item - // to allow Backspace to delete the bullet. private static bool IsAtListItemStart(ITextPointer position) { // Check for empty ListItem case if (typeof(ListItem).IsAssignableFrom(position.ParentType) && position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) { return true; } while (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) { Type parentType = position.ParentType; if (TextSchema.IsBlock(parentType)) { if (TextSchema.IsParagraphOrBlockUIContainer(parentType)) { position = position.GetNextContextPosition(LogicalDirection.Backward); if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && typeof(ListItem).IsAssignableFrom(position.ParentType)) { return true; } } return false; } position = position.GetNextContextPosition(LogicalDirection.Backward); } return false; } // Tests if a position is at the start of a Block // within a ListItem. // // position must be normalized at an insertion point. private static bool IsAtListItemChildStart(ITextPointer position, bool emptyChildOnly) { if (position.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart) { return false; } if (emptyChildOnly && position.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.ElementEnd) { return false; } ITextPointer navigator = position.CreatePointer(); // Cross inline opening tags. while (navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && typeof(Inline).IsAssignableFrom(navigator.ParentType)) { navigator.MoveToElementEdge(ElementEdge.BeforeStart); } // Check if navigator is at the start of a block. if (!(navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && TextSchema.IsParagraphOrBlockUIContainer(navigator.ParentType))) { return false; } // Move just past the block. navigator.MoveToElementEdge(ElementEdge.BeforeStart); return typeof(ListItem).IsAssignableFrom(navigator.ParentType); } // Tests if position1 and position2 cross a BlockUIContainer boundary. private static bool IsBlockUIContainerBoundaryCrossed(TextPointer position1, TextPointer position2) { return (position1.Parent is BlockUIContainer || position2.Parent is BlockUIContainer) && position1.Parent != position2.Parent; } // ........................................................................... // // Delete Words // // ........................................................................... private static void OnDeleteNextWord(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange) { return; } TextEditorTyping._FlushPendingInputItems(This); ITextPointer wordBoundary = This.Selection.End.CreatePointer(); // When selection is not empty the command deletes selected content // without extending it to the word bopundary. For empty selection // the command deletes a content from caret position to // nearest word boundary in a given direction if (This.Selection.IsEmpty) { TextPointerBase.MoveToNextWordBoundary(wordBoundary, LogicalDirection.Forward); } if (TextRangeEditTables.IsTableStructureCrossed(This.Selection.Start, wordBoundary)) { return; } ITextRange textRange = new TextRange(This.Selection.Start, wordBoundary); // When a range is TableCellRange we do not want to make deletions if (textRange.IsTableCellRange) { return; } if (!textRange.IsEmpty) { using (This.Selection.DeclareChangeBlock()) { // Note asymetry with Backspace: we do not load springload formatting here if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); } This.Selection.Select(textRange.Start, textRange.End); // Delete selected text This.Selection.Text = String.Empty; } } } private static void OnDeletePreviousWord(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange) { // return; } TextEditorTyping._FlushPendingInputItems(This); ITextPointer wordBoundary = This.Selection.Start.CreatePointer(); // When selection is not empty the command deletes selected content // without extending it to the word bopundary. For empty selection // the command deletes a content from caret position to // nearest word boundary in a given direction if (This.Selection.IsEmpty) { TextPointerBase.MoveToNextWordBoundary(wordBoundary, LogicalDirection.Backward); } // When the movement to word boundary crosses table structure, ignore the command if (TextRangeEditTables.IsTableStructureCrossed(wordBoundary, This.Selection.Start)) { return; } // Build a range from a start of a word preceding start of selection, ending at the end of whole selection // This range is supposed to be deleted by the operation. ITextRange textRange = new TextRange(wordBoundary, This.Selection.End); // When a range is TableCellRange we do not want to make deletions if (textRange.IsTableCellRange) { return; } if (!textRange.IsEmpty) { using (This.Selection.DeclareChangeBlock()) { // Note asymetry with Backspace: we DO load springload formatting here if (This.AcceptsRichContent) { ((TextSelection)This.Selection).ClearSpringloadFormatting(); This.Selection.Select(textRange.Start, textRange.End); ((TextSelection)This.Selection).SpringloadCurrentFormatting(); } else { This.Selection.Select(textRange.Start, textRange.End); } // Delete selected text This.Selection.Text = String.Empty; } } } // ........................................................................... // // Enter Breaks // // ........................................................................... ////// EnterParagraphBreak/EnterLineBreak command QueryStatus handler /// private static void OnQueryStatusEnterBreak(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { args.ContinueRouting = true; return; } if (This.Selection.IsTableCellRange || !This.AcceptsReturn) { args.ContinueRouting = true; return; } args.CanExecute = true; } // EnterParagraphBreak/EnterLineBreak command handler private static void OnEnterBreak(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } if (This.Selection.IsTableCellRange || !This.AcceptsReturn || !This.UiScope.IsKeyboardFocused) { return; } TextEditorTyping._FlushPendingInputItems(This); // We do not merge Enter typing with other typing - for better undo structuring using (This.Selection.DeclareChangeBlock()) { // Flag to indicate if selection was changed. It may be unaffected in following cases: // 1. In plain text case, Environment.NewLine can not fit in MaxLength // 2. In rich text case, we cannot split a hyperlink ancestor to insert a paragraph break bool wasSelectionChanged; if (This.AcceptsRichContent && This.Selection.Start is TextPointer) { // Paragraph insertion for the case of rich text wasSelectionChanged = HandleEnterBreakForRichText(This, args.Command); } else { // Newline insertion for plain text wasSelectionChanged = HandleEnterBreakForPlainText(This); } // Update caret and clear SuggestedX only when selection has changed. if (wasSelectionChanged) { // Position the caret. This.Selection.SetCaretToPosition(This.Selection.End, LogicalDirection.Forward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); // Forget previously suggested horizontal position TextEditorSelection._ClearSuggestedX(This); } } } // Helper for OnEnterBreak for rich text case private static bool HandleEnterBreakForRichText(TextEditor This, ICommand command) { bool wasSelectionChanged = true; // Save current inline settings to continue on the next paragraph ((TextSelection)This.Selection).SpringloadCurrentFormatting(); if (!This.Selection.IsEmpty) { // Delete selected content This.Selection.Text = String.Empty; } if (HandleEnterBreakWhenStructuralBoundaryIsCrossed(This, command)) { // We are crossing structural boundary and // selection was updated if HandleEnterBreakWhenStructuralBoundaryIsCrossed returned true } else { TextPointer newEnd = ((TextSelection)This.Selection).End; if (command == EditingCommands.EnterParagraphBreak) { if (newEnd.HasNonMergeableInlineAncestor && !TextPointerBase.IsPositionAtNonMergeableInlineBoundary(newEnd)) { // Selection end is in the middle of a hyperlink element, enter is a no-op. wasSelectionChanged = false; } else { newEnd = TextRangeEdit.InsertParagraphBreak(newEnd, /*moveIntoSecondParagraph*/true); } } else if (command == EditingCommands.EnterLineBreak) { newEnd = newEnd.InsertLineBreak(); } if (wasSelectionChanged) { This.Selection.Select(newEnd, newEnd); } } return wasSelectionChanged; } // Helper for OnEnterBreak for plain text case private static bool HandleEnterBreakForPlainText(TextEditor This) { bool wasSelectionChanged = true; // Filter Environment.NewLine based on TextBox.MaxLength string filteredText = This._FilterText(Environment.NewLine, This.Selection); if (filteredText != String.Empty) { This.Selection.Text = Environment.NewLine; } else { // Do not update selection if Environment.NewLine can not fit in. wasSelectionChanged = false; } return wasSelectionChanged; } // Helper for rich text OnEnterBreak case, handles special cases when a // structural boundary such as listitem, table, blockuicontainer is crossed. private static bool HandleEnterBreakWhenStructuralBoundaryIsCrossed(TextEditor This, ICommand command) { Invariant.Assert(This.Selection.Start is TextPointer); TextPointer position = (TextPointer)This.Selection.Start; bool structuralBoundaryIsCrossed = true; if (TextPointerBase.IsAtRowEnd(position)) { // For both ParagraphBreak and LineBreak commands, insert a new row after the current one TextRange range = ((TextSelection)This.Selection).InsertRows(+1); This.Selection.SetCaretToPosition(range.Start, LogicalDirection.Forward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); } else if (This.Selection.IsEmpty && (TextPointerBase.IsInEmptyListItem(position) || IsAtListItemChildStart(position, true /* emptyChildOnly */)) && command == EditingCommands.EnterParagraphBreak) { // Unindent the list by one level. TextEditorLists.DecreaseIndentation(This); } else if (TextPointerBase.IsBeforeFirstTable(position) || TextPointerBase.IsAtBlockUIContainerStart(position)) { // Calling EnsureInsertionPosition has the effect of inserting a paragraph BEFORE the table or BlockUIContainer/Table. // In this case, we do not want to move selection end to the paragraph just created. TextRangeEditTables.EnsureInsertionPosition(position); } else if (TextPointerBase.IsAtBlockUIContainerEnd(position)) { // Calling EnsureInsertionPosition has the effect of inserting a paragraph AFTER the BlockUIContainer. // Update selection end to position in the following paragraph. TextPointer newEnd = TextRangeEditTables.EnsureInsertionPosition(position); This.Selection.Select(newEnd, newEnd); } else { structuralBoundaryIsCrossed = false; } return structuralBoundaryIsCrossed; } // ........................................................................... // // Flow Direction // // ........................................................................... ////// LeftToRightFlowDirection command event handler. /// private static void OnFlowDirectionCommand(TextEditor This, Key key) { // using (This.Selection.DeclareChangeBlock()) { if (key == Key.LeftShift) { if (This.AcceptsRichContent && (This.Selection is TextSelection)) { // NOTE: We do not call OnApplyProperty to avoid recursion for FlushPendingInput ((TextSelection)This.Selection).ApplyPropertyValue(FlowDocument.FlowDirectionProperty, FlowDirection.LeftToRight, /*applyToParagraphs*/true); } else { Invariant.Assert(This.UiScope != null); UIElementPropertyUndoUnit.Add(This.TextContainer, This.UiScope, FrameworkElement.FlowDirectionProperty, FlowDirection.LeftToRight); This.UiScope.SetValue(FrameworkElement.FlowDirectionProperty, FlowDirection.LeftToRight); } } else { Invariant.Assert(key == Key.RightShift); if (This.AcceptsRichContent && (This.Selection is TextSelection)) { // NOTE: We do not call OnApplyProperty to avoid recursion for FlushPendingInput ((TextSelection)This.Selection).ApplyPropertyValue(FlowDocument.FlowDirectionProperty, FlowDirection.RightToLeft, /*applyToParagraphs*/true); } else { Invariant.Assert(This.UiScope != null); UIElementPropertyUndoUnit.Add(This.TextContainer, This.UiScope, FrameworkElement.FlowDirectionProperty, FlowDirection.RightToLeft); This.UiScope.SetValue(FrameworkElement.FlowDirectionProperty, FlowDirection.RightToLeft); } } ((TextSelection)This.Selection).UpdateCaretState(CaretScrollMethod.Simple); } } // ........................................................................... // // In some controls, Space and Shift+Space keys are mapped to // scroll down and scroll up commands respectively. // In TextEditor, we handle them as text input. // Using the command system allows controls to override the existing default behavior. // ........................................................................... // Space, Shift+Space handler private static void OnSpace(object sender, ExecutedRoutedEventArgs e) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly || !This._IsSourceInScope(e.OriginalSource)) { return; } // If this event is our Cicero TextStore composition, we always handles through ITextStore::SetText. if (This.TextStore != null && This.TextStore.IsComposing) { return; } if (This.ImmComposition != null && This.ImmComposition.IsComposition) { return; } // Consider event handled e.Handled = true; if (This.TextView != null) { This.TextView.ThrottleBackgroundTasksForUserInput(); } ScheduleInput(This, new TextInputItem(This, " ", /*isInsertKeyToggled:*/!This._OvertypeMode)); } // ........................................................................... // // Tab and Back-Tab // // ........................................................................... ////// ForwardTabStop command QueryStatus handler /// private static void OnQueryStatusTabForward(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This != null && This.AcceptsTab) { args.CanExecute = true; } else { args.ContinueRouting = true; } } ////// BackwardTabStop command QueryStatus handler /// private static void OnQueryStatusTabBackward(object sender, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This != null && This.AcceptsTab) { args.CanExecute = true; } else { args.ContinueRouting = true; } } // Tab handler. private static void OnTabForward(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } TextEditorTyping._FlushPendingInputItems(This); if (HandleTabInTables(This, LogicalDirection.Forward)) { // All done on table level. return; } if (This.AcceptsRichContent && (!This.Selection.IsEmpty || TextPointerBase.IsAtParagraphOrBlockUIContainerStart(This.Selection.Start)) && EditingCommands.IncreaseIndentation.CanExecute(null, (IInputElement)sender)) { // In RichTextBox Tab/Shift+Tab keys work as paragraph/list indentation EditingCommands.IncreaseIndentation.Execute(null, (IInputElement)sender); } else { // In plain text we treat tab as a characters always DoTextInput(This, "\t", /*isInsertKeyToggled:*/!This._OvertypeMode, /*acceptControlCharacters:*/true); } } // Shift+Tab handler. private static void OnTabBackward(object sender, ExecutedRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(sender); if (This == null || !This._IsEnabled || This.IsReadOnly) { return; } // TextEditorTyping._FlushPendingInputItems(This); if (HandleTabInTables(This, LogicalDirection.Backward)) { // All done on table level. return; } if (This.AcceptsRichContent && (!This.Selection.IsEmpty || TextPointerBase.IsAtParagraphOrBlockUIContainerStart(This.Selection.Start)) && EditingCommands.DecreaseIndentation.CanExecute(null, (IInputElement)sender)) { // In RichTextBox Tab/Shift+Tab keys work as paragraph/list indentation EditingCommands.DecreaseIndentation.Execute(null, (IInputElement)sender); } else { // In plain text we treat tab as a characters always DoTextInput(This, "\t", /*isInsertKeyToggled:*/!This._OvertypeMode, /*acceptControlCharacters:*/true); } } // Command handler for Tab and ShiftTab - moves caret between table cells // if the selection is within a table. Otherwise does nothing and returns false. private static bool HandleTabInTables(TextEditor This, LogicalDirection direction) { if (!This.AcceptsRichContent) { return false; } if (This.Selection.IsTableCellRange) { // When table cell range is selected, Tab simply collapses // a selection to a content of a first cell This.Selection.SetCaretToPosition(This.Selection.Start, LogicalDirection.Backward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false); return true; } if (This.Selection.IsEmpty && TextPointerBase.IsAtRowEnd(This.Selection.End)) { // From the end of row we go to the first cell of a next row TableCell cell = null; TableRow row = ((TextPointer)This.Selection.End).Parent as TableRow; Invariant.Assert(row != null); TableRowGroup body = row.RowGroup; int rowIndex = body.Rows.IndexOf(row); if (direction == LogicalDirection.Forward) { if (rowIndex + 1 < body.Rows.Count) { cell = body.Rows[rowIndex + 1].Cells[0]; } } else { if (rowIndex > 0) { cell = body.Rows[rowIndex - 1].Cells[body.Rows[rowIndex - 1].Cells.Count - 1]; } } if (cell != null) { This.Selection.Select(cell.ContentStart, cell.ContentEnd); } return true; } // Check if selection is within a table TextElement parent = ((TextPointer)This.Selection.Start).Parent as TextElement; while (parent != null && !(parent is TableCell)) { parent = parent.Parent as TextElement; } if (parent is TableCell) { TableCell cell = (TableCell)parent; TableRow row = cell.Row; TableRowGroup body = row.RowGroup; int cellIndex = row.Cells.IndexOf(cell); int rowIndex = body.Rows.IndexOf(row); if (direction == LogicalDirection.Forward) { if (cellIndex + 1 < row.Cells.Count) { cell = row.Cells[cellIndex + 1]; } else if (rowIndex + 1 < body.Rows.Count) { cell = body.Rows[rowIndex + 1].Cells[0]; } else { // } } else { if (cellIndex > 0) { cell = row.Cells[cellIndex - 1]; } else if (rowIndex > 0) { cell = body.Rows[rowIndex - 1].Cells[body.Rows[rowIndex - 1].Cells.Count - 1]; } else { // } } Invariant.Assert(cell != null); This.Selection.Select(cell.ContentStart, cell.ContentEnd); return true; } return false; } // ...................................................... // // Handling Text Input // // ...................................................... ////// This is a single method used to insert user input characters. /// It takes care of typing undo, springload formatting, overtype mode etc. /// /// /// /// Text to insert. /// /// /// Reflects a state of Insert key at the moment of textData input. /// /// /// True indicates that control characters like '\t' or '\r' etc. can be inserted. /// False means that all control characters are filtered out. /// private static void DoTextInput(TextEditor This, string textData, bool isInsertKeyToggled, bool acceptControlCharacters) { // Hide the mouse cursor on user input. HideCursor(This); // Remove control characters. Note that this is not included into _FilterText, // because we want such kind of filtering only for real input, // not for copy/paste. if (!acceptControlCharacters) { for (int i = 0; i < textData.Length; i++) { if (Char.IsControl(textData[i])) { textData = textData.Remove(i--, 1); // decrement i to compensate for character removal } } } string filteredText = This._FilterText(textData, This.Selection); if (filteredText.Length == 0) { return; } TextEditorTyping.OpenTypingUndoUnit(This); UndoCloseAction closeAction = UndoCloseAction.Rollback; try { using (This.Selection.DeclareChangeBlock()) { This.Selection.ApplyTypingHeuristics(This.AllowOvertype && This._OvertypeMode && filteredText != "\t"); This.SetSelectedText(filteredText, InputLanguageManager.Current.CurrentInputLanguage); // Create caret position normalized backward to keep formatting of a character just typed ITextPointer caretPosition = This.Selection.End.CreatePointer(LogicalDirection.Backward); // Set selection at the end of input content This.Selection.SetCaretToPosition(caretPosition, LogicalDirection.Backward, /*allowStopAtLineEnd:*/true, /*allowStopNearSpace:*/true); // Note: Using explicit backward orientation we keep formatting with // a previous character during typing. closeAction = UndoCloseAction.Commit; } } finally { TextEditorTyping.CloseTypingUndoUnit(This, closeAction); } } // Takes state originating with a KeyDownEvent or TextInputEvent and // schedules it for eventual handling. // // Normally we delay handling until a Background priority event fires. // This has the effect of batching multiple input events when // layout cannot keep up with the input stream. // // However, if any mouse events are pending, we handle the event // immediately, since otherwise we risk the possibility of handling // the events out of order. private static void ScheduleInput(TextEditor This, InputItem item) { if (!This.AcceptsRichContent || IsMouseInputPending(This)) { // We have to do the work now, or we'll get out of synch. TextEditorTyping._FlushPendingInputItems(This); item.Do(); } else { TextEditorThreadLocalStore threadLocalStore; threadLocalStore = TextEditor._ThreadLocalStore; if (threadLocalStore.PendingInputItems == null) { threadLocalStore.PendingInputItems = new ArrayList(1); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(BackgroundInputCallback), This); } threadLocalStore.PendingInputItems.Add(item); } } // Returns true if any mouse input event is currently waiting in the // win32 message queue for processing. // Avalon doesn't keep a separate queue for input events. Instead // it interleaves work items with the win32 input queue. ////// Critical - Calls PeekMessage, and accesses the root window /// TreatAsSafe - The information it returns is safe to return. /// [SecurityCritical,SecurityTreatAsSafe] private static bool IsMouseInputPending(TextEditor This) { bool mouseInputPending = false; IWin32Window win32Window = PresentationSource.CriticalFromVisual(This.UiScope) as IWin32Window; if (win32Window != null) { IntPtr hwnd = IntPtr.Zero; new UIPermission(UIPermissionWindow.AllWindows).Assert(); // BlessedAssert try { hwnd = win32Window.Handle; } finally { UIPermission.RevertAssert(); } if (hwnd != (IntPtr)0) { System.Windows.Interop.MSG message = new System.Windows.Interop.MSG(); mouseInputPending = UnsafeNativeMethods.PeekMessage(ref message, new HandleRef(null, hwnd), NativeMethods.WM_MOUSEFIRST, NativeMethods.WM_MOUSELAST, NativeMethods.PM_NOREMOVE); } } return mouseInputPending; } // Background priority callback used to process keystrokes. private static object BackgroundInputCallback(object This) { TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; Invariant.Assert(This is TextEditor); Invariant.Assert(threadLocalStore.PendingInputItems != null); try { TextEditorTyping._FlushPendingInputItems((TextEditor)This); } finally { threadLocalStore.PendingInputItems = null; } return null; } ////// Callback for shutdown finished dispatcher. Before shutdown dispatcher, we should clean /// InputLanguageChangedEventHandler. /// private static void OnDispatcherShutdownFinished(object sender, EventArgs args) { // Remove the dispatcher shutdown finished event handler Dispatcher.CurrentDispatcher.ShutdownFinished -= new EventHandler(OnDispatcherShutdownFinished); // Remove the input language changed event handler InputLanguageManager.Current.InputLanguageChanged -= new InputLanguageEventHandler(OnInputLanguageChanged); TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore; // Clear InputLanguageChangeEventHandler count threadLocalStore.InputLanguageChangeEventHandlerCount = 0; } // InputLanguageChanged handler. private static void OnInputLanguageChanged(object sender, InputLanguageEventArgs e) { TextSelection.OnInputLanguageChanged(e.NewLanguage); } // Base class for keyboard/text input items. // Individual keystroke/text events are batched and handled together // when layout cannot keep up with the input stream. private abstract class InputItem { // Ctor. internal InputItem(TextEditor textEditor) { _textEditor = textEditor; } // Handles the input event. internal abstract void Do(); // The TextEditor instance on which this input item applies. TextEditor _textEditor; protected TextEditor TextEditor { get { return _textEditor; } } } // Holds state originating from a single TextInputEvent. private class TextInputItem : InputItem { // Ctor. internal TextInputItem(TextEditor textEditor, string text, bool isInsertKeyToggled) : base (textEditor) { _text = text; _isInsertKeyToggled = isInsertKeyToggled; } // Inserts event content into the document. internal override void Do() { if (TextEditor.UiScope == null) { // We dont want to process the input item if the editor has already been detached from its UiScope. return; } DoTextInput(TextEditor, _text, _isInsertKeyToggled, /*acceptControlCharacters:*/false); } // Text to input. private readonly string _text; private readonly bool _isInsertKeyToggled; } // Holds state originating from a single KeyDownEvent. private class KeyUpInputItem : InputItem { // Ctor. internal KeyUpInputItem(TextEditor textEditor, Key key, ModifierKeys modifiers) : base(textEditor) { _key = key; _modifiers = modifiers; } // Fires the command associated with a keystroke. internal override void Do() { if (TextEditor.UiScope == null) { // We dont want to process the input item if the editor has already been detached from its UiScope. return; } // Delegate the work to specific handlers. switch (_key) { case Key.RightShift: // Only support RTL flow direction in case of having the installed // bidi input language. if (TextSelection.IsBidiInputLanguageInstalled() == true) { TextEditorTyping.OnFlowDirectionCommand(TextEditor, _key); } break; case Key.LeftShift: TextEditorTyping.OnFlowDirectionCommand(TextEditor, _key); break; default: Invariant.Assert(false, "Unexpected key value!"); break; } } // Key associated with the original event. private readonly Key _key; // Modifier state when the original event fired. private readonly ModifierKeys _modifiers; } // ---------------------------------------------------------- // // Merge Typing Undo Units // // ---------------------------------------------------------- #region Merge Typing Undo Units ////// The helper for typing undo unit merging. /// Supposed to be called in the beginning of typing block - /// before making any changes. /// Assumes that CloseTypingUndoUnit method will be called /// after the change is completed. /// private static void OpenTypingUndoUnit(TextEditor This) { UndoManager undoManager = This._GetUndoManager(); if (undoManager != null && undoManager.IsEnabled) { if (This._typingUndoUnit != null && undoManager.LastUnit == This._typingUndoUnit && !This._typingUndoUnit.Locked) { undoManager.Reopen(This._typingUndoUnit); } else { This._typingUndoUnit = new TextParentUndoUnit(This.Selection); undoManager.Open(This._typingUndoUnit); } } } ////// The helper for typing undo unit megring. /// Supposed to be called at the end of typing block - /// after all changes are done. /// Assumes that OpenTypingUndoUnit method was called /// in the beginning of this sequence. /// private static void CloseTypingUndoUnit(TextEditor This, UndoCloseAction closeAction) { UndoManager undoManager = This._GetUndoManager(); if (undoManager != null && undoManager.IsEnabled) { if (This._typingUndoUnit != null && undoManager.LastUnit == This._typingUndoUnit && !This._typingUndoUnit.Locked) { if (This._typingUndoUnit is TextParentUndoUnit) { ((TextParentUndoUnit)This._typingUndoUnit).RecordRedoSelectionState(); } undoManager.Close(This._typingUndoUnit, closeAction); } } else { This._typingUndoUnit = null; } } ////// StartInputCorrection command QueryStatus handler /// private static void OnQueryStatusNYI(object target, CanExecuteRoutedEventArgs args) { TextEditor This = TextEditor._GetTextEditor(target); if (This == null) { return; } args.CanExecute = true; } #endregion Merge Typing Undo Units // MouseMoveEvent listener. private static void OnMouseMove(object sender, MouseEventArgs e) { // Un-vanish the cursor on any mouse move. _ShowCursor(); } // MouseMoveEvent listener. // We only need this event because of the edge case where // moving the mouse from the outermost pixel of the UiScope to // another UIElement's real estate doesn't raise a MouseMoveEvent. private static void OnMouseLeave(object sender, MouseEventArgs e) { // Un-vanish the cursor on any mouse leave. _ShowCursor(); } // Hides the mouse cursor when the user starts typing. private static void HideCursor(TextEditor This) { if (!TextEditor._ThreadLocalStore.HideCursor && SystemParameters.MouseVanish && This.UiScope.IsMouseOver) { TextEditor._ThreadLocalStore.HideCursor = true; SafeNativeMethods.ShowCursor(false); } } // When the mouse cursor is over a Hyperlink, force a cursor update // to display the "hand" cursor appropriately. private static void UpdateHyperlinkCursor(TextEditor This) { if (This.UiScope is RichTextBox && This.TextView != null && This.TextView.IsValid) { TextPointer pointer = (TextPointer)This.TextView.GetTextPositionFromPoint(Mouse.GetPosition(This.TextView.RenderScope), false); if (pointer != null && pointer.Parent is TextElement && TextSchema.HasHyperlinkAncestor((TextElement)pointer.Parent)) { Mouse.UpdateCursor(); } } } #endregion Private Methods } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- ExpressionBuilder.cs
- EtwTrace.cs
- SafeArrayRankMismatchException.cs
- ExtractedStateEntry.cs
- XmlSchemaException.cs
- Stack.cs
- CommonXSendMessage.cs
- CatalogPartChrome.cs
- XamlTemplateSerializer.cs
- BlurBitmapEffect.cs
- ComponentFactoryHelpers.cs
- ScriptControlDescriptor.cs
- Restrictions.cs
- ScriptingAuthenticationServiceSection.cs
- NameTable.cs
- TextAction.cs
- ALinqExpressionVisitor.cs
- WorkflowViewManager.cs
- MsmqIntegrationMessageProperty.cs
- SqlNotificationRequest.cs
- InfiniteIntConverter.cs
- XmlSchemaInclude.cs
- HorizontalAlignConverter.cs
- NotFiniteNumberException.cs
- StoreItemCollection.cs
- XmlConvert.cs
- counter.cs
- Rotation3D.cs
- ConfigUtil.cs
- IdentityNotMappedException.cs
- WebPartTransformerCollection.cs
- AutoGeneratedFieldProperties.cs
- PassportAuthenticationEventArgs.cs
- DateTimeOffsetAdapter.cs
- SamlAuthenticationStatement.cs
- WrapPanel.cs
- PerformanceCounterLib.cs
- HtmlObjectListAdapter.cs
- WeakReadOnlyCollection.cs
- WebPartDisplayMode.cs
- ListViewSelectEventArgs.cs
- Configuration.cs
- BindingParameterCollection.cs
- ServiceAuthorizationBehavior.cs
- ConditionCollection.cs
- SqlDataSourceStatusEventArgs.cs
- HttpModuleAction.cs
- XmlAttributeOverrides.cs
- ChannelHandler.cs
- RectangleGeometry.cs
- WindowsPrincipal.cs
- DataGridViewAutoSizeColumnsModeEventArgs.cs
- AssertFilter.cs
- StreamDocument.cs
- MasterPage.cs
- QueueProcessor.cs
- SecurityException.cs
- DynamicMethod.cs
- HttpRawResponse.cs
- WebPartChrome.cs
- SemanticValue.cs
- PeerTransportListenAddressConverter.cs
- XmlSchemaAnyAttribute.cs
- SymbolTable.cs
- XmlSchemaDatatype.cs
- RuntimeComponentFilter.cs
- DBCommand.cs
- CommandID.cs
- ItemMap.cs
- EmptyElement.cs
- HttpRequestWrapper.cs
- ChangePassword.cs
- PkcsMisc.cs
- MbpInfo.cs
- DataSvcMapFileSerializer.cs
- Stack.cs
- ClientSettingsStore.cs
- DateBoldEvent.cs
- HttpClientProtocol.cs
- PeerCollaboration.cs
- SqlCharStream.cs
- PageBreakRecord.cs
- ProxyGenerationError.cs
- BamlTreeNode.cs
- BooleanProjectedSlot.cs
- SpeakProgressEventArgs.cs
- QilName.cs
- _LoggingObject.cs
- XmlObjectSerializerReadContextComplexJson.cs
- LicenseContext.cs
- Models.cs
- FormViewUpdatedEventArgs.cs
- SizeAnimationClockResource.cs
- UidPropertyAttribute.cs
- UdpRetransmissionSettings.cs
- XmlParser.cs
- MimeTypePropertyAttribute.cs
- BamlStream.cs
- WindowsFormsEditorServiceHelper.cs
- Font.cs