|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MS.Internal;
using System.Globalization;
using System.Windows.Controls.Primitives; // TextBoxBase
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.IO;
using MS.Win32;
using System.Windows.Controls;
//
// Description: Holds and manipulates the text selection state for TextEditor.
//
namespace System.Windows.Documents
{
/// <summary>
/// The TextSelection class encapsulates selection state for the RichTextBox
/// control. It has no public constructor, but is exposed via a public
/// property on RichTextBox.
/// </summary>
public sealed class TextSelection : TextRange, ITextSelection
{
#region Constructors
//------------------------------------------------------
//
// Constructors
//
//------------------------------------------------------
// Contstructor.
// TextSelection does not have a public constructor. It is only accessible
// through TextEditor's Selection property.
internal TextSelection(TextEditor textEditor)
: base(textEditor.TextContainer.Start, textEditor.TextContainer.Start)
{
ITextSelection thisSelection = (ITextSelection)this;
Invariant.Assert(textEditor.UiScope != null);
// Attach the selection to its editor
_textEditor = textEditor;
// Initialize active pointers of the selection - anchor and moving pointers
SetActivePositions(/*AnchorPosition:*/thisSelection.Start, thisSelection.End);
// Activate selection in case if this control has keyboard focus already
thisSelection.UpdateCaretAndHighlight();
}
#endregion Constructors
// *****************************************************
// *****************************************************
// *****************************************************
//
// Abstract TextSelection Implementation
//
// *****************************************************
// *****************************************************
// *****************************************************
//------------------------------------------------------
//
// ITextRange implementation
//
//------------------------------------------------------
#region ITextRange Implementation
//......................................................
//
// Selection Building
//
//......................................................
/// <summary>
/// <see cref="System.Windows.Documents.TextRange.Select"/>
/// </summary>
void ITextRange.Select(ITextPointer anchorPosition, ITextPointer movingPosition)
{
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.Select(this, anchorPosition, movingPosition);
SetActivePositions(anchorPosition, movingPosition);
}
finally
{
TextRangeBase.EndChange(this);
}
}
/// <summary>
/// <see cref="System.Windows.Documents.TextRange.SelectWord"/>
/// </summary>
void ITextRange.SelectWord(ITextPointer position)
{
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.SelectWord(this, position);
ITextSelection thisSelection = this;
SetActivePositions(/*anchorPosition:*/thisSelection.Start, thisSelection.End);
}
finally
{
TextRangeBase.EndChange(this);
}
}
/// <summary>
/// <see cref="System.Windows.Documents.TextRange.SelectParagraph"/>
/// </summary>
void ITextRange.SelectParagraph(ITextPointer position)
{
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.SelectParagraph(this, position);
// Check whether this behave correctly on double-click-drag; double-Click-extendwithkeyboard
ITextSelection thisSelection = this;
SetActivePositions(/*anchorPosition:*/position, thisSelection.End);
}
finally
{
TextRangeBase.EndChange(this);
}
}
/// <summary>
/// <see cref="System.Windows.Documents.ITextRange.ApplyTypingHeuristics"/>
/// </summary>
void ITextRange.ApplyTypingHeuristics(bool overType)
{
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.ApplyInitialTypingHeuristics(this);
// For non-empty selection start with saving current formatting
if (!this.IsEmpty && _textEditor.AcceptsRichContent)
{
SpringloadCurrentFormatting();
// Note: we springload formatting before overtype expansion
}
TextRangeBase.ApplyFinalTypingHeuristics(this, overType);
}
finally
{
TextRangeBase.EndChange(this);
}
}
/// <summary>
/// <see cref="System.Windows.Documents.TextRange.GetPropertyValue"/>
/// </summary>
object ITextRange.GetPropertyValue(DependencyProperty formattingProperty)
{
object propertyValue;
if (this.IsEmpty && TextSchema.IsCharacterProperty(formattingProperty))
{
// For empty selection return springloaded formatting (only character formatting properties can be springloaded)
propertyValue = ((TextSelection)this).GetCurrentValue(formattingProperty);
}
else
{
// Otherwise return base implementation from a TextRange.
propertyValue = TextRangeBase.GetPropertyValue(this, formattingProperty);
}
return propertyValue;
}
//......................................................
//
// Plain Text Modification
//
//......................................................
//------------------------------------------------------
//
// Overrides
//
//------------------------------------------------------
// Set true if a Changed event is pending.
bool ITextRange._IsChanged
{
get
{
return _IsChanged;
}
set
{
// TextStore needs to know about state changes
// from false to true.
if (!_IsChanged && value)
{
if (this.TextStore != null)
{
this.TextStore.OnSelectionChange();
}
if (this.ImmComposition != null)
{
this.ImmComposition.OnSelectionChange();
}
}
_IsChanged = value;
}
}
/// <summary>
/// </summary>
void ITextRange.NotifyChanged(bool disableScroll, bool skipEvents)
{
// Notify text store about selection movement.
if (this.TextStore != null)
{
this.TextStore.OnSelectionChanged();
}
// Notify ImmComposition about selection movement.
if (this.ImmComposition != null)
{
this.ImmComposition.OnSelectionChanged();
}
if (!skipEvents)
{
TextRangeBase.NotifyChanged(this, disableScroll);
}
if (!disableScroll)
{
// Force a synchronous layout update. If the update was big enough, background layout
// kicked in and the caret won't otherwise be updated. Note this will block the thread
// while layout runs.
//
// It's possible an application repositioned the caret
// while listening to a change event just raised, but in that case the following
// code should be harmless.
ITextPointer movingPosition = ((ITextSelection)this).MovingPosition;
if (this.TextView != null && this.TextView.IsValid &&
!this.TextView.Contains(movingPosition))
{
movingPosition.ValidateLayout();
}
// If layout wasn't valid, then there's a pending caret update
// that will proceed correctly now. Otherwise the whole operation
// is a nop.
}
// Fixup the caret.
UpdateCaretState(disableScroll ? CaretScrollMethod.None : CaretScrollMethod.Simple);
}
//------------------------------------------------------
//
// ITextRange Properties
//
//------------------------------------------------------
//......................................................
//
// Content - rich and plain
//
//......................................................
/// <summary>
/// <see cref="System.Windows.Documents.TextRange.Text"/>
/// </summary>
string ITextRange.Text
{
get
{
return TextRangeBase.GetText(this);
}
set
{
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.SetText(this, value);
if (this.IsEmpty)
{
// We need to ensure appropriate caret visual position.
((ITextSelection)this).SetCaretToPosition(((ITextRange)this).End, LogicalDirection.Forward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false);
}
Invariant.Assert(!this.IsTableCellRange);
SetActivePositions(((ITextRange)this).Start, ((ITextRange)this).End);
}
finally
{
TextRangeBase.EndChange(this);
}
}
}
#endregion ITextRange Implementation
//------------------------------------------------------
//
// ITextSelection implementation
//
//------------------------------------------------------
#region ITextSelection Implementation
void ITextSelection.UpdateCaretAndHighlight()
{
FrameworkElement uiScope = UiScope;
FrameworkElement owner = CaretElement.GetOwnerElement(uiScope);
bool showSelection = false;
bool isBlinkEnabled = false;
bool isSelectionActive = false;
if (uiScope.IsEnabled && this.TextView != null)
{
if (uiScope.IsKeyboardFocused)
{
showSelection = true;
isBlinkEnabled = true;
isSelectionActive = true;
}
else if (uiScope.IsFocused &&
((IsRootElement(FocusManager.GetFocusScope(uiScope)) && IsFocusWithinRoot()) || // either UiScope root window has keyboard focus
_textEditor.IsContextMenuOpen))// or UiScope has a context menu open
{
showSelection = true;
isBlinkEnabled = false;
isSelectionActive = true;
}
else if (!this.IsEmpty && (bool)owner.GetValue(TextBoxBase.IsInactiveSelectionHighlightEnabledProperty))
{
showSelection = true;
isBlinkEnabled = false;
isSelectionActive = false;
}
}
owner.SetValue(TextBoxBase.IsSelectionActivePropertyKey, isSelectionActive);
if (showSelection)
{
if (isSelectionActive)
{
// Update the TLS first, so that EnsureCaret is working in
// the correct context. Please note that TLS is only relevant
// when selection is active and the caret is showing. If the
// Adorner is only meant to render the selection highlight then
// the TLS is irrelavant.
SetThreadSelection();
}
// Create Adorner that will render both the caret and selection highlight
EnsureCaret(isBlinkEnabled, isSelectionActive, CaretScrollMethod.None);
// Highlight selection
Highlight();
}
else
{
// Update the TLS first, so that the caret is working in
// the correct context.
ClearThreadSelection();
// delete the caret
DetachCaretFromVisualTree();
// Remove highlight
Unhighlight();
}
}
/// <summary>
/// </summary>
ITextPointer ITextSelection.AnchorPosition
{
get
{
Invariant.Assert(this.IsEmpty || _anchorPosition != null);
Invariant.Assert(_anchorPosition == null || _anchorPosition.IsFrozen);
return this.IsEmpty ? ((ITextSelection)this).Start : _anchorPosition;
}
}
/// <summary>
/// The position within this selection that responds to user input.
/// </summary>
ITextPointer ITextSelection.MovingPosition
{
get
{
ITextSelection thisSelection = this;
ITextPointer movingPosition;
if (this.IsEmpty)
{
movingPosition = thisSelection.Start;
}
else
{
switch (_movingPositionEdge)
{
case MovingEdge.Start:
movingPosition = thisSelection.Start;
break;
case MovingEdge.StartInner:
movingPosition = thisSelection.TextSegments[0].End;
break;
case MovingEdge.EndInner:
movingPosition = thisSelection.TextSegments[thisSelection.TextSegments.Count - 1].Start;
break;
case MovingEdge.End:
movingPosition = thisSelection.End;
break;
case MovingEdge.None:
default:
Invariant.Assert(false, "MovingEdge should never be None with non-empty TextSelection!");
movingPosition = null;
break;
}
movingPosition = movingPosition.GetFrozenPointer(_movingPositionDirection);
}
return movingPosition;
}
}
/// <summary>
/// </summary>
void ITextSelection.SetCaretToPosition(ITextPointer caretPosition, LogicalDirection direction, bool allowStopAtLineEnd, bool allowStopNearSpace)
{
// We need a pointer with appropriate direction,
// becasue it will be used in textRangeBase.Select method for
// pointer normalization.
caretPosition = caretPosition.CreatePointer(direction);
// Normalize the position in its logical direction - to get to text content over there.
caretPosition.MoveToInsertionPosition(direction);
// We need a pointer with the reverse direction to confirm
// the line wrapping position. So we can ensure Bidi caret navigation.
// Bidi can have the different caret position by setting the
// logical direction, so we have to only set the logical direction
// as the forward for the real line wrapping position.
ITextPointer reversePosition = caretPosition.CreatePointer(direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward);
// Check line wrapping condition
if (!allowStopAtLineEnd &&
((TextPointerBase.IsAtLineWrappingPosition(caretPosition, this.TextView) &&
TextPointerBase.IsAtLineWrappingPosition(reversePosition, this.TextView)) ||
TextPointerBase.IsNextToPlainLineBreak(caretPosition, LogicalDirection.Backward) ||
TextSchema.IsBreak(caretPosition.GetElementType(LogicalDirection.Backward))))
{
// Caret is at wrapping position, and we are not allowed to stay at end of line,
// so we choose forward direction to appear in the begiinning of a second line
caretPosition.SetLogicalDirection(LogicalDirection.Forward);
}
else
{
if (caretPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.Text &&
caretPosition.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
// This is statistically most typical case. No "smartness" needed
// to choose standard Forward orientation for the caret.
// NOTE: By using caretPosition's direction we solve BiDi caret orientation case:
// The orietnation reflects a direction from where caret has been moved
// or orientation where mouse clicked. So we will stick with appropriate
// character.
// Nothing to do. The caretPosition is good to go.
}
else if (!allowStopNearSpace)
{
// There are some tags around, and we are not allowed to choose a side near to space.
// So we need to perform some content analysis.
char[] charBuffer = new char[1];
if (caretPosition.GetPointerContext(direction) == TextPointerContext.Text &&
caretPosition.GetTextInRun(direction, charBuffer, 0, 1) == 1 &&
Char.IsWhiteSpace(charBuffer[0]))
{
LogicalDirection oppositeDirection = direction == LogicalDirection.Forward ? LogicalDirection.Backward : LogicalDirection.Forward;
// Check formatting switch condition at this position
FlowDirection initialFlowDirection = (FlowDirection)caretPosition.GetValue(FrameworkElement.FlowDirectionProperty);
bool moved = caretPosition.MoveToInsertionPosition(oppositeDirection);
if (moved &&
initialFlowDirection == (FlowDirection)caretPosition.GetValue(FrameworkElement.FlowDirectionProperty) &&
(caretPosition.GetPointerContext(oppositeDirection) != TextPointerContext.Text ||
caretPosition.GetTextInRun(oppositeDirection, charBuffer, 0, 1) != 1 ||
!Char.IsWhiteSpace(charBuffer[0])))
{
// In the opposite direction we have a non-space
// character. So we choose that direction
direction = oppositeDirection;
caretPosition.SetLogicalDirection(direction);
}
}
}
}
// Now that orientation of a caretPosition is identified,
// build an empty selection at this position
TextRangeBase.BeginChange(this);
try
{
TextRangeBase.Select(this, caretPosition, caretPosition);
// Note how Select method works for the case of empty range:
// It creates a single instance TextPointer normalized and oriented
// in a direction taken from caretPosition:
ITextSelection thisSelection = this;
Invariant.Assert(thisSelection.Start.LogicalDirection == caretPosition.LogicalDirection); // orientation must be as passed
Invariant.Assert(this.IsEmpty);
//Invariant.Assert((object)thisSelection.Start == (object)thisSelection.End); // it must be the same instance of TextPointer
//Invariant.Assert(TextPointerBase.IsAtInsertionPosition(thisSelection.Start, caretPosition.LogicalDirection)); // normalization must be done in the same diredction as orientation
// Clear active positions when selection is empty
SetActivePositions(null, null);
}
finally
{
TextRangeBase.EndChange(this);
}
}
//......................................................
//
// Extending via movingEnd movements
//
//......................................................
// Worker for ExtendToPosition, handles all ITextContainers.
void ITextSelection.ExtendToPosition(ITextPointer position)
{
TextRangeBase.BeginChange(this);
try
{
ITextSelection thisSelection = (ITextSelection)this;
// Store initial anchor position
ITextPointer anchorPosition = thisSelection.AnchorPosition;
//Build new selection
TextRangeBase.Select(thisSelection, anchorPosition, position);
// Store active positions.
SetActivePositions(anchorPosition, position);
}
finally
{
TextRangeBase.EndChange(this);
}
}
// Worker for ExtendToNextInsertionPosition, handles all ITextContainers.
bool ITextSelection.ExtendToNextInsertionPosition(LogicalDirection direction)
{
bool moved = false;
TextRangeBase.BeginChange(this);
try
{
ITextPointer anchorPosition = ((ITextSelection)this).AnchorPosition;
ITextPointer movingPosition = ((ITextSelection)this).MovingPosition;
ITextPointer newMovingPosition;
if (this.IsTableCellRange)
{
// Both moving and anchor positions are within a single Table, in seperate
// cells. Select next cell.
newMovingPosition = TextRangeEditTables.GetNextTableCellRangeInsertionPosition(this, direction);
}
else if (movingPosition is TextPointer && TextPointerBase.IsAtRowEnd(movingPosition))
{
// Moving position at at a Table row end, anchor position is outside
// the Table. Select next row.
newMovingPosition = TextRangeEditTables.GetNextRowEndMovingPosition(this, direction);
}
else if (movingPosition is TextPointer && TextRangeEditTables.MovingPositionCrossesCellBoundary(this))
{
// Moving position at at a Table row start, anchor position is outside
// the Table. Select next row.
newMovingPosition = TextRangeEditTables.GetNextRowStartMovingPosition(this, direction);
}
else
{
// No Table logic.
newMovingPosition = GetNextTextSegmentInsertionPosition(direction);
}
if (newMovingPosition == null && direction == LogicalDirection.Forward)
{
// When moving forward we cannot find next insertion position, set the end of selection after the last paragraph
// (which is not an insertion position)
if (movingPosition.CompareTo(movingPosition.TextContainer.End) != 0)
{
newMovingPosition = movingPosition.TextContainer.End;
}
}
// Now that new movingPosition is prepared, build the new selection
if (newMovingPosition != null)
{
moved = true;
// Re-build a range for the new pair of positions
TextRangeBase.Select(this, anchorPosition, newMovingPosition);
// Make sure that active positions are inside of a selection
// Set the moving position direction to point toward the inner content.
LogicalDirection contentDirection = (anchorPosition.CompareTo(newMovingPosition) <= 0) ?
LogicalDirection.Backward : LogicalDirection.Forward;
newMovingPosition = newMovingPosition.GetFrozenPointer(contentDirection);
SetActivePositions(anchorPosition, newMovingPosition);
}
}
finally
{
TextRangeBase.EndChange(this);
}
return moved;
}
// Finds new movingPosition for the selection when it is in TextSegment state.
// Returns null if there is no next insertion position in the requested direction.
private ITextPointer GetNextTextSegmentInsertionPosition(LogicalDirection direction)
{
ITextSelection thisSelection = (ITextSelection)this;
// Move one over symbol in a given direction
return thisSelection.MovingPosition.GetNextInsertionPosition(direction);
}
bool ITextSelection.Contains(Point point)
{
ITextSelection thisSelection = (ITextSelection)this;
if (thisSelection.IsEmpty)
{
return false;
}
if (this.TextView == null || !this.TextView.IsValid)
{
return false;
}
bool contains = false;
ITextPointer position = this.TextView.GetTextPositionFromPoint(point, /*snapToText:*/false);
// Did we hit any text?
if (position != null && thisSelection.Contains(position))
{
// If we did, make sure the range covers at least one full character.
// Check both character edges.
position = position.GetNextInsertionPosition(position.LogicalDirection);
if (position != null && thisSelection.Contains(position))
{
contains = true;
}
}
// Point snapped to text did not hit anything, but we still have a chance
// to hit selection - in inter-paragraph or end-of-paragraph areas - highlighted by selection
if (!contains)
{
if (_caretElement != null && _caretElement.SelectionGeometry != null &&
_caretElement.SelectionGeometry.FillContains(point))
{
contains = true;
}
}
return contains;
}
#region ITextSelection Implementation
//......................................................
//
// Interaction with Selection host
//
//......................................................
// Called by TextEditor.OnDetach, when the behavior is shut down.
void ITextSelection.OnDetach()
{
ITextSelection thisSelection = (ITextSelection)this;
// Check if we need to deactivate the selection
thisSelection.UpdateCaretAndHighlight();
// Delete highlight layer created for this selection (if any)
if (_highlightLayer != null && thisSelection.Start.TextContainer.Highlights.GetLayer(typeof(TextSelection)) == _highlightLayer)
{
thisSelection.Start.TextContainer.Highlights.RemoveLayer(_highlightLayer);
}
_highlightLayer = null;
// Detach the selection from its editor
_textEditor = null;
}
// ITextView.Updated event listener.
// Called by the TextEditor.
void ITextSelection.OnTextViewUpdated()
{
if ((this.UiScope.IsKeyboardFocused || this.UiScope.IsFocused))
{
// Use the locally defined caretElement because _caretElement can be null by
// detaching CaretElement object
// Stress bug#1583327 indicate that _caretElement can be set to null by
// detaching. So the below code is caching the caret element instance locally.
CaretElement caretElement = _caretElement;
if (caretElement != null)
{
caretElement.OnTextViewUpdated();
}
}
if (_pendingUpdateCaretStateCallback)
{
// The TextView calls this method synchronously, before it finishes its Arrange
// pass, so defer the remaining work until the TextView is valid.
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(UpdateCaretStateWorker), null);
}
}
// Perform any cleanup necessary when removing the current UiScope
// from the visual tree (eg, during a template change).
void ITextSelection.DetachFromVisualTree()
{
DetachCaretFromVisualTree();
}
// Italic command event handler. Called by TextEditor.
void ITextSelection.RefreshCaret()
{
// Update the caret to show it as italic or normal caret.
RefreshCaret(_textEditor, _textEditor.Selection);
}
// This is called from TextStore when the InterimSelection style of the current selection is changed.
void ITextSelection.OnInterimSelectionChanged(bool interimSelection)
{
// Update the caret to show or remove the interim block caret.
UpdateCaretState(CaretScrollMethod.None);
}
//......................................................
//
// Selection Heuristics
//
//......................................................
// Moves the selection to the mouse cursor position.
// Extends the active end if extend == true, otherwise the selection
// is collapsed to a caret.
void ITextSelection.SetSelectionByMouse(ITextPointer cursorPosition, Point cursorMousePoint)
{
ITextSelection thisSelection = (ITextSelection)this;
if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0)
{
// Shift modifier pressed - extending selection from existing one
thisSelection.ExtendSelectionByMouse(cursorPosition, /*forceWordSelection:*/false, /*forceParagraphSelection*/false);
}
else
{
// No shift modifier pressed - move selection to a cursor position
MoveSelectionByMouse(cursorPosition, cursorMousePoint);
}
}
// Extends the selection to the mouse cursor position.
void ITextSelection.ExtendSelectionByMouse(ITextPointer cursorPosition, bool forceWordSelection, bool forceParagraphSelection)
{
ITextSelection thisSelection = (ITextSelection)this;
// Check whether the cursor has been actually moved - compare with the previous position
if (forceParagraphSelection || _previousCursorPosition != null && cursorPosition.CompareTo(_previousCursorPosition) == 0)
{
// Mouse was not actually moved. Ignore the event.
return;
}
thisSelection.BeginChange();
try
{
if (!BeginMouseSelectionProcess(cursorPosition))
{
return;
}
// Get anchor position
ITextPointer anchorPosition = ((ITextSelection)this).AnchorPosition;
// Identify words on selection ends
TextSegment anchorWordRange;
TextSegment cursorWordRange;
IdentifyWordsOnSelectionEnds(anchorPosition, cursorPosition, forceWordSelection, out anchorWordRange, out cursorWordRange);
// Calculate selection boundary positions
ITextPointer startPosition;
ITextPointer movingPosition;
if (anchorWordRange.Start.CompareTo(cursorWordRange.Start) <= 0)
{
startPosition = anchorWordRange.Start.GetFrozenPointer(LogicalDirection.Forward);
movingPosition = cursorWordRange.End.GetFrozenPointer(LogicalDirection.Backward);
}
else
{
startPosition = anchorWordRange.End.GetFrozenPointer(LogicalDirection.Backward);
movingPosition = cursorWordRange.Start.GetFrozenPointer(LogicalDirection.Forward);
}
// Note that we use includeCellAtMovingPosition=true because we want that hit-tested table cell
// be included into selection no matter whether it's empty or not.
TextRangeBase.Select(this, startPosition, movingPosition, /*includeCellAtMovingPosition:*/true);
SetActivePositions(anchorPosition, movingPosition);
// Store previous cursor position - for the next extension event
_previousCursorPosition = cursorPosition.CreatePointer();
Invariant.Assert(thisSelection.Contains(thisSelection.AnchorPosition));
}
finally
{
thisSelection.EndChange();
}
}
// Part of ExtendSelectionByMouse method:
// Checks whether selection has been started and initiates selection process
// Usually always returns true,
// returns false only as a special value indicating that we need to return without executing selection expansion code.
private bool BeginMouseSelectionProcess(ITextPointer cursorPosition)
{
if (_previousCursorPosition == null)
{
// This is a beginning of mouse selection guesture.
// Initialize the guesture state
// initially autoword expansion of both ends is enabled
_anchorWordRangeHasBeenCrossedOnce = false;
_allowWordExpansionOnAnchorEnd = true;
_reenterPosition = null;
if (this.GetUIElementSelected() != null)
{
// This means that we have just received mousedown event and selected embedded element in this event.
// MoveMove event is sent immediately, but we don't want to loose UIElement selection,
// so we do not extend to the cursorPosition now.
_previousCursorPosition = cursorPosition.CreatePointer();
return false;
}
}
return true;
}
// Part of ExtendSelectionByMouse method:
// Identifies words on selection ends.
private void IdentifyWordsOnSelectionEnds(ITextPointer anchorPosition, ITextPointer cursorPosition, bool forceWordSelection, out TextSegment anchorWordRange, out TextSegment cursorWordRange)
{
if (forceWordSelection)
{
anchorWordRange = TextPointerBase.GetWordRange(anchorPosition);
cursorWordRange = TextPointerBase.GetWordRange(cursorPosition, cursorPosition.LogicalDirection);
}
else
{
// Define whether word adjustment is allowed. Pressing Shift+Control prevents from auto-word expansion.
bool disableWordExpansion = _textEditor.AutoWordSelection == false || ((Keyboard.Modifiers & ModifierKeys.Shift) != 0 && (Keyboard.Modifiers & ModifierKeys.Control) != 0);
if (disableWordExpansion)
{
anchorWordRange = new TextSegment(anchorPosition, anchorPosition);
cursorWordRange = new TextSegment(cursorPosition, cursorPosition);
}
else
{
// Autoword expansion heuristics
// -----------------------------
// Word autoword heuristics:
// a) After active end returned to selected area, autoword expansion on active end is disabled
// b) After active end returned to the very first word, expansion on anchor word is disabled either
// We do this though only if selection has crossed initial word boundary at least once.
// c) After active end crosses new word, autoword expansion of active end is enabled again
// Calculate a word range for anchor position
anchorWordRange = TextPointerBase.GetWordRange(anchorPosition);
// Check if we re-entering selection or moving outside
// and set autoexpansion flags accordingly
if (_previousCursorPosition != null &&
(anchorPosition.CompareTo(cursorPosition) < 0 && cursorPosition.CompareTo(_previousCursorPosition) < 0 ||
_previousCursorPosition.CompareTo(cursorPosition) < 0 && cursorPosition.CompareTo(anchorPosition) < 0))
{
// Re-entering selection.
// Store position of reentering
_reenterPosition = cursorPosition.CreatePointer();
// When re-entering reaches initial word, disable word expansion on anchor end either
if (_anchorWordRangeHasBeenCrossedOnce && anchorWordRange.Contains(cursorPosition))
{
_allowWordExpansionOnAnchorEnd = false;
}
}
else
{
// Extending the selection.
// Check if we are crossing a boundary of last reentered word to re-enable word expansion on moving end
if (_reenterPosition != null)
{
TextSegment lastReenteredWordRange = TextPointerBase.GetWordRange(_reenterPosition);
if (!lastReenteredWordRange.Contains(cursorPosition))
{
_reenterPosition = null;
}
}
}
// Identify expanded range on both ends
// We need smarter version of TextSegment.Contains - which would normalize position towards a segmenet internal before comparison
// I did not take a risk to do it before making more thorogh analysis. The three check below is a fix for hotbug #1193240.
if (anchorWordRange.Contains(cursorPosition) ||
anchorWordRange.Contains(cursorPosition.GetInsertionPosition(LogicalDirection.Forward)) ||
anchorWordRange.Contains(cursorPosition.GetInsertionPosition(LogicalDirection.Backward)))
{
// Selection does not cross word boundary, so shrink selection to exact anchor/cursor positions
anchorWordRange = new TextSegment(anchorPosition, anchorPosition);
cursorWordRange = new TextSegment(cursorPosition, cursorPosition);
}
else
{
// Selection crosses word boundary.
_anchorWordRangeHasBeenCrossedOnce = true;
if (!_allowWordExpansionOnAnchorEnd || //
TextPointerBase.IsAtWordBoundary(anchorPosition, /*insideWordDirection:*/LogicalDirection.Forward))
{
// We collapse anchorPosition in two cases:
// If we have been re-entering the initial word before -
// then we treat it as an indicator that user wants exact position on anchor end
// or
// if selection starts exactly on word boundary -
// then we should not include the following word (when selection extends backward).
//
// So in the both cases we collapse anchorWordRange to exact _anchorPosition
anchorWordRange = new TextSegment(anchorPosition, anchorPosition);
}
if (TextPointerBase.IsAfterLastParagraph(cursorPosition) ||
TextPointerBase.IsAtWordBoundary(cursorPosition, /*insideWordDirection:*/LogicalDirection.Forward))
{
cursorWordRange = new TextSegment(cursorPosition, cursorPosition);
}
else
{
if (_reenterPosition == null)
{
// We are not in re-entering mode; expand moving end to word boundary
cursorWordRange = TextPointerBase.GetWordRange(cursorPosition, cursorPosition.LogicalDirection);
}
else
{
// We are in re-entering mode; use exact moving end position
cursorWordRange = new TextSegment(cursorPosition, cursorPosition);
}
}
}
}
}
}
//......................................................
//
// Table Selection
//
//......................................................
/// <summary>
/// Extends table selection by one row in a given direction
/// </summary>
/// <param name="direction">
/// LogicalDirection.Forward means moving active cell one row down,
/// LogicalDirection.Backward - one row up.
/// </param>
bool ITextSelection.ExtendToNextTableRow(LogicalDirection direction)
{
TableCell anchorCell;
TableCell movingCell;
TableRowGroup rowGroup;
int nextRowIndex;
TableCell nextCell;
if (!this.IsTableCellRange)
{
return false;
}
Invariant.Assert(!this.IsEmpty);
Invariant.Assert(_anchorPosition != null);
Invariant.Assert(_movingPositionEdge != MovingEdge.None);
if (!TextRangeEditTables.IsTableCellRange((TextPointer)_anchorPosition, (TextPointer)((ITextSelection)this).MovingPosition, /*includeCellAtMovingPosition:*/false, out anchorCell, out movingCell))
{
return false;
}
Invariant.Assert(anchorCell != null && movingCell != null);
rowGroup = movingCell.Row.RowGroup;
nextCell = null;
if (direction == LogicalDirection.Forward)
{
// Move movingPosition to the following row.
// Find a row in the forward direction
nextRowIndex = movingCell.Row.Index + movingCell.RowSpan;
if (nextRowIndex < rowGroup.Rows.Count)
{
nextCell = FindCellAtColumnIndex(rowGroup.Rows[nextRowIndex].Cells, movingCell.ColumnIndex);
}
}
else
{
// Find preceding row containing a cell in position of movingCell's first column
nextRowIndex = movingCell.Row.Index - 1;
while (nextRowIndex >= 0)
{
nextCell = FindCellAtColumnIndex(rowGroup.Rows[nextRowIndex].Cells, movingCell.ColumnIndex);
if (nextCell != null)
{
break;
}
nextRowIndex--;
}
}
if (nextCell != null)
{
// This check for cell start position is not safe. It gives wrong result on elements nested in Cells
ITextPointer movingPosition = nextCell.ContentEnd.CreatePointer();
movingPosition.MoveToNextInsertionPosition(LogicalDirection.Forward);
TextRangeBase.Select(this, _anchorPosition, movingPosition);
// Make sure that active positions are inside of a selection
SetActivePositions(_anchorPosition, movingPosition);
return true;
}
return false;
}
//------------------------------------------------------
//
// ITextSelection Properties
//
//------------------------------------------------------
// True if the current seleciton is for interim character.
// Korean Interim character is now invisilbe selection (no highlight) and the controls needs to
// have the block caret to indicate the interim character.
// This should be updated by TextStore.
internal bool IsInterimSelection
{
get
{
if (this.TextStore != null)
{
return TextStore.IsInterimSelection;
}
return false;
}
}
// IsInterimSelection wrapper for ITextSelection.
bool ITextSelection.IsInterimSelection
{
get
{
return this.IsInterimSelection;
}
}
#endregion ITextSelection Implementation
// *****************************************************
// *****************************************************
// *****************************************************
//
// Concrete TextSelection Implementation
//
// *****************************************************
// *****************************************************
// *****************************************************
//------------------------------------------------------
//
// Public Methods
//
//------------------------------------------------------
//......................................................
//
// Selection extension (moving active end)
//
//......................................................
/// <summary>
/// Potential public method - concrete equivalent of abstract method
/// </summary>
internal TextPointer AnchorPosition
{
get
{
// ATTENTION: This method is supposed to be a pure redirection
// to corresponding ITextSelection method - to keep abstract and concrete selection behavior consistent
return (TextPointer)((ITextSelection)this).AnchorPosition;
}
}
/// <summary>
/// Potential public method - concrete equivalent of abstract method
/// </summary>
internal TextPointer MovingPosition
{
get
{
// ATTENTION: This method is supposed to be a pure redirection
// to corresponding ITextSelection method - to keep abstract and concrete selection behavior consistent
return (TextPointer)((ITextSelection)this).MovingPosition;
}
}
/// <summary>
/// Potential public method - concrete equivalent of abstract method
/// </summary>
internal void SetCaretToPosition(TextPointer caretPosition, LogicalDirection direction, bool allowStopAtLineEnd, bool allowStopNearSpace)
{
// ATTENTION: This method is supposed to be a pure redirection
// to corresponding ITextSelection method - to keep abstract and concrete selection behavior consistent
((ITextSelection)this).SetCaretToPosition(caretPosition, direction, allowStopAtLineEnd, allowStopNearSpace);
}
// Make this method public
internal bool ExtendToNextInsertionPosition(LogicalDirection direction)
{
// ATTENTION: This method is supposed to be a pure redirection
// to corresponding ITextSelection method - to keep abstract and concrete selection behavior consistent
return ((ITextSelection)this).ExtendToNextInsertionPosition(direction);
}
#endregion Public Methods
#region Internal Methods
//------------------------------------------------------
//
// Internal Methods
//
//------------------------------------------------------
// InputLanguageChanged event handler. Called by TextEditor.
internal static void OnInputLanguageChanged(CultureInfo cultureInfo)
{
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
// Check the changed input language of the cultureInfo to draw the BiDi caret in case of BiDi language
// like as Arabic or Hebrew input language.
if (IsBidiInputLanguage(cultureInfo))
{
threadLocalStore.Bidi = true;
}
else
{
threadLocalStore.Bidi = false;
}
// Update caret on focused text editor
if (threadLocalStore.FocusedTextSelection != null)
{
((ITextSelection)threadLocalStore.FocusedTextSelection).RefreshCaret();
}
}
// Returns true if the text matching a pixel position falls within
// the selection.
internal bool Contains(Point point)
{
return ((ITextSelection)this).Contains(point);
}
//------------------------------------------------------
//
// Internal Virtual Methods - TextSelection Extensibility
//
//------------------------------------------------------
//......................................................
//
// Formatting
//
//......................................................
/// <summary>
/// Append an object at the end of the TextRange.
/// </summary>
internal override void InsertEmbeddedUIElementVirtual(FrameworkElement embeddedElement)
{
TextRangeBase.BeginChange(this);
try
{
base.InsertEmbeddedUIElementVirtual(embeddedElement);
this.ClearSpringloadFormatting();
}
finally
{
TextRangeBase.EndChange(this);
}
}
/// <summary>
/// Applies a property value to a selection.
/// In case of empty selection sets property to springloaded property set.
/// </summary>
internal override void ApplyPropertyToTextVirtual(DependencyProperty formattingProperty, object value, bool applyToParagraphs, PropertyValueAction propertyValueAction)
{
if (!TextSchema.IsParagraphProperty(formattingProperty) && !TextSchema.IsCharacterProperty(formattingProperty))
{
return; // Ignore any unknown property
}
// Check whether we are in a situation when auto-word formatting must happen
if (this.IsEmpty && TextSchema.IsCharacterProperty(formattingProperty) &&
!applyToParagraphs &&
formattingProperty != FrameworkElement.FlowDirectionProperty) // We dont want to apply flowdirection property to inlines when selection is empty.
{
TextSegment autoWordRange = TextRangeBase.GetAutoWord(this);
if (autoWordRange.IsNull)
{
// This property goes to springload formatting. We should not create undo unit for it.
if (_springloadFormatting == null)
{
_springloadFormatting = new DependencyObject();
}
_springloadFormatting.SetValue(formattingProperty, value);
}
else
{
// TextRange will create undo unit with proper name
new TextRange(autoWordRange.Start, autoWordRange.End).ApplyPropertyValue(formattingProperty, value);
}
}
else
{
// No word to auto-format. Apply property to a selection.
// TextRange will create undo unit with proper name
base.ApplyPropertyToTextVirtual(formattingProperty, value, applyToParagraphs, propertyValueAction);
this.ClearSpringloadFormatting();
}
}
internal override void ClearAllPropertiesVirtual()
{
// Character property - applies to text runs in selected range, or springloaded if selection is empty
if (this.IsEmpty)
{
this.ClearSpringloadFormatting();
}
else
{
TextRangeBase.BeginChange(this);
try
{
base.ClearAllPropertiesVirtual();
this.ClearSpringloadFormatting();
}
finally
{
TextRangeBase.EndChange(this);
}
}
}
//......................................................
//
// Range Serialization
//
//......................................................
// Worker for Xml property setter; enables extensibility for TextSelection
internal override void SetXmlVirtual(TextElement fragment)
{
TextRangeBase.BeginChange(this);
try
{
base.SetXmlVirtual(fragment);
this.ClearSpringloadFormatting();
}
finally
{
TextRangeBase.EndChange(this);
}
}
// Worker for Load public method; enables extensibility for TextSelection
internal override void LoadVirtual(Stream stream, string dataFormat)
{
TextRangeBase.BeginChange(this);
try
{
base.LoadVirtual(stream, dataFormat);
this.ClearSpringloadFormatting();
}
finally
{
TextRangeBase.EndChange(this);
}
}
//......................................................
//
// Table Editing
//
//......................................................
// Worker for InsertTable; enables extensibility for TextSelection
internal override Table InsertTableVirtual(int rowCount, int columnCount)
{
using (DeclareChangeBlock())
{
Table table = base.InsertTableVirtual(rowCount, columnCount);
if (table != null)
{
TextPointer cellStart = table.RowGroups[0].Rows[0].Cells[0].ContentStart;
this.SetCaretToPosition(cellStart, LogicalDirection.Backward, /*allowStopAtLineEnd:*/false, /*allowStopNearSpace:*/false);
}
return table;
}
}
// ..............................................................
//
// Springload Formatting
//
// ..............................................................
#region Springload Formatting
/// <summary>
/// Function used in OnApplyProperty commands for character formatting properties.
/// It takes care of springload formatting (applied to empty selection).
/// </summary>
/// <param name="formattingProperty">
/// Property whose value is subject to be toggled.
/// </param>
/// <returns>
/// The value of a property
/// </returns>
internal object GetCurrentValue(DependencyProperty formattingProperty)
{
ITextSelection thisSelection = this;
object propertyValue = DependencyProperty.UnsetValue;
if (thisSelection.Start is TextPointer)
{
if (_springloadFormatting != null && this.IsEmpty)
{
// Get springload value
propertyValue = _springloadFormatting.ReadLocalValue(formattingProperty);
if (propertyValue == DependencyProperty.UnsetValue)
{
propertyValue = this.Start.Parent.GetValue(formattingProperty);
}
}
}
// If there's no spring loaded value, read the local value.
if (propertyValue == DependencyProperty.UnsetValue)
{
propertyValue = this.PropertyPosition.GetValue(formattingProperty);
}
return propertyValue;
}
/// <summary>
/// Reads a set of current formatting properties (from selection start)
/// into _springloadFormatting - to apply potentially for the following input.
/// </summary>
internal void SpringloadCurrentFormatting()
{
if (((ITextSelection)this).Start is TextPointer)
{
TextPointer start = this.Start;
Inline ancestor = start.GetNonMergeableInlineAncestor();
if (ancestor != null)
{
// Unless the selection is wholly contained within a Hyperlink, we don't
// want to springload its character properties.
if (this.End.GetNonMergeableInlineAncestor() != ancestor)
{
start = ancestor.ElementEnd;
}
}
if (_springloadFormatting == null)
{
SpringloadCurrentFormatting(start.Parent);
}
}
}
private void SpringloadCurrentFormatting(DependencyObject parent)
{
// Create new bag for formatting properties
_springloadFormatting = new DependencyObject();
// Check if we have an object to read from
if (parent == null)
{
return;
}
DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(Inline));
DependencyProperty[] noninheritableProperties = TextSchema.GetNoninheritableProperties(typeof(Span));
// Walk up the tree. At each step, if the element is typographical only,
// grab all non-inherited values. When we reach the top of the tree, grab all
// values.
DependencyObject element = parent;
while (element is Inline)
{
TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(element.GetType(), typeof(TextElementEditingBehaviorAttribute));
if (att.IsTypographicOnly)
{
for (int i = 0; i < inheritableProperties.Length; i++)
{
if (_springloadFormatting.ReadLocalValue(inheritableProperties[i]) == DependencyProperty.UnsetValue &&
inheritableProperties[i] != FrameworkElement.LanguageProperty &&
inheritableProperties[i] != FrameworkElement.FlowDirectionProperty &&
System.Windows.DependencyPropertyHelper.GetValueSource(element, inheritableProperties[i]).BaseValueSource != BaseValueSource.Inherited)
{
object value = parent.GetValue(inheritableProperties[i]);
_springloadFormatting.SetValue(inheritableProperties[i], value);
}
}
for (int i = 0; i < noninheritableProperties.Length; i++)
{
if (_springloadFormatting.ReadLocalValue(noninheritableProperties[i]) == DependencyProperty.UnsetValue &&
noninheritableProperties[i] != TextElement.TextEffectsProperty &&
System.Windows.DependencyPropertyHelper.GetValueSource(element, noninheritableProperties[i]).BaseValueSource != BaseValueSource.Inherited)
{
object value = parent.GetValue(noninheritableProperties[i]);
_springloadFormatting.SetValue(noninheritableProperties[i], value);
}
}
}
element = ((TextElement)element).Parent;
}
}
/// <summary>
/// Clears springload formatting, so that the following text input
/// will use formatting from a current position.
/// </summary>
internal void ClearSpringloadFormatting()
{
if (((ITextSelection)this).Start is TextPointer)
{
// Delete all springloaded values
_springloadFormatting = null;
// Update caret italic state
((ITextSelection)this).RefreshCaret();
}
}
/// <summary>
/// Applies springload formatting to a given content range.
/// Clears springloadFormatting after applying it.
/// </summary>
internal void ApplySpringloadFormatting()
{
if (!(((ITextSelection)this).Start is TextPointer))
{
return;
}
if (this.IsEmpty)
{
// We can't apply formatting to non-TextContainers or empty selection.
return;
}
if (_springloadFormatting != null)
{
Invariant.Assert(this.Start.LogicalDirection == LogicalDirection.Backward);
Invariant.Assert(this.End.LogicalDirection == LogicalDirection.Forward);
LocalValueEnumerator springloadFormattingValues = _springloadFormatting.GetLocalValueEnumerator();
while (!this.IsEmpty && springloadFormattingValues.MoveNext())
{
// Note: we repeatedly check for IsEmpty because the selection
// may become empty as a result of normalization after formatting
// (thai character sequence).
LocalValueEntry propertyEntry = springloadFormattingValues.Current;
Invariant.Assert(TextSchema.IsCharacterProperty(propertyEntry.Property));
base.ApplyPropertyValue(propertyEntry.Property, propertyEntry.Value);
}
ClearSpringloadFormatting();
}
}
#endregion Springload Formatting
#region Caret Support
// ..............................................................
//
// Caret Support
//
// ..............................................................
// Shows/hides the caret and scrolls it into view if requested.
// Called when the range is moved or layout is updated.
internal void UpdateCaretState(CaretScrollMethod caretScrollMethod)
{
Invariant.Assert(caretScrollMethod != CaretScrollMethod.Unset);
if (_pendingCaretNavigation)
{
caretScrollMethod = CaretScrollMethod.Navigation;
_pendingCaretNavigation = false;
}
if (_caretScrollMethod == CaretScrollMethod.Unset)
{
_caretScrollMethod = caretScrollMethod;
// Post a "Loaded" priority operation to the dispatcher queue.
// Operations at Loaded priority are processed when layout and render is
// done but just before items at input priority are serviced.
// We want the update caret worker to run after layout is clean.
if (_textEditor.TextView != null && _textEditor.TextView.IsValid)
{
UpdateCaretStateWorker(null);
}
else
{
_pendingUpdateCaretStateCallback = true;
}
}
else if (caretScrollMethod != CaretScrollMethod.None)
{
_caretScrollMethod = caretScrollMethod;
}
}
// Get the caret brush that is the inverted color from the system window or background color.
internal static Brush GetCaretBrush(TextEditor textEditor)
{
Color backgroundColor;
ITextSelection focusedTextSelection;
object backgroundPropertyValue;
// If TextBoxBase.CaretBrush has been set, use that instead of the default inverting behavior.
Brush caretBrush = (Brush)textEditor.UiScope.GetValue(TextBoxBase.CaretBrushProperty);
if (caretBrush != null)
{
return caretBrush;
}
// Get the default background from the system color or UiScope's background
backgroundPropertyValue = textEditor.UiScope.GetValue(System.Windows.Controls.Panel.BackgroundProperty);
if (backgroundPropertyValue != null && backgroundPropertyValue != DependencyProperty.UnsetValue &&
backgroundPropertyValue is SolidColorBrush)
{
backgroundColor = ((SolidColorBrush)backgroundPropertyValue).Color;
}
else
{
backgroundColor = SystemColors.WindowColor;
}
// Get the background color from current selection
focusedTextSelection = textEditor.Selection;
if (focusedTextSelection is TextSelection)
{
backgroundPropertyValue = ((TextSelection)focusedTextSelection).GetCurrentValue(TextElement.BackgroundProperty);
if (backgroundPropertyValue != null && backgroundPropertyValue != DependencyProperty.UnsetValue)
{
if (backgroundPropertyValue is SolidColorBrush)
{
backgroundColor = ((SolidColorBrush)backgroundPropertyValue).Color;
}
}
}
// Invert the color to get the caret color from the system window or background color.
byte r = (byte)~(backgroundColor.R);
byte g = (byte)~(backgroundColor.G);
byte b = (byte)~(backgroundColor.B);
caretBrush = new SolidColorBrush(Color.FromRgb(r, g, b));
caretBrush.Freeze();
return caretBrush;
}
#endregion Caret Support
#region Bidi Support
//......................................................
//
// BIDI Support
//
//......................................................
/// <summary>
/// Check the installed bidi input language from the current
/// input keyboard list.
/// </summary>
internal static bool IsBidiInputLanguageInstalled()
{
bool bidiInputLanguageInstalled;
bidiInputLanguageInstalled = false;
int keyboardListCount = (int)SafeNativeMethods.GetKeyboardLayoutList(0, null);
if (keyboardListCount > 0)
{
int keyboardListIndex;
IntPtr[] keyboardList;
keyboardList = new IntPtr[keyboardListCount];
keyboardListCount = SafeNativeMethods.GetKeyboardLayoutList(keyboardListCount, keyboardList);
for (keyboardListIndex = 0;
(keyboardListIndex < keyboardList.Length) && (keyboardListIndex < keyboardListCount);
keyboardListIndex++)
{
CultureInfo cultureInfo = new CultureInfo((short)keyboardList[keyboardListIndex]);
if (IsBidiInputLanguage(cultureInfo))
{
bidiInputLanguageInstalled = true;
break;
}
}
}
return bidiInputLanguageInstalled;
}
#endregion Bidi Support
// Forces a synchronous layout validation, up to the selection moving position.
void ITextSelection.ValidateLayout()
{
((ITextSelection)this).MovingPosition.ValidateLayout();
}
#endregion Internal methods
//------------------------------------------------------
//
// Internal Properties
//
//------------------------------------------------------
#region Internal Properties
// Caret associated with this TextSelection.
internal CaretElement CaretElement
{
get
{
return _caretElement;
}
}
// Caret associated with this TextSelection.
CaretElement ITextSelection.CaretElement
{
get
{
return this.CaretElement;
}
}
// Returns true iff there are no additional insertion positions are either
// end of the selection.
bool ITextSelection.CoversEntireContent
{
get
{
ITextSelection This = this;
return (This.Start.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.Text &&
This.End.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text &&
This.Start.GetNextInsertionPosition(LogicalDirection.Backward) == null &&
This.End.GetNextInsertionPosition(LogicalDirection.Forward) == null);
}
}
#endregion Internal Properties
//------------------------------------------------------
//
// Private Methods
//
//------------------------------------------------------
#region Private Methods
// Stores this TextSelection into our TLS slot.
private void SetThreadSelection()
{
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
// Store this selection as focused one
threadLocalStore.FocusedTextSelection = this;
}
// Removes this TextSelection from our TLS slot.
private void ClearThreadSelection()
{
// Clear currently focused selection, if it's us
if (TextEditor._ThreadLocalStore.FocusedTextSelection == this)
{
TextEditor._ThreadLocalStore.FocusedTextSelection = null;
}
}
// GotKeyboardFocus event handler. Called by UpdateCaretAndHighlight.
private void Highlight()
{
ITextContainer textContainer = ((ITextSelection)this).Start.TextContainer;
// If we are using the adorner, then we should not instantiate highlight layers
// for TextContainer or PasswordTextContainer.
if (FrameworkAppContextSwitches.UseAdornerForTextboxSelectionRendering
&& (textContainer is TextContainer || textContainer is PasswordTextContainer))
{
return;
}
// Make sure that a highlight layer exists for drawing this selection
if (_highlightLayer == null)
{
_highlightLayer = new TextSelectionHighlightLayer(this);
}
// Make selection visible
if (textContainer.Highlights.GetLayer(typeof(TextSelection)) == null)
{
textContainer.Highlights.AddLayer(_highlightLayer);
}
}
// LostKeyboardFocus event handler. Called by UpdateCaretAndHighlight.
private void Unhighlight()
{
ITextContainer textContainer = ((ITextSelection)this).Start.TextContainer;
TextSelectionHighlightLayer highlightLayer = textContainer.Highlights.GetLayer(typeof(TextSelection)) as TextSelectionHighlightLayer;
if (highlightLayer != null)
{
textContainer.Highlights.RemoveLayer(highlightLayer);
Invariant.Assert(textContainer.Highlights.GetLayer(typeof(TextSelection)) == null);
}
}
//......................................................
//
// Active Positions of Selection
//
//......................................................
/// <summary>
/// Stores normalized anchor and moving positions for the selection.
/// Ensures that they are both inside of range Start/End.
/// </summary>
/// <param name="anchorPosition">
/// A position which must be stored as initial position for the selection.
/// </param>
/// <param name="movingPosition">
/// The "hot" or active selection edge which responds to user input.
/// </param>
private void SetActivePositions(ITextPointer anchorPosition, ITextPointer movingPosition)
{
// The following settings are used in auto-word exapnsion.
// By setting a _previousPosition we are clearing them all -
// they will be re-initialized in the beginning of a selection
// expansion guesture in ExtendSelectionByMouse method
// Previous position is needed for selection gestures to remember
// where mouse drag happed last time. Used for word autoexpansion.
_previousCursorPosition = null;
if (this.IsEmpty)
{
_anchorPosition = null;
_movingPositionEdge = MovingEdge.None;
return;
}
Invariant.Assert(anchorPosition != null);
ITextSelection thisSelection = (ITextSelection)this;
// Normalize and store new selection anchor position
_anchorPosition = anchorPosition.GetInsertionPosition(anchorPosition.LogicalDirection);
// Ensure that anchor position is within one of text segments
if (_anchorPosition.CompareTo(thisSelection.Start) < 0)
{
_anchorPosition = thisSelection.Start.GetFrozenPointer(_anchorPosition.LogicalDirection);
}
else if (_anchorPosition.CompareTo(thisSelection.End) > 0)
{
_anchorPosition = thisSelection.End.GetFrozenPointer(_anchorPosition.LogicalDirection);
}
_movingPositionEdge = ConvertToMovingEdge(anchorPosition, movingPosition);
_movingPositionDirection = movingPosition.LogicalDirection;
}
// Uses the current selection state to match an ITextPointer to one of the possible
// moving position edges.
private MovingEdge ConvertToMovingEdge(ITextPointer anchorPosition, ITextPointer movingPosition)
{
ITextSelection thisSelection = this;
MovingEdge movingEdge;
if (thisSelection.IsEmpty)
{
// Empty selections have no moving edge.
movingEdge = MovingEdge.None;
}
else if (thisSelection.TextSegments.Count < 2)
{
// Simple text selections move opposite their anchor positions.
movingEdge = (anchorPosition.CompareTo(movingPosition) <= 0) ? MovingEdge.End : MovingEdge.Start;
}
else
{
// Table selection. Look for an exact match.
if (movingPosition.CompareTo(thisSelection.Start) == 0)
{
movingEdge = MovingEdge.Start;
}
else if (movingPosition.CompareTo(thisSelection.End) == 0)
{
movingEdge = MovingEdge.End;
}
else if (movingPosition.CompareTo(thisSelection.TextSegments[0].End) == 0)
{
movingEdge = MovingEdge.StartInner;
}
else if (movingPosition.CompareTo(thisSelection.TextSegments[thisSelection.TextSegments.Count-1].Start) == 0)
{
movingEdge = MovingEdge.EndInner;
}
else
{
movingEdge = (anchorPosition.CompareTo(movingPosition) <= 0) ? MovingEdge.End : MovingEdge.Start;
}
}
return movingEdge;
}
//......................................................
//
// Selection Building With Mouse
//
//......................................................
// Moves the selection to the mouse cursor position.
// If the cursor is facing a UIElement, select the UIElement.
// Sets new selection anchor to a given cursorPosition.
private void MoveSelectionByMouse(ITextPointer cursorPosition, Point cursorMousePoint)
{
ITextSelection thisSelection = (ITextSelection)this;
if (this.TextView == null)
{
return;
}
Invariant.Assert(this.TextView.IsValid); // We just checked RenderScope. We'll use TextView below
ITextPointer movingPosition = null;
if (cursorPosition.GetPointerContext(cursorPosition.LogicalDirection) == TextPointerContext.EmbeddedElement)
{
Rect objectEdgeRect = this.TextView.GetRectangleFromTextPosition(cursorPosition);
// Check for embedded object.
// If the click happend inside of it we need to select it as a whole, when content is not read-only.
if (!_textEditor.IsReadOnly && ShouldSelectEmbeddedObject(cursorPosition, cursorMousePoint, objectEdgeRect))
{
movingPosition = cursorPosition.GetNextContextPosition(cursorPosition.LogicalDirection);
}
}
// Move selection to this position
if (movingPosition == null)
{
thisSelection.SetCaretToPosition(cursorPosition, cursorPosition.LogicalDirection, /*allowStopAtLineEnd:*/true, /*allowStopNearSpace:*/false);
}
else
{
thisSelection.Select(cursorPosition, movingPosition);
}
}
// Helper for MoveSelectionByMouse
private bool ShouldSelectEmbeddedObject(ITextPointer cursorPosition, Point cursorMousePoint, Rect objectEdgeRect)
{
// Although we now know that cursorPosition is facing an embedded object,
// we still need an additional test to determine if the original mouse point
// fell within the object or outside it's bouding box (which can happen when
// a mouse click is snapped to the nearest content).
// If the mouse point is outside the object, we don't want to select it.
if (!objectEdgeRect.IsEmpty &&
cursorMousePoint.Y >= objectEdgeRect.Y && cursorMousePoint.Y < objectEdgeRect.Y + objectEdgeRect.Height)
{
// Compare X coordinates of mouse down point and object edge rect,
// depending on the FlowDirection of the render scope and paragraph content.
FlowDirection renderScopeFlowDirection = (FlowDirection)this.TextView.RenderScope.GetValue(Block.FlowDirectionProperty);
FlowDirection paragraphFlowDirection = (FlowDirection)cursorPosition.GetValue(Block.FlowDirectionProperty);
if (renderScopeFlowDirection == FlowDirection.LeftToRight)
{
if (paragraphFlowDirection == FlowDirection.LeftToRight &&
(cursorPosition.LogicalDirection == LogicalDirection.Forward && objectEdgeRect.X < cursorMousePoint.X ||
cursorPosition.LogicalDirection == LogicalDirection.Backward && cursorMousePoint.X < objectEdgeRect.X))
{
return true;
}
else if (paragraphFlowDirection == FlowDirection.RightToLeft &&
(cursorPosition.LogicalDirection == LogicalDirection.Forward && objectEdgeRect.X > cursorMousePoint.X ||
cursorPosition.LogicalDirection == LogicalDirection.Backward && cursorMousePoint.X > objectEdgeRect.X))
{
return true;
}
}
else
{
if (paragraphFlowDirection == FlowDirection.LeftToRight &&
(cursorPosition.LogicalDirection == LogicalDirection.Forward && objectEdgeRect.X > cursorMousePoint.X ||
cursorPosition.LogicalDirection == LogicalDirection.Backward && cursorMousePoint.X > objectEdgeRect.X))
{
return true;
}
else if (paragraphFlowDirection == FlowDirection.RightToLeft &&
(cursorPosition.LogicalDirection == LogicalDirection.Forward && objectEdgeRect.X < cursorMousePoint.X ||
cursorPosition.LogicalDirection == LogicalDirection.Backward && cursorMousePoint.X < objectEdgeRect.X))
{
return true;
}
}
}
return false;
}
//......................................................
//
// Caret Support
//
//......................................................
// Redraws a caret using current setting for italic - taking springload formatting into account.
private static void RefreshCaret(TextEditor textEditor, ITextSelection textSelection)
{
object fontStylePropertyValue;
bool italic;
if (textSelection == null || textSelection.CaretElement == null)
{
return;
}
// NOTE: We are using GetCurrentValue to take springload formatting into account.
fontStylePropertyValue = ((TextSelection)textSelection).GetCurrentValue(TextElement.FontStyleProperty);
italic = (textEditor.AcceptsRichContent && fontStylePropertyValue != DependencyProperty.UnsetValue && (FontStyle)fontStylePropertyValue == FontStyles.Italic);
textSelection.CaretElement.RefreshCaret(italic);
}
// Called after a caret navigation, to signal that the next caret
// scroll-into-view should include hueristics to include following
// text.
internal void OnCaretNavigation()
{
_pendingCaretNavigation = true;
}
// Called after a caret navigation, to signal that the next caret
// scroll-into-view should include hueristics to include following
// text.
void ITextSelection.OnCaretNavigation()
{
OnCaretNavigation();
}
// Callback for UpdateCaretState worker.
private object UpdateCaretStateWorker(object o)
{
_pendingUpdateCaretStateCallback = false;
// This can happen if selection has been detached by TextEditor.OnDetach.
if (_textEditor == null)
{
return null;
}
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
CaretScrollMethod caretScrollMethod = _caretScrollMethod;
_caretScrollMethod = CaretScrollMethod.Unset;
// Use the locally defined caretElement because _caretElement can be null by
// detaching CaretElement object
CaretElement caretElement = _caretElement;
if (caretElement == null)
{
return null;
}
if (threadLocalStore.FocusedTextSelection == null)
{
// If we have multiple windows open, a non-blinking caret might be showing
// in the given TextEditor's UiScope. If the selection for that Editor is
// not empty, we need to hide the caret.
if (!this.IsEmpty)
{
caretElement.Hide();
}
return null;
}
// When the TextView is not valid, there is nothing to do
if (_textEditor.TextView == null || !_textEditor.TextView.IsValid)
{
// Do we need to clear obsolte highlight?
return null;
}
if (!this.VerifyAdornerLayerExists())
{
caretElement.Hide();
}
// Identify caret position
// Make sure that moving position is inside of selection
ITextPointer caretPosition = IdentifyCaretPosition(this);
// If caret position is not valid in focusedTextSelection.TextView, we cannot update it.
if (caretPosition.HasValidLayout)
{
Rect caretRectangle;
bool italic = false;
bool caretVisible = this.IsEmpty && (!_textEditor.IsReadOnly || _textEditor.IsReadOnlyCaretVisible);
if (!this.IsInterimSelection)
{
caretRectangle = CalculateCaretRectangle(this, caretPosition);
if (this.IsEmpty)
{
// Identify italic condition (including springload) - to use for caret shaping
object fontStylePropertyValue = GetPropertyValue(TextElement.FontStyleProperty);
italic = (_textEditor.AcceptsRichContent && fontStylePropertyValue != DependencyProperty.UnsetValue && (FontStyle)fontStylePropertyValue == FontStyles.Italic);
}
}
else
{
caretRectangle = CalculateInterimCaretRectangle(this);
// Caret always visible on the interim input mode.
caretVisible = true;
}
Brush caretBrush = GetCaretBrush(_textEditor);
// Calculate the scroll origin position to scroll it with the caret position.
double scrollToOriginPosition = CalculateScrollToOriginPosition(_textEditor, caretPosition, caretRectangle.X);
// Re-render the caret.
// Get a bounding rect from the active end of selection.
caretElement.Update(caretVisible, caretRectangle, caretBrush, 1.0, italic, caretScrollMethod, scrollToOriginPosition);
}
// CaretElement.Update(...) makes a conditional call to BringIntoView()
// on the text view's associated element, which makes the view invalid...
if (this.TextView.IsValid && !this.TextView.RendersOwnSelection)
{
// Re-render selection. Need to do this to invalidate and update adorner for this caret element.
caretElement.UpdateSelection();
}
return null;
}
// Helper for UpdateCaretState -- identifies caret position from text selection.
private static ITextPointer IdentifyCaretPosition(ITextSelection currentTextSelection)
{
ITextPointer caretPosition = currentTextSelection.MovingPosition;
if (!currentTextSelection.IsEmpty)
{
// Even when we do not draw the blinking caret, we need this position
// for scrolling caret position into view.
// Special case for nonempty selection extended beyond end of line
if ((caretPosition.LogicalDirection == LogicalDirection.Backward && //
caretPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart) || //
TextPointerBase.IsAfterLastParagraph(caretPosition))
{
// This means that selection has been expanded by ExtendToLineEnd/ExtendToDocumentEnd command.
// TextView in this case cannot give the rect from this position;
// we need to move backward to the end of content
caretPosition = caretPosition.CreatePointer();
caretPosition.MoveToNextInsertionPosition(LogicalDirection.Backward);
caretPosition.SetLogicalDirection(LogicalDirection.Forward);
}
}
// TextView.GetRectangleFromTextPosition returns the end of character rect in case of Backward
// logical direction at the start caret position of the docuemtn or paragraph, so we reset the
// logical direction with Forward to get the right rect of caret at the start position of
// document or paragraph.
if (caretPosition.LogicalDirection == LogicalDirection.Backward && //
caretPosition.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart && //
(caretPosition.GetNextInsertionPosition(LogicalDirection.Backward) == null || //
TextPointerBase.IsNextToAnyBreak(caretPosition, LogicalDirection.Backward)))
{
caretPosition = caretPosition.CreatePointer();
caretPosition.SetLogicalDirection(LogicalDirection.Forward);
}
return caretPosition;
}
// Helper for UpdateCaretState -- calculates caret rectangle from text selection.
// regular (non-interim) case of caret positioning
private static Rect CalculateCaretRectangle(ITextSelection currentTextSelection, ITextPointer caretPosition)
{
Transform caretTransform;
Rect caretRectangle = currentTextSelection.TextView.GetRawRectangleFromTextPosition(caretPosition, out caretTransform);
if (caretRectangle.IsEmpty)
{
// Caret is not at an insertion position, it has no geometry
// and will not be displayed.
return Rect.Empty;
}
// Convert to local coordiantes.
caretRectangle = caretTransform.TransformBounds(caretRectangle);
// We will use the system defined caret width later.
caretRectangle.Width = 0;
if (currentTextSelection.IsEmpty)
{
// Calculate caret height - from current font size (ignoring a rect returned by TextView,
// as it can be a rect of embedded object, which should not affect caret height)
double fontSize = (double)currentTextSelection.GetPropertyValue(TextElement.FontSizeProperty);
FontFamily fontFamily = (FontFamily)currentTextSelection.GetPropertyValue(TextElement.FontFamilyProperty);
double caretHeight = fontFamily.LineSpacing * fontSize;
if (caretHeight < caretRectangle.Height)
{
// Decrease the height of caret to the font height and lower the caret to keep its bottom
// staying on the baseline.
caretRectangle.Y += caretRectangle.Height - caretHeight;
caretRectangle.Height = caretHeight;
}
if (!caretTransform.IsIdentity)
{
Point top = new Point(caretRectangle.X, caretRectangle.Y);
Point bottom = new Point(caretRectangle.X, caretRectangle.Y + caretRectangle.Height);
caretTransform.TryTransform(top, out top);
caretTransform.TryTransform(bottom, out bottom);
caretRectangle.Y += caretRectangle.Height - Math.Abs(bottom.Y - top.Y);
caretRectangle.Height = Math.Abs(bottom.Y - top.Y);
}
}
return caretRectangle;
}
// Helper for UpdateCaretState -- handles the korean interim caret.
private static Rect CalculateInterimCaretRectangle(ITextSelection focusedTextSelection)
{
// There is no checking for empty selection here. Is this correct? Do we want this behavior for non-empty selection?
// Get the current flow direction on the selection of interim.
// This is for getting the right size of distance on the interim character
// whatever the current flow direction is.
FlowDirection flowDirection = (FlowDirection)focusedTextSelection.Start.GetValue(FrameworkElement.FlowDirectionProperty);
ITextPointer nextCharacterPosition;
Rect nextCharacterRectangle;
Rect caretRectangle;
if (flowDirection != FlowDirection.RightToLeft)
{
// Flow direction is Left-to-Right
// Get the rectangle for both interim selection start position and the next character
// position.
nextCharacterPosition = focusedTextSelection.Start.CreatePointer(LogicalDirection.Forward);
caretRectangle = focusedTextSelection.TextView.GetRectangleFromTextPosition(nextCharacterPosition);
// Get the next character position from the start position of the interim selection
// on the left to right flow direction.
nextCharacterPosition.MoveToNextInsertionPosition(LogicalDirection.Forward);
nextCharacterPosition.SetLogicalDirection(LogicalDirection.Backward);
nextCharacterRectangle = focusedTextSelection.TextView.GetRectangleFromTextPosition(nextCharacterPosition);
}
else
{
// Flow direction is Right-to-Left
// Get the rectangle for both interim selection end position and the next character
// position.
nextCharacterPosition = focusedTextSelection.End.CreatePointer(LogicalDirection.Backward);
caretRectangle = focusedTextSelection.TextView.GetRectangleFromTextPosition(nextCharacterPosition);
// Get the next character position from the end position of the interim selection
// on the right to left flow direction.
nextCharacterPosition.MoveToNextInsertionPosition(LogicalDirection.Backward);
nextCharacterPosition.SetLogicalDirection(LogicalDirection.Forward);
nextCharacterRectangle = focusedTextSelection.TextView.GetRectangleFromTextPosition(nextCharacterPosition);
}
// The interim next character position should be great than the current interim position.
// Otherwise, we show the caret as the normal state that use the system caret width.
// In case of BiDi character, the next character position can be greater than the current position.
if (!caretRectangle.IsEmpty && !nextCharacterRectangle.IsEmpty && nextCharacterRectangle.Left > caretRectangle.Left)
{
// Get the interim caret width to show the interim block caret.
caretRectangle.Width = nextCharacterRectangle.Left - caretRectangle.Left;
}
return caretRectangle;
}
// Helper for UpdateCaretStateWorker -- Calculate the scroll origin position to scroll caret
// with the scroll origin position so that we can ensure of displaying caret with the wrapped word.
//
// There are four cases of different corrdinate by the flow direction on UiScope and Paragraph.
// UiScope has two flow direction which is LeftToRightflow directioin and another is RightToLeft.
// Paragraph has also two flow direction which is LeftToRightflow directioin and another is RightToLeft.
//
// The below is the example of how horizontal corrdinate and scroll origin value base on the different
// four cases. So we have to calculate the scroll to origin position base on the case. Simply we can
// get the scroll to origin value as zero if UiScope and Paragraph's flow direction is the same.
// Otherwise, the scroll to origin value is the extent width value that is the max width.
//
// <<For instance>>
// Case 1.
// UiScope FlowDirection: LTR(LeftToRight)
// Paragraph FlowDirection: LTR(LefTToRight)
// Horizontal origin: "Left"
// Scroll horizontal origin: "0"
// Wrapping to: "Left"
// ABC ......
// XYZ|
//
// Case 2.
// UiScope FlowDirection: LTR(LeftToRight)
// Paragraph FlowDirection: RTL(RightToLeft)
// Horizontal origin: "Left"
// Scroll horizontal origin: "Max:Extent Width"
// Wrapping to: "Right"
// ......ABC
// XYZ|
//
// Case 3.
// UiScope FlowDirection: RTL(RightToLeft)
// Paragraph FlowDirection: RTL(RightToLeft)
// horizontal origin: "Right"
// Scroll horizontal origin: "0"
// Wrapping to: "Right"
// ......ABC
// XYZ|
//
// Case 4.
// UiScope FlowDirection: RTL(RightToLeft)
// Paragraph FlowDirection: LTR(LefTToRight)
// horizontal origin: "Right"
// Scroll horizontal origin: "Max:Extent Width"
// Wrapping to: "Left"
// ABC ......
// XYZ|
private static double CalculateScrollToOriginPosition(TextEditor textEditor, ITextPointer caretPosition, double horizontalCaretPosition)
{
double scrollToOriginPosition = double.NaN;
if (textEditor.UiScope is TextBoxBase)
{
double viewportWidth = ((TextBoxBase)textEditor.UiScope).ViewportWidth;
double extentWidth = ((TextBoxBase)textEditor.UiScope).ExtentWidth;
// Calculate the scroll to the origin position position when the horizontal scroll is available
if (viewportWidth != 0 && extentWidth != 0 && viewportWidth < extentWidth)
{
bool needScrollToOriginPosition = false;
// Check whether we need to calculate the scroll origin position to scroll it with the caret
// position. If the caret position is out of the current visual viewport area, the scroll
// to origin positioin will be calculated to scroll into the origin position first that
// ensure of displaying the wrapped word.
//
// Note that horizontalCaretPosition is always relative to the viewport, not the document.
if (horizontalCaretPosition < 0 || horizontalCaretPosition >= viewportWidth)
{
needScrollToOriginPosition = true;
}
if (needScrollToOriginPosition)
{
// Set the scroll original position as zero
scrollToOriginPosition = 0;
// Get the flow direction of uiScope
FlowDirection uiScopeflowDirection = (FlowDirection)textEditor.UiScope.GetValue(FrameworkElement.FlowDirectionProperty);
// Get the flow direction of the current paragraph and compare it with uiScope's flow direction.
Block paragraphOrBlockUIContainer = (caretPosition is TextPointer) ? ((TextPointer)caretPosition).ParagraphOrBlockUIContainer : null;
if (paragraphOrBlockUIContainer != null)
{
FlowDirection pagraphFlowDirection = paragraphOrBlockUIContainer.FlowDirection;
// If the flow direction is different between uiScopoe and paragaph,
// the original scroll position is the extent width value.
if (uiScopeflowDirection != pagraphFlowDirection)
{
scrollToOriginPosition = extentWidth;
}
}
// Adjust scroll position by current viewport offset
scrollToOriginPosition -= ((TextBoxBase)textEditor.UiScope).HorizontalOffset;
}
}
}
return scrollToOriginPosition;
}
private CaretElement EnsureCaret(bool isBlinkEnabled, bool isSelectionActive, CaretScrollMethod scrollMethod)
{
TextEditorThreadLocalStore threadLocalStore = TextEditor._ThreadLocalStore;
if (_caretElement == null)
{
// Create new caret
_caretElement = new CaretElement(_textEditor, isBlinkEnabled);
_caretElement.IsSelectionActive = isSelectionActive;
// Check the current input language to draw the BiDi caret in case of BiDi language
// like as Arabic or Hebrew input language.
// Move this somewhere where it'll only be called once
if (IsBidiInputLanguage(InputLanguageManager.Current.CurrentInputLanguage))
{
TextEditor._ThreadLocalStore.Bidi = true;
}
else
{
TextEditor._ThreadLocalStore.Bidi = false;
}
}
else
{
// Please note that it is important to set the IsSelectionActive property before
// calling SetBlinking. This is because SetBlinking calls Win32CreateCaret &
// Win32DestroyCaret both of which meaningfully use this flag.
_caretElement.IsSelectionActive = isSelectionActive;
_caretElement.SetBlinking(isBlinkEnabled);
}
UpdateCaretState(scrollMethod);
return _caretElement;
}
/// <summary>
/// Walk up the tree from the RenderScope to the UiScope until we find an
/// AdornerDecorator or ScrollContentPresenter.
/// </summary>
/// <returns>true if one is found, false otherwise</returns>
private bool VerifyAdornerLayerExists()
{
DependencyObject element = TextView.RenderScope;
while (element != _textEditor.UiScope && element != null)
{
if (element is AdornerDecorator || element is System.Windows.Controls.ScrollContentPresenter)
{
return true;
}
element = VisualTreeHelper.GetParent(element);
}
return false;
}
//......................................................
//
// BIDI Support
//
//......................................................
/// <summary>
/// Check the input language of cultureInfo whether it is the bi-directional language or not.
/// </summary>
/// <param name="cultureInfo"></param>
/// <returns>
/// Return true if the passed cultureInfo is the bi-directional language like as Arabic or Hebrew.
/// Otherwise, return false.
/// </returns>
private static bool IsBidiInputLanguage(CultureInfo cultureInfo)
{
bool bidiInput;
string fontSignature;
bidiInput = false;
fontSignature = new String(new Char[FONTSIGNATURE_SIZE]);
// Get the font signature to know the current LCID is BiDi(Arabic, Hebrew etc.) or not.
if (UnsafeNativeMethods.GetLocaleInfoW(cultureInfo.LCID, NativeMethods.LOCALE_FONTSIGNATURE, fontSignature, FONTSIGNATURE_SIZE) != 0)
{
// Compare fontSignature[7] with 0x0800 to detect BiDi language.
if ((fontSignature[FONTSIGNATURE_BIDI_INDEX] & FONTSIGNATURE_BIDI) != 0)
{
bidiInput = true;
}
}
return bidiInput;
}
//......................................................
//
// Table Selection
//
//......................................................
private static TableCell FindCellAtColumnIndex(TableCellCollection cells, int columnIndex)
{
for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
{
TableCell cell;
int startColumnIndex;
int endColumnIndex;
cell = cells[cellIndex];
startColumnIndex = cell.ColumnIndex;
endColumnIndex = startColumnIndex + cell.ColumnSpan - 1;
if (startColumnIndex <= columnIndex && columnIndex <= endColumnIndex)
{
return cell;
}
}
return null;
}
/// <summary>
/// Determines if the given element has any ancestor.
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private static bool IsRootElement(DependencyObject element)
{
return GetParentElement(element) == null;
}
/// <summary>
/// Workaround to approximate whether or not our window is active.
/// </summary>
/// <returns></returns>
private bool IsFocusWithinRoot()
{
DependencyObject element = this.UiScope;
DependencyObject parent = this.UiScope;
while (parent != null)
{
element = parent;
parent = GetParentElement(element);
}
if (element is UIElement && ((UIElement)element).IsKeyboardFocusWithin)
{
return true;
}
return false;
}
/// <summary>
/// Helper method to determine an element's parent, using the umpteen methods of
/// parenting.
/// </summary>
/// <param name="element"></param>
private static DependencyObject GetParentElement(DependencyObject element)
{
DependencyObject parent;
if (element is FrameworkElement || element is FrameworkContentElement)
{
parent = LogicalTreeHelper.GetParent(element);
if (parent == null && element is FrameworkElement)
{
parent = ((FrameworkElement)element).TemplatedParent;
if (parent == null && element is Visual)
{
parent = VisualTreeHelper.GetParent(element);
}
}
}
else if (element is Visual)
{
parent = VisualTreeHelper.GetParent(element);
}
else
{
parent = null;
}
return parent;
}
// Removes the caret from the visual tree.
private void DetachCaretFromVisualTree()
{
if (_caretElement != null)
{
_caretElement.DetachFromView();
_caretElement = null;
}
}
#endregion Private methods
//------------------------------------------------------
//
// Private Properties
//
//------------------------------------------------------
#region Private Properties
TextEditor ITextSelection.TextEditor
{
get
{
return _textEditor;
}
}
ITextView ITextSelection.TextView
{
get
{
return _textEditor.TextView;
}
}
private ITextView TextView
{
get
{
return ((ITextSelection)this).TextView;
}
}
private TextStore TextStore
{
get
{
return _textEditor.TextStore;
}
}
private ImmComposition ImmComposition
{
get
{
return _textEditor.ImmComposition;
}
}
// UiScope associated with this TextSelection's TextEditor.
private FrameworkElement UiScope
{
get
{
return _textEditor.UiScope;
}
}
// Position from which to spring load property values.
private ITextPointer PropertyPosition
{
get
{
ITextSelection This = this;
ITextPointer position = null;
if (!This.IsEmpty)
{
position = TextPointerBase.GetFollowingNonMergeableInlineContentStart(This.Start);
}
if (position == null)
{
position = This.Start;
}
position.Freeze();
return position;
}
}
#endregion Private Properties
//------------------------------------------------------
//
// Private Types
//
//------------------------------------------------------
#region Private Types
// The four possible positions for the selection moving position.
private enum MovingEdge { Start, StartInner, EndInner, End, None };
#endregion Private Types
//------------------------------------------------------
//
// Private Fields
//
//------------------------------------------------------
#region Private Fields
// Selected Content
// ----------------
// TextEditor that owns this selection.
private TextEditor _textEditor;
// Container for highlights on selected text.
private TextSelectionHighlightLayer _highlightLayer;
// Springload Formatting
// ---------------------
// Dependency object representing a set of springloaded formatting properties
private DependencyObject _springloadFormatting;
// Selection autoexpansion for unit boundaries
// -------------------------------------------
// A position where the last selection gesture has been initiated.
// Selection gestures are: mouseDown-Move-...-Move-Up, Shift+ArrowDown-...-Down-Up.
// Note that Shift+MouseDown-Move-...-Move-Up is a gesture continuation,
// it does not change _anchorPosition.
// Actual selection always contains this position but may be extended
// to a wider range - to encompass whole units such as words,
// hyperlinks, sentences, paragraphs, table cells etc.
// Must be renamed to _initialPosition.
// It may coinside with Start or End, but it may be also in some other
// corner of rectangular table range, or within a word-expanded selection.
// Use TextNavigator for this member
private ITextPointer _anchorPosition;
// A position to which user input is applied.
// It may coinside with Start or End, but it may be also in some other
// corner of rectangular table range.
private MovingEdge _movingPositionEdge;
// LogicalDirection for the moving position.
// If the selection is empty, this value is ignored and
// the direction matches this.Start. Otherwise we respect
// the value, which typically points inwards towards the
// content but may point outward to include an empty line.
private LogicalDirection _movingPositionDirection;
// Text position used on previous step of mouse dragging selection
// It is used in selection dragging heuristic to identify a situation when
// dragging end returned back to selected area which means that
// autoword expansion must be temporary stopped - until a new word
// boundary is crossed. See also _reenterPosition.
private ITextPointer _previousCursorPosition;
// Text position where dragged mouse re-entered a selection. This word should not be autoexapnded
private ITextPointer _reenterPosition;
// Flag indicating that initial word boundary has been crossed at least once doring selection expansion
private bool _anchorWordRangeHasBeenCrossedOnce;
// Flag allows autoword expansion for anchor end of selection
private bool _allowWordExpansionOnAnchorEnd;
// Font signature size as 16
private const int FONTSIGNATURE_SIZE = 16;
// BIDI font signature index from GetLocaleInfo.
private const int FONTSIGNATURE_BIDI_INDEX = 7;
// BIDI font signature value
private const int FONTSIGNATURE_BIDI = 0x0800;
// Signals how the caret should be scrolled into view on the next
// caret update.
private CaretScrollMethod _caretScrollMethod;
// If true, signals that the next caret
// scroll-into-view should include hueristics to include following
// text.
private bool _pendingCaretNavigation;
// Caret associated with this selection.
private CaretElement _caretElement;
// Flag set true after scheduling a callback to UpdateCaretStateWorker.
// Used to prevent unbounded callback allocations on the Dispatcher queue --
// we fold redundant update requests into a single queue item.
bool _pendingUpdateCaretStateCallback;
#endregion Private Fields
}
}
|