|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms.Layout;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
using Windows.Win32.UI.Controls.RichEdit;
namespace System.Windows.Forms;
/// <summary>
/// Implements the basic functionality required by text
/// controls.
/// </summary>
[DefaultEvent(nameof(TextChanged))]
[DefaultBindingProperty(nameof(Text))]
[Designer($"System.Windows.Forms.Design.TextBoxBaseDesigner, {AssemblyRef.SystemDesign}")]
public abstract partial class TextBoxBase : Control
{
// The boolean properties for this control are contained in the textBoxFlags bit
// vector. We can store up to 32 boolean values in this one vector. Here we
// create the bitmasks for each bit in the vector.
private static readonly int s_autoSize = BitVector32.CreateMask();
private static readonly int s_hideSelection = BitVector32.CreateMask(s_autoSize);
private static readonly int s_multiline = BitVector32.CreateMask(s_hideSelection);
private static readonly int s_modified = BitVector32.CreateMask(s_multiline);
private static readonly int s_readOnly = BitVector32.CreateMask(s_modified);
private static readonly int s_acceptsTab = BitVector32.CreateMask(s_readOnly);
private static readonly int s_wordWrap = BitVector32.CreateMask(s_acceptsTab);
private static readonly int s_creatingHandle = BitVector32.CreateMask(s_wordWrap);
private static readonly int s_codeUpdateText = BitVector32.CreateMask(s_creatingHandle);
private static readonly int s_shortcutsEnabled = BitVector32.CreateMask(s_codeUpdateText);
private static readonly int s_scrollToCaretOnHandleCreated = BitVector32.CreateMask(s_shortcutsEnabled);
private static readonly int s_setSelectionOnHandleCreated = BitVector32.CreateMask(s_scrollToCaretOnHandleCreated);
private static readonly object s_acceptsTabChangedEvent = new();
private static readonly object s_borderStyleChangedEvent = new();
private static readonly object s_hideSelectionChangedEvent = new();
private static readonly object s_modifiedChangedEvent = new();
private static readonly object s_multilineChangedEvent = new();
private static readonly object s_readOnlyChangedEvent = new();
/// <summary>
/// The current border for this edit control.
/// </summary>
private BorderStyle _borderStyle = BorderStyle.Fixed3D;
/// <summary>
/// Controls the maximum length of text in the edit control.
/// Matches the Windows limit.
/// </summary>
private int _maxLength = 32767;
/// <summary>
/// Used by the autoSizing code to help figure out the desired height of
/// the edit box.
/// </summary>
private int _requestedHeight;
private bool _integralHeightAdjust;
// these indices are used to cache the values of the selection, by doing this
// if the handle isn't created yet, we don't force a creation.
private int _selectionStart;
private int _selectionLength;
/// <summary>
/// Controls firing of click event (Left click).
/// This is used by TextBox, RichTextBox and MaskedTextBox, code was moved down from TextBox/RichTextBox
/// but cannot make it as default behavior to avoid introducing breaking changes.
/// </summary>
private bool _doubleClickFired;
private static int[]? s_shortcutsToDisable;
// We store all boolean properties in here.
private BitVector32 _textBoxFlags;
/// <summary>
/// Creates a new TextBox control. Uses the parent's current font and color
/// set.
/// </summary>
internal TextBoxBase() : base()
{
// this class overrides GetPreferredSizeCore, let Control automatically cache the result
SetExtendedState(ExtendedStates.UserPreferredSizeCache, true);
_textBoxFlags[s_autoSize | s_hideSelection | s_wordWrap | s_shortcutsEnabled] = true;
SetStyle(ControlStyles.FixedHeight, _textBoxFlags[s_autoSize]);
SetStyle(ControlStyles.StandardClick
| ControlStyles.StandardDoubleClick
| ControlStyles.UseTextForAccessibility
| ControlStyles.UserPaint, false);
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
#pragma warning restore WFO5001
// cache requestedHeight. Note: Control calls DefaultSize (overridable) in the constructor
// to set the control's cached height that is returned when calling Height, so we just
// need to get the cached height here.
_requestedHeight = Height;
}
/// <summary>
/// Gets or sets
/// a value indicating whether pressing the TAB key
/// in a multiline text box control types
/// a TAB character in the control instead of moving the focus to the next control
/// in the tab order.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[SRDescription(nameof(SR.TextBoxAcceptsTabDescr))]
public bool AcceptsTab
{
get
{
return _textBoxFlags[s_acceptsTab];
}
set
{
if (_textBoxFlags[s_acceptsTab] != value)
{
_textBoxFlags[s_acceptsTab] = value;
OnAcceptsTabChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnAcceptsTabChangedDescr))]
public event EventHandler? AcceptsTabChanged
{
add => Events.AddHandler(s_acceptsTabChangedEvent, value);
remove => Events.RemoveHandler(s_acceptsTabChangedEvent, value);
}
/// <summary>
/// Gets or sets a value indicating whether the following shortcuts should be enabled or not:
/// Ctrl-Z, Ctrl-C, Ctrl-X, Ctrl-V, Ctrl-A, Ctrl-L, Ctrl-R, Ctrl-E, Ctrl-I, Ctrl-Y,
/// Ctrl-BackSpace, Ctrl-Del, Shift-Del, Shift-Ins.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxShortcutsEnabledDescr))]
public virtual bool ShortcutsEnabled
{
get
{
return _textBoxFlags[s_shortcutsEnabled];
}
set
{
s_shortcutsToDisable ??=
[
(int)Shortcut.CtrlZ, (int)Shortcut.CtrlC, (int)Shortcut.CtrlX,
(int)Shortcut.CtrlV, (int)Shortcut.CtrlA, (int)Shortcut.CtrlL, (int)Shortcut.CtrlR,
(int)Shortcut.CtrlE, (int)Shortcut.CtrlY, (int)Keys.Control + (int)Keys.Back,
(int)Shortcut.CtrlDel, (int)Shortcut.ShiftDel, (int)Shortcut.ShiftIns, (int)Shortcut.CtrlJ
];
_textBoxFlags[s_shortcutsEnabled] = value;
}
}
/// <summary>
/// Implements the <see cref="ShortcutsEnabled"/> property.
/// </summary>
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
// First call parent's ProcessCmdKey, since we don't to eat up
// the shortcut key we are not supported in TextBox.
bool returnedValue = base.ProcessCmdKey(ref msg, keyData);
if (!ShortcutsEnabled && s_shortcutsToDisable is not null)
{
foreach (int shortcutValue in s_shortcutsToDisable)
{
if ((int)keyData == shortcutValue ||
(int)keyData == (shortcutValue | (int)Keys.Shift))
{
return true;
}
}
}
// There are a few keys that change the alignment of the text, but that
// are not ignored by the native control when the ReadOnly property is set.
// We need to workaround that.
if (_textBoxFlags[s_readOnly])
{
int k = (int)keyData;
if (k is ((int)Shortcut.CtrlL) // align left
or ((int)Shortcut.CtrlR) // align right
or ((int)Shortcut.CtrlE) // align center
or ((int)Shortcut.CtrlJ)) // align justified
{
return true;
}
}
if (!ReadOnly && (keyData == (Keys.Control | Keys.Back) || keyData == (Keys.Control | Keys.Shift | Keys.Back)))
{
if (SelectionLength != 0)
{
SetSelectedTextInternal(string.Empty, clearUndo: false);
}
else if (SelectionStart != 0)
{
int boundaryStart = ClientUtils.GetWordBoundaryStart(Text, SelectionStart);
int length = SelectionStart - boundaryStart;
BeginUpdateInternal();
SelectionStart = boundaryStart;
SelectionLength = length;
EndUpdateInternal();
SetSelectedTextInternal(string.Empty, clearUndo: false);
}
return true;
}
return returnedValue;
}
/// <summary>
/// Gets or sets a value indicating whether the size
/// of the control automatically adjusts when the font assigned to the control
/// is changed.
///
/// Note: this works differently than other Controls' AutoSize, so we're hiding
/// it to avoid confusion.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxAutoSizeDescr))]
[RefreshProperties(RefreshProperties.Repaint)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool AutoSize
{
get
{
return _textBoxFlags[s_autoSize];
}
set
{
// Note that we intentionally do not call base. TextBoxes size themselves by
// overriding SetBoundsCore (old RTM code). We let CommonProperties.GetAutoSize
// continue to return false to keep our LayoutEngines from messing with TextBoxes.
// This is done for backwards compatibility since the new AutoSize behavior differs.
if (_textBoxFlags[s_autoSize] != value)
{
_textBoxFlags[s_autoSize] = value;
// AutoSize's effects are ignored for a multi-line textbox
//
if (!Multiline)
{
SetStyle(ControlStyles.FixedHeight, value);
AdjustHeight(false);
}
OnAutoSizeChanged(EventArgs.Empty);
}
}
}
/// <summary>
/// Gets or sets the background color of the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DispId(PInvokeCore.DISPID_BACKCOLOR)]
[SRDescription(nameof(SR.ControlBackColorDescr))]
public override Color BackColor
{
get
{
if (ShouldSerializeBackColor())
{
return base.BackColor;
}
else if (ReadOnly)
{
return SystemColors.Control;
}
else
{
return SystemColors.Window;
}
}
set => base.BackColor = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override Image? BackgroundImage
{
get => base.BackgroundImage;
set => base.BackgroundImage = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? AutoSizeChanged
{
add => base.AutoSizeChanged += value;
remove => base.AutoSizeChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageChanged
{
add => base.BackgroundImageChanged += value;
remove => base.BackgroundImageChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override ImageLayout BackgroundImageLayout
{
get => base.BackgroundImageLayout;
set => base.BackgroundImageLayout = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageLayoutChanged
{
add => base.BackgroundImageLayoutChanged += value;
remove => base.BackgroundImageLayoutChanged -= value;
}
/// <summary>
/// Gets or sets the border type
/// of the text box control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(BorderStyle.Fixed3D)]
[DispId(PInvokeCore.DISPID_BORDERSTYLE)]
[SRDescription(nameof(SR.TextBoxBorderDescr))]
public BorderStyle BorderStyle
{
get => _borderStyle;
set
{
if (_borderStyle != value)
{
SourceGenerated.EnumValidator.Validate(value);
_borderStyle = value;
UpdateStyles();
RecreateHandle();
// PreferredSize depends on BorderStyle : thru CreateParams.ExStyle in User32!AdjustRectEx.
// So when the BorderStyle changes let the parent of this control know about it.
using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.BorderStyle))
{
OnBorderStyleChanged(EventArgs.Empty);
}
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnBorderStyleChangedDescr))]
public event EventHandler? BorderStyleChanged
{
add => Events.AddHandler(s_borderStyleChangedEvent, value);
remove => Events.RemoveHandler(s_borderStyleChangedEvent, value);
}
internal virtual bool CanRaiseTextChangedEvent => true;
protected override bool CanEnableIme => !(ReadOnly || PasswordProtect) && base.CanEnableIme;
/// <summary>
/// Gets a value indicating whether the user can undo the previous operation in a text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxCanUndoDescr))]
public bool CanUndo => IsHandleCreated && (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CANUNDO) != 0;
/// <summary>
/// Returns the parameters needed to create the handle. Inheriting classes
/// can override this to provide extra functionality. They should not,
/// however, forget to call base.getCreateParams() first to get the struct
/// filled up with the basic info.
/// </summary>
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ClassName = PInvoke.WC_EDIT;
cp.Style |= PInvoke.ES_AUTOHSCROLL | PInvoke.ES_AUTOVSCROLL;
if (!_textBoxFlags[s_hideSelection])
{
cp.Style |= PInvoke.ES_NOHIDESEL;
}
if (_textBoxFlags[s_readOnly])
{
cp.Style |= PInvoke.ES_READONLY;
}
cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;
cp.ExStyle &= ~(int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
switch (_borderStyle)
{
case BorderStyle.Fixed3D:
cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
break;
case BorderStyle.FixedSingle:
cp.Style |= (int)WINDOW_STYLE.WS_BORDER;
break;
}
if (_textBoxFlags[s_multiline])
{
cp.Style |= PInvoke.ES_MULTILINE;
if (_textBoxFlags[s_wordWrap])
{
cp.Style &= ~PInvoke.ES_AUTOHSCROLL;
}
}
return cp;
}
}
/// <summary>
/// This property is overridden and hidden from statement completion
/// on controls that are based on Win32 Native Controls.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override bool DoubleBuffered
{
get => base.DoubleBuffered;
set => base.DoubleBuffered = value;
}
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new event EventHandler? Click
{
add => base.Click += value;
remove => base.Click -= value;
}
[Browsable(true)]
[EditorBrowsable(EditorBrowsableState.Always)]
public new event MouseEventHandler? MouseClick
{
add => base.MouseClick += value;
remove => base.MouseClick -= value;
}
protected override Cursor DefaultCursor
{
get
{
return Cursors.IBeam;
}
}
/// <summary>
/// Deriving classes can override this to configure a default size for their control.
/// This is more efficient than setting the size in the control's constructor.
/// </summary>
protected override Size DefaultSize
{
get
{
return new Size(100, PreferredHeight);
}
}
/// <summary>
/// Gets or sets the foreground color of the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DispId(PInvokeCore.DISPID_FORECOLOR)]
[SRDescription(nameof(SR.ControlForeColorDescr))]
public override Color ForeColor
{
get
{
if (ShouldSerializeForeColor())
{
return base.ForeColor;
}
else
{
return SystemColors.WindowText;
}
}
set => base.ForeColor = value;
}
/// <summary>
/// Gets or sets a value indicating whether the selected
/// text in the text box control remains highlighted when the control loses focus.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxHideSelectionDescr))]
public bool HideSelection
{
get
{
return _textBoxFlags[s_hideSelection];
}
set
{
if (_textBoxFlags[s_hideSelection] != value)
{
_textBoxFlags[s_hideSelection] = value;
RecreateHandle();
OnHideSelectionChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnHideSelectionChangedDescr))]
public event EventHandler? HideSelectionChanged
{
add => Events.AddHandler(s_hideSelectionChangedEvent, value);
remove => Events.RemoveHandler(s_hideSelectionChangedEvent, value);
}
/// <summary>
/// Internal version of ImeMode property. The ImeMode of TextBoxBase controls depend on its IME restricted
/// mode which is determined by the CanEnableIme property which checks whether the control is in Password or
/// ReadOnly mode.
/// </summary>
protected override ImeMode ImeModeBase
{
get => (DesignMode || CanEnableIme) ? base.ImeModeBase : ImeMode.Disable;
set => base.ImeModeBase = value;
}
/// <summary>
/// Gets or
/// sets the lines of text in an text box control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[MergableProperty(false)]
[Localizable(true)]
[AllowNull]
[SRDescription(nameof(SR.TextBoxLinesDescr))]
[Editor($"System.Windows.Forms.Design.StringArrayEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
public string[] Lines
{
get
{
string text = Text;
List<string> list = [];
int lineStart = 0;
while (lineStart < text.Length)
{
int lineEnd = lineStart;
for (; lineEnd < text.Length; lineEnd++)
{
char c = text[lineEnd];
if (c is '\r' or '\n')
{
break;
}
}
string line = text[lineStart..lineEnd];
list.Add(line);
// Treat "\r", "\r\n", and "\n" as new lines
if (lineEnd < text.Length && text[lineEnd] == '\r')
{
lineEnd++;
}
if (lineEnd < text.Length && text[lineEnd] == '\n')
{
lineEnd++;
}
lineStart = lineEnd;
}
// Corner case -- last character in Text is a new line; need to add blank line to list
if (text.Length > 0 && (text[^1] == '\r' || text[^1] == '\n'))
{
list.Add(string.Empty);
}
return [.. list];
}
set
{
// unparse this string list...
if (value is not null && value.Length > 0)
{
Text = string.Join(Environment.NewLine, value);
}
else
{
Text = string.Empty;
}
}
}
/// <summary>
/// Gets or sets the maximum number of
/// characters the user can type into the text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(32767)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxMaxLengthDescr))]
public virtual int MaxLength
{
get
{
return _maxLength;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
if (_maxLength != value)
{
_maxLength = value;
UpdateMaxLength();
}
}
}
/// <summary>
/// Gets or sets a value that indicates that the text box control has been modified by the user since
/// the control was created or its contents were last set.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxModifiedDescr))]
public bool Modified
{
get
{
if (IsHandleCreated)
{
bool curState = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETMODIFY) != 0;
if (_textBoxFlags[s_modified] != curState)
{
// Raise ModifiedChanged event. See WmReflectCommand for more info.
_textBoxFlags[s_modified] = curState;
OnModifiedChanged(EventArgs.Empty);
}
return curState;
}
else
{
return _textBoxFlags[s_modified];
}
}
set
{
if (Modified != value)
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY, (WPARAM)(BOOL)value);
// Must maintain this state always in order for the
// test in the Get method to work properly.
}
_textBoxFlags[s_modified] = value;
OnModifiedChanged(EventArgs.Empty);
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnModifiedChangedDescr))]
public event EventHandler? ModifiedChanged
{
add => Events.AddHandler(s_modifiedChangedEvent, value);
remove => Events.RemoveHandler(s_modifiedChangedEvent, value);
}
/// <summary>
/// Gets or sets a value indicating whether this
/// is a multiline text box control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[Localizable(true)]
[SRDescription(nameof(SR.TextBoxMultilineDescr))]
[RefreshProperties(RefreshProperties.All)]
public virtual bool Multiline
{
get
{
return _textBoxFlags[s_multiline];
}
set
{
if (_textBoxFlags[s_multiline] != value)
{
using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.Multiline))
{
_textBoxFlags[s_multiline] = value;
if (value)
{
// Multi-line textboxes do not have fixed height
SetStyle(ControlStyles.FixedHeight, false);
}
else
{
// Single-line textboxes may have fixed height, depending on AutoSize
SetStyle(ControlStyles.FixedHeight, AutoSize);
}
RecreateHandle();
AdjustHeight(false);
OnMultilineChanged(EventArgs.Empty);
}
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnMultilineChangedDescr))]
public event EventHandler? MultilineChanged
{
add => Events.AddHandler(s_multilineChangedEvent, value);
remove => Events.RemoveHandler(s_multilineChangedEvent, value);
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Padding Padding
{
get => base.Padding;
set => base.Padding = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRCategory(nameof(SR.CatLayout))]
[SRDescription(nameof(SR.ControlOnPaddingChangedDescr))]
public new event EventHandler? PaddingChanged
{
add => base.PaddingChanged += value;
remove => base.PaddingChanged -= value;
}
/// <summary>
/// Determines if the control is in password protect mode. This is overridden in TextBox and
/// MaskedTextBox and is false by default so RichTextBox that doesn't support Password doesn't
/// have to care about this.
/// </summary>
private protected virtual bool PasswordProtect => false;
/// <summary>
/// Returns the preferred
/// height for a single-line text box.
/// </summary>
[SRCategory(nameof(SR.CatLayout))]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Advanced)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxPreferredHeightDescr))]
public int PreferredHeight
{
get
{
// COMPAT we must return the same busted height we did in Everett, even
// if it doesn't take multiline and word wrap into account. For better accuracy and/or wrapping use
// GetPreferredSize instead.
int height = FontHeight;
if (_borderStyle != BorderStyle.None)
{
height += SystemInformation.GetBorderSizeForDpi(_deviceDpi).Height * 4 + 3;
}
return height;
}
}
// GetPreferredSizeCore
// This method can return a different value than PreferredHeight! It properly handles
// border style + multiline and wordwrap.
internal override Size GetPreferredSizeCore(Size proposedConstraints)
{
// 3px vertical space is required between the text and the border to keep the last
// line from being clipped.
// This 3 pixel size was added in everett and we do this to maintain compat.
// old everett behavior was FontHeight + [SystemInformation.BorderSize.Height * 4 + 3]
// however the [ ] was only added if borderstyle was not none.
Size bordersAndPadding = SizeFromClientSize(Size.Empty) + Padding.Size;
if (BorderStyle != BorderStyle.None)
{
bordersAndPadding += new Size(0, 3);
}
if (BorderStyle == BorderStyle.FixedSingle)
{
// Bump these by 2px to match BorderStyle.Fixed3D - they'll be omitted from the SizeFromClientSize call.
bordersAndPadding.Width += 2;
bordersAndPadding.Height += 2;
}
// Reduce constraints by border/padding size
proposedConstraints -= bordersAndPadding;
// Fit the text to the remaining space.
// Fixed for .NET Framework 4.0
TextFormatFlags format = TextFormatFlags.NoPrefix;
if (!Multiline)
{
format |= TextFormatFlags.SingleLine;
}
else if (WordWrap)
{
format |= TextFormatFlags.WordBreak;
}
Size textSize = TextRenderer.MeasureText(Text, Font, proposedConstraints, format);
// We use this old computation as a lower bound to ensure backwards compatibility.
textSize.Height = Math.Max(textSize.Height, FontHeight);
Size preferredSize = textSize + bordersAndPadding;
return preferredSize;
}
/// <summary>
/// Get the currently selected text start position and length. Use this method internally
/// to avoid calling SelectionStart + SelectionLength each of which does essentially the
/// same (save one message round trip).
/// </summary>
internal unsafe void GetSelectionStartAndLength(out int start, out int length)
{
int end = 0;
if (!IsHandleCreated)
{
// It is possible that the cached values are no longer valid if the Text has been changed
// while the control does not have a handle. We need to return valid values. We also need
// to keep the old cached values in case the Text is changed again making the cached values
// valid again.
AdjustSelectionStartAndEnd(_selectionStart, _selectionLength, out start, out end, -1);
length = end - start;
}
else
{
start = 0;
int startResult = 0;
PInvokeCore.SendMessage(this, PInvokeCore.EM_GETSEL, (WPARAM)(&startResult), ref end);
start = startResult;
// Here, we return the max of either 0 or the # returned by
// the windows call. This eliminates a problem on nt4 where
// a huge negative # is being returned.
start = Math.Max(0, start);
// ditto for end
end = Math.Max(0, end);
length = end - start;
}
#if DEBUG
{
string t = WindowText;
int len;
end = start + length - 1;
if (t is null)
{
len = 0;
}
else
{
len = t.Length;
}
Debug.Assert(end <= len,
$"SelectionEnd is outside the set of valid caret positions for the current WindowText (end ={end}, WindowText.Length ={len})");
}
#endif
}
/// <summary>
/// Gets or sets a value indicating whether text in the text box is read-only.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(false)]
[RefreshProperties(RefreshProperties.Repaint)]
[SRDescription(nameof(SR.TextBoxReadOnlyDescr))]
public bool ReadOnly
{
get
{
return _textBoxFlags[s_readOnly];
}
set
{
if (_textBoxFlags[s_readOnly] != value)
{
_textBoxFlags[s_readOnly] = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETREADONLY, (WPARAM)(BOOL)value);
}
OnReadOnlyChanged(EventArgs.Empty);
VerifyImeRestrictedModeChanged();
}
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.TextBoxBaseOnReadOnlyChangedDescr))]
public event EventHandler? ReadOnlyChanged
{
add => Events.AddHandler(s_readOnlyChangedEvent, value);
remove => Events.RemoveHandler(s_readOnlyChangedEvent, value);
}
/// <summary>
/// The currently selected text in the control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[AllowNull]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectedTextDescr))]
public virtual string SelectedText
{
get
{
GetSelectionStartAndLength(out int selStart, out int selLength);
return Text.Substring(selStart, selLength);
}
set
{
SetSelectedTextInternal(value, true);
}
}
/// <summary>
/// Replaces the selected text with the one passed in.
/// </summary>
internal virtual void SetSelectedTextInternal(string? text, bool clearUndo)
{
if (!IsHandleCreated)
{
CreateHandle();
}
text ??= string.Empty;
// The EM_LIMITTEXT message limits only the text the user can enter. It does not affect any text
// already in the edit control when the message is sent, nor does it affect the length of the text
// copied to the edit control by the WM_SETTEXT message.
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT);
if (clearUndo)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_REPLACESEL, 0, text);
// For consistency with Text, we clear the modified flag
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY);
ClearUndo();
}
else
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_REPLACESEL, (WPARAM)(-1), text);
}
// Re-enable user input.
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT, (WPARAM)_maxLength);
}
/// <summary>
/// Gets or sets the number of characters selected in the text box.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectionLengthDescr))]
public virtual int SelectionLength
{
get
{
GetSelectionStartAndLength(out int _, out int length);
return length;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
GetSelectionStartAndLength(out int start, out int length);
if (value != length)
{
Select(start, value);
}
}
}
/// <summary>
/// Gets or sets the starting point of text selected in the text box.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.TextBoxSelectionStartDescr))]
public int SelectionStart
{
get
{
GetSelectionStartAndLength(out int start, out _);
return start;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
Select(value, SelectionLength);
}
}
/// <summary>
/// Gets or sets the current text in the text box.
/// </summary>
[Localizable(true)]
[AllowNull]
[Editor($"System.ComponentModel.Design.MultilineStringEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
public override string Text
{
get => base.Text;
set
{
if (value != base.Text)
{
base.Text = value;
if (IsHandleCreated)
{
// clear the modified flag
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY);
}
}
}
}
[Browsable(false)]
public virtual int TextLength
// Note: Currently WinForms does not fully support surrogates. If
// the text contains surrogate characters this property may return incorrect values.
=> IsHandleCreated ? PInvokeCore.GetWindowTextLength(this) : Text.Length;
internal override string WindowText
{
get => base.WindowText;
set
{
value ??= string.Empty;
// Since setting the WindowText while the handle is created generates a WM_COMMAND message, we must trap
// that case and prevent the event from getting fired, or we get double "TextChanged" events.
if (!WindowText.Equals(value))
{
_textBoxFlags[s_codeUpdateText] = true;
try
{
base.WindowText = value;
}
finally
{
_textBoxFlags[s_codeUpdateText] = false;
}
}
}
}
/// <summary>
/// In certain circumstances we might have to force text into the window whether or not the text is the same.
/// Make this a method on <see cref="TextBoxBase"/> rather than <see cref="RichTextBox"/> (which is the only
/// control that needs this at this point), since we need to set <see cref="s_codeUpdateText"/>.
/// </summary>
internal void ForceWindowText(string? value)
{
value ??= string.Empty;
_textBoxFlags[s_codeUpdateText] = true;
try
{
if (IsHandleCreated)
{
PInvoke.SetWindowText(this, value);
}
else
{
Text = value.Length == 0 ? null : value;
}
}
finally
{
_textBoxFlags[s_codeUpdateText] = false;
}
}
/// <summary>
/// Gets or sets a value indicating whether a multiline text box control automatically wraps words to the
/// beginning of the next line when necessary.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[DefaultValue(true)]
[SRDescription(nameof(SR.TextBoxWordWrapDescr))]
public bool WordWrap
{
get
{
return _textBoxFlags[s_wordWrap];
}
set
{
using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.WordWrap))
{
if (_textBoxFlags[s_wordWrap] != value)
{
_textBoxFlags[s_wordWrap] = value;
RecreateHandle();
}
}
}
}
/// <summary>
/// Adjusts the height of a single-line edit control to match the height of
/// the control's font.
/// </summary>
private void AdjustHeight(bool returnIfAnchored)
{
// If we're anchored to two opposite sides of the form, don't adjust the size because
// we'll lose our anchored size by resetting to the requested width.
if (returnIfAnchored && (Anchor & (AnchorStyles.Top | AnchorStyles.Bottom)) == (AnchorStyles.Top | AnchorStyles.Bottom))
{
return;
}
int saveHeight = _requestedHeight;
try
{
if (_textBoxFlags[s_autoSize] && !_textBoxFlags[s_multiline])
{
Height = PreferredHeight;
}
else
{
int curHeight = Height;
// Changing the font of a multi-line textbox can sometimes cause a painting problem
// The only workaround I can find is to size the textbox big enough for the font, and
// then restore its correct size.
if (_textBoxFlags[s_multiline])
{
Height = Math.Max(saveHeight, PreferredHeight + 2); // 2 = fudge factor
}
_integralHeightAdjust = true;
try
{
Height = saveHeight;
}
finally
{
_integralHeightAdjust = false;
}
}
}
finally
{
_requestedHeight = saveHeight;
}
}
/// <summary>
/// Append text to the current text of text box.
/// </summary>
public void AppendText(string? text)
{
if (string.IsNullOrEmpty(text))
{
return;
}
GetSelectionStartAndLength(out int selStart, out int selLength);
try
{
// This enables you to use SelectionColor to AppendText in color.
int endOfText = GetEndPosition();
SelectInternal(endOfText, endOfText, endOfText);
SelectedText = text;
}
finally
{
// If AppendText is called when the control is docked and the form is minimized,
// all the text will scroll to the top and the control will look empty when the
// form is restored. We work around this by selecting back whatever was originally
// selected when AppendText was called.
if (Width == 0 || Height == 0)
{
Select(selStart, selLength);
}
}
}
/// <summary>
/// Clears all text from the text box control.
/// </summary>
public void Clear()
{
Text = null;
}
/// <summary>
/// Clears information about the most recent operation
/// from the undo buffer of the text box.
/// </summary>
public void ClearUndo()
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_EMPTYUNDOBUFFER);
}
}
protected bool ContainsNavigationKeyCode(Keys keyCode) => keyCode switch
{
Keys.Up or Keys.Down or Keys.PageUp or Keys.PageDown or Keys.Home or Keys.End or Keys.Left or Keys.Right => true,
_ => false,
};
/// <summary>
/// Copies the current selection in the text box to the Clipboard.
/// </summary>
public void Copy() => PInvokeCore.SendMessage(this, PInvokeCore.WM_COPY);
protected override AccessibleObject CreateAccessibilityInstance() => new TextBoxBaseAccessibleObject(this);
protected override void CreateHandle()
{
// This "creatingHandle" stuff is to avoid property change events
// when we set the Text property.
_textBoxFlags[s_creatingHandle] = true;
try
{
base.CreateHandle();
// send EM_SETSEL message
SetSelectionOnHandle();
}
finally
{
_textBoxFlags[s_creatingHandle] = false;
}
}
/// <summary>
/// Moves the current selection in the text box to the Clipboard.
/// </summary>
public void Cut() => PInvokeCore.SendMessage(this, PInvokeCore.WM_CUT);
/// <summary>
/// Returns the text end position (one past the last input character). This property is virtual to allow MaskedTextBox
/// to set the last input char position as opposed to the last char position which may be a mask character.
/// </summary>
internal virtual int GetEndPosition()
{
// +1 because RichTextBox has this funny EOF pseudo-character after all the text.
return IsHandleCreated ? TextLength + 1 : TextLength;
}
/// <summary>
/// Overridden to handle TAB key.
/// </summary>
protected override bool IsInputKey(Keys keyData)
{
if ((keyData & Keys.Alt) != Keys.Alt)
{
switch (keyData & Keys.KeyCode)
{
case Keys.Tab:
// Single-line RichEd's want tab characters (see WM_GETDLGCODE),
// so we don't ask it
return Multiline && _textBoxFlags[s_acceptsTab] && ((keyData & Keys.Control) == 0);
case Keys.Escape:
if (Multiline)
{
return false;
}
break;
case Keys.Back:
if (!ReadOnly)
{
return true;
}
break;
case Keys.PageUp:
case Keys.PageDown:
case Keys.Home:
case Keys.End:
return true;
// else fall through to base
}
}
return base.IsInputKey(keyData);
}
/// <summary>
/// Overridden to update the newly created handle with the settings of the
/// MaxLength and PasswordChar properties.
/// </summary>
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!IsHandleCreated)
{
return;
}
// it's likely here that the create params could have changed
// the border size/etc.
CommonProperties.xClearPreferredSizeCache(this);
AdjustHeight(true);
UpdateMaxLength();
if (_textBoxFlags[s_modified])
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY, (WPARAM)(BOOL)true);
}
if (_textBoxFlags[s_scrollToCaretOnHandleCreated])
{
ScrollToCaret();
_textBoxFlags[s_scrollToCaretOnHandleCreated] = false;
}
}
protected override void OnHandleDestroyed(EventArgs e)
{
_textBoxFlags[s_modified] = Modified;
_textBoxFlags[s_setSelectionOnHandleCreated] = true;
// Update text selection cached values to be restored when recreating the handle.
GetSelectionStartAndLength(out _selectionStart, out _selectionLength);
base.OnHandleDestroyed(e);
}
/// <summary>
/// Replaces the current selection in the text box with the contents of the Clipboard.
/// </summary>
public void Paste() => PInvokeCore.SendMessage(this, PInvokeCore.WM_PASTE);
protected override bool ProcessDialogKey(Keys keyData)
{
Keys keyCode = keyData & Keys.KeyCode;
if (keyCode == Keys.Tab && AcceptsTab && (keyData & Keys.Control) != 0)
{
// When this control accepts Tabs, Ctrl-Tab is treated exactly like Tab.
keyData &= ~Keys.Control;
}
return base.ProcessDialogKey(keyData);
}
/// <summary>
/// TextBox / RichTextBox Onpaint.
/// </summary>
/// <hideinheritance/>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event PaintEventHandler? Paint
{
add => base.Paint += value;
remove => base.Paint -= value;
}
protected virtual void OnAcceptsTabChanged(EventArgs e)
{
if (Events[s_acceptsTabChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected virtual void OnBorderStyleChanged(EventArgs e)
{
if (Events[s_borderStyleChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
AdjustHeight(false);
}
protected virtual void OnHideSelectionChanged(EventArgs e)
{
if (Events[s_hideSelectionChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected virtual void OnModifiedChanged(EventArgs e)
{
if (Events[s_modifiedChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
/// <summary>
/// Raises the MouseUp event.
/// </summary>
protected override void OnMouseUp(MouseEventArgs mevent)
{
if (mevent is not null && mevent.Button == MouseButtons.Left)
{
if (!ValidationCancelled && PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
{
if (!_doubleClickFired)
{
OnClick(mevent);
OnMouseClick(mevent);
}
else
{
_doubleClickFired = false;
OnDoubleClick(mevent);
OnMouseDoubleClick(mevent);
}
}
_doubleClickFired = false;
}
// Because the code has been like that since long time, we assume that mevent is not null.
base.OnMouseUp(mevent!);
}
protected virtual void OnMultilineChanged(EventArgs e)
{
if (Events[s_multilineChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override void OnPaddingChanged(EventArgs e)
{
base.OnPaddingChanged(e);
AdjustHeight(false);
}
protected virtual void OnReadOnlyChanged(EventArgs e)
{
if (Events[s_readOnlyChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
protected override void OnTextChanged(EventArgs e)
{
// since AutoSize existed in Everett, (and is the default) we can't
// relayout the parent when the "PreferredSize" of the control changes.
// this means a multiline = true textbox won't naturally grow in height when
// the text changes.
CommonProperties.xClearPreferredSizeCache(this);
base.OnTextChanged(e);
if (PInvoke.UiaClientsAreListening())
{
RaiseAccessibilityTextChangedEvent();
}
}
private protected virtual void RaiseAccessibilityTextChangedEvent()
{
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextChangedEventId);
using var textVariant = PasswordProtect ? (VARIANT)string.Empty : (VARIANT)Text;
AccessibilityObject.RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID.UIA_ValueValuePropertyId, textVariant, textVariant);
}
}
/// <summary>
/// Returns the character nearest to the given point.
/// </summary>
public virtual char GetCharFromPosition(Point pt)
{
string t = Text;
int index = GetCharIndexFromPosition(pt);
return (index < 0 || index >= t.Length) ? (char)0 : t[index];
}
/// <summary>
/// Returns the index of the character nearest to the given point.
/// </summary>
public virtual int GetCharIndexFromPosition(Point pt)
{
int index = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CHARFROMPOS, 0, PARAM.FromPoint(pt));
index = PARAM.LOWORD(index);
if (index < 0)
{
index = 0;
}
else
{
string t = Text;
// EM_CHARFROMPOS will return an invalid number if the last character in the RichEdit
// is a newline.
//
if (index >= t.Length)
{
index = Math.Max(t.Length - 1, 0);
}
}
return index;
}
/// <summary>
/// Returns the number of the line containing a specified character position
/// in a textbox. Note that this returns the physical line number
/// and not the conceptual line number. For example, if the first conceptual
/// line (line number 0) word-wraps and extends to the second line, and if
/// you pass the index of a overflowed character, GetLineFromCharIndex would
/// return 1 and not 0.
/// </summary>
public virtual int GetLineFromCharIndex(int index) => (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_LINEFROMCHAR, (WPARAM)index);
/// <summary>
/// Returns the location of the character at the given index.
/// </summary>
public virtual Point GetPositionFromCharIndex(int index)
{
if (index < 0 || index >= Text.Length)
{
return Point.Empty;
}
int i = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_POSFROMCHAR, (WPARAM)index);
return new Point(PARAM.SignedLOWORD(i), PARAM.SignedHIWORD(i));
}
/// <summary>
/// Returns the index of the first character of a given line. Returns -1 of lineNumber is invalid.
/// </summary>
public int GetFirstCharIndexFromLine(int lineNumber)
{
ArgumentOutOfRangeException.ThrowIfNegative(lineNumber);
return (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_LINEINDEX, (WPARAM)lineNumber);
}
/// <summary>
/// Returns the index of the first character of the line where the caret is.
/// </summary>
public int GetFirstCharIndexOfCurrentLine() => (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_LINEINDEX, (WPARAM)(-1));
/// <summary>
/// Ensures that the caret is visible in the TextBox window, by scrolling the
/// TextBox control surface if necessary.
/// </summary>
public unsafe void ScrollToCaret()
{
if (!IsHandleCreated)
{
_textBoxFlags[s_scrollToCaretOnHandleCreated] = true;
return;
}
if (string.IsNullOrEmpty(WindowText))
{
// If there is no text, then there is no place to go.
return;
}
using ComScope<IRichEditOle> richEdit = new(null);
if (PInvokeCore.SendMessage(this, PInvokeCore.EM_GETOLEINTERFACE, 0, (void**)richEdit) == 0)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SCROLLCARET);
return;
}
using var textDocument = richEdit.TryQuery<ITextDocument>(out HRESULT hr);
if (hr.Succeeded)
{
// When the user calls RichTextBox::ScrollToCaret we want the RichTextBox to show as much text as
// possible. Here is how we do that:
//
// 1. We scroll the RichTextBox all the way to the bottom so the last line of text is the last visible line.
// 2. We get the first visible line.
// 3. If the first visible line is smaller than the start of the selection, then we are done:
// The selection fits inside the RichTextBox display rectangle.
// 4. Otherwise, scroll the selection to the top of the RichTextBox.
GetSelectionStartAndLength(out int selStart, out int selLength);
int selStartLine = GetLineFromCharIndex(selStart);
using ComScope<ITextRange> windowTextRange = new(null);
textDocument.Value->Range(WindowText.Length - 1, WindowText.Length - 1, windowTextRange).ThrowOnFailure();
// 1. Scroll the RichTextBox all the way to the bottom
windowTextRange.Value->ScrollIntoView((int)tomConstants.tomEnd).ThrowOnFailure();
// 2. Get the first visible line.
int firstVisibleLine = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETFIRSTVISIBLELINE);
// 3. If the first visible line is smaller than the start of the selection, we are done.
if (firstVisibleLine <= selStartLine)
{
return;
}
else
{
// 4. Scroll the selection to the top of the RichTextBox.
using ComScope<ITextRange> selectionTextRange = new(null);
textDocument.Value->Range(selStart, selStart + selLength, selectionTextRange).ThrowOnFailure();
selectionTextRange.Value->ScrollIntoView((int)tomConstants.tomStart).ThrowOnFailure();
return;
}
}
PInvokeCore.SendMessage(this, PInvokeCore.EM_SCROLLCARET);
}
/// <summary>
/// Sets the SelectionLength to 0.
/// </summary>
public void DeselectAll()
{
SelectionLength = 0;
}
/// <summary>
/// Selects a range of text in the text box.
/// </summary>
public void Select(int start, int length)
{
ArgumentOutOfRangeException.ThrowIfNegative(start);
int textLen = TextLength;
if (start > textLen)
{
// We shouldn't allow positive length if you're starting at the end, but
// should allow negative length.
long longLength = Math.Min(0, (long)length + start - textLen);
if (longLength < int.MinValue)
{
length = int.MinValue;
}
else
{
length = (int)longLength;
}
start = textLen;
}
SelectInternal(start, length, textLen);
}
/// <summary>
/// Performs the actual select without doing arg checking.
///
/// Send in -1 for the textLen parameter if you don't have the text
/// length cached when calling this method. It will be computed.
/// But if you do have it cached, please pass it in. This will avoid
/// the expensive call to the TextLength property.
/// </summary>
private protected virtual void SelectInternal(int selectionStart, int selectionLength, int textLength)
{
// if our handle is created - send message...
if (IsHandleCreated)
{
AdjustSelectionStartAndEnd(selectionStart, selectionLength, out int start, out int end, textLength);
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETSEL, (WPARAM)start, (LPARAM)end);
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationEvent(end == 0
? UIA_EVENT_ID.UIA_AutomationFocusChangedEventId
: UIA_EVENT_ID.UIA_Text_TextSelectionChangedEventId);
}
}
else
{
// otherwise, wait until handle is created to send this message.
// Store the indices until then...
_selectionStart = selectionStart;
_selectionLength = selectionLength;
_textBoxFlags[s_setSelectionOnHandleCreated] = true;
}
}
/// <summary>
/// Selects all text in the text box.
/// </summary>
public void SelectAll()
{
int textLength = TextLength;
SelectInternal(0, textLength, textLength);
}
/// <summary>
/// Overrides Control.setBoundsCore to enforce autoSize.
/// </summary>
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
if (!_integralHeightAdjust && height != Height)
{
_requestedHeight = height;
}
if (_textBoxFlags[s_autoSize] && !_textBoxFlags[s_multiline])
{
height = PreferredHeight;
}
base.SetBoundsCore(x, y, width, height, specified);
}
private static void Swap(ref int n1, ref int n2) => (n1, n2) = (n2, n1);
// Send in -1 if you don't have the text length cached
// when calling this method. It will be computed. If not,
// please pass in the text length as the last parameter.
// This will avoid the expensive call to the TextLength
// property.
internal void AdjustSelectionStartAndEnd(int selStart, int selLength, out int start, out int end, int textLen)
{
start = selStart;
end = 0;
if (start <= -1)
{
start = -1;
}
else
{
int textLength;
if (textLen >= 0)
{
textLength = textLen;
}
else
{
textLength = TextLength;
}
if (start > textLength)
{
start = textLength;
}
checked
{
try
{
end = start + selLength;
}
catch (OverflowException)
{
// Since we overflowed, cap at the max/min value: we'll correct the value below
end = start > 0 ? int.MaxValue : int.MinValue;
}
}
// Make sure end is in range
if (end < 0)
{
end = 0;
}
else if (end > textLength)
{
end = textLength;
}
}
}
// Called by CreateHandle or OnHandleCreated
internal void SetSelectionOnHandle()
{
Debug.Assert(IsHandleCreated, "Don't call this method until the handle is created.");
if (_textBoxFlags[s_setSelectionOnHandleCreated])
{
_textBoxFlags[s_setSelectionOnHandleCreated] = false;
AdjustSelectionStartAndEnd(_selectionStart, _selectionLength, out int start, out int end, -1);
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETSEL, (WPARAM)start, (LPARAM)end);
}
}
/// <summary>
/// Converts byte offset to unicode offsets.
/// When processing WM_GETSEL/WM_SETSEL, EDIT control works with byte offsets instead of character positions
/// as opposed to RICHEDIT which does it always as character positions.
/// This method is used when handling the WM_GETSEL message.
/// </summary>
private static void ToUnicodeOffsets(string str, ref int start, ref int end)
{
Encoding e = Encoding.Default;
byte[] bytes = e.GetBytes(str);
bool swap = start > end;
if (swap)
{
Swap(ref start, ref end);
}
// Make sure start and end are within the string
//
if (start < 0)
{
start = 0;
}
if (start > bytes.Length)
{
start = bytes.Length;
}
if (end > bytes.Length)
{
end = bytes.Length;
}
// IMPORTANT: Avoid off-by-1 errors!
// The end value passed in is the character immediately after the last character selected.
int newStart = start == 0 ? 0 : e.GetCharCount(bytes, 0, start);
end = newStart + e.GetCharCount(bytes, start, end - start);
start = newStart;
if (swap)
{
Swap(ref start, ref end);
}
}
/// <summary>
/// Converts unicode offset to byte offsets.
/// When processing WM_GETSEL/WM_SETSEL, EDIT control works with byte offsets instead of character positions
/// as opposed to RICHEDIT which does it always as character positions.
/// This method is used when handling the WM_SETSEL message.
/// </summary>
internal static void ToDbcsOffsets(string str, ref int start, ref int end)
{
Encoding e = Encoding.Default;
bool swap = start > end;
if (swap)
{
Swap(ref start, ref end);
}
// Make sure start and end are within the string
//
if (start < 0)
{
start = 0;
}
if (start > str.Length)
{
start = str.Length;
}
if (end < start)
{
end = start;
}
if (end > str.Length)
{
end = str.Length;
}
// IMPORTANT: Avoid off-by-1 errors!
// The end value passed in is the character immediately after the last character selected.
int newStart = start == 0 ? 0 : e.GetByteCount(str.AsSpan(0, start));
end = newStart + e.GetByteCount(str.AsSpan(start, end - start));
start = newStart;
if (swap)
{
Swap(ref start, ref end);
}
}
/// <summary>
/// Provides some interesting information for the TextBox control in
/// String form.
/// </summary>
public override string ToString()
{
string s = base.ToString();
string txt = Text;
if (txt.Length > 40)
{
txt = $"{txt.AsSpan(0, 40)}...";
}
return $"{s}, Text: {txt}";
}
/// <summary>
/// Undoes the last edit operation in the text box.
/// </summary>
public void Undo() => PInvokeCore.SendMessage(this, PInvokeCore.EM_UNDO);
internal virtual void UpdateMaxLength()
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_LIMITTEXT, (WPARAM)_maxLength);
}
}
internal override HBRUSH InitializeDCForWmCtlColor(HDC dc, MessageId msg)
{
if (msg == PInvokeCore.WM_CTLCOLORSTATIC && !ShouldSerializeBackColor())
{
// Let the Win32 Edit control handle background colors itself.
// This is necessary because a disabled edit control will display a different
// BackColor than when enabled.
return default;
}
else
{
return base.InitializeDCForWmCtlColor(dc, msg);
}
}
private void WmReflectCommand(ref Message m)
{
if (!_textBoxFlags[s_codeUpdateText] && !_textBoxFlags[s_creatingHandle])
{
uint hiword = m.WParamInternal.HIWORD;
if (hiword == PInvoke.EN_CHANGE && CanRaiseTextChangedEvent)
{
OnTextChanged(EventArgs.Empty);
}
else if (hiword == PInvoke.EN_UPDATE)
{
// Force update to the Modified property, which will trigger ModifiedChanged event handlers
_ = Modified;
}
}
}
private void WmSetFont(ref Message m)
{
base.WndProc(ref m);
if (!_textBoxFlags[s_multiline])
{
PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMARGINS, (WPARAM)(PInvoke.EC_LEFTMARGIN | PInvoke.EC_RIGHTMARGIN));
}
}
private void WmGetDlgCode(ref Message m)
{
base.WndProc(ref m);
m.ResultInternal = AcceptsTab
? (LRESULT)(m.ResultInternal | (int)PInvoke.DLGC_WANTTAB)
: (LRESULT)(m.ResultInternal & ~(int)(PInvoke.DLGC_WANTTAB | PInvoke.DLGC_WANTALLKEYS));
}
/// <summary>
/// Handles the WM_CONTEXTMENU message. Show the ContextMenuStrip if present.
/// </summary>
private void WmTextBoxContextMenu(ref Message m)
{
if (ContextMenuStrip is null)
{
return;
}
Point client;
bool keyboardActivated = false;
// LParam will be -1 when the user invokes the context menu with the keyboard.
if (m.LParamInternal == -1)
{
keyboardActivated = true;
client = new Point(Width / 2, Height / 2);
}
else
{
client = PointToClient(PARAM.ToPoint(m.LParamInternal));
}
// Only show the context menu when clicked in the client area.
if (ClientRectangle.Contains(client))
{
ContextMenuStrip.ShowInternal(this, client, keyboardActivated);
}
}
/// <summary>
/// The control's window procedure. Inheriting classes can override this
/// to add extra functionality, but should not forget to call
/// base.wndProc(m); to ensure the control continues to function properly.
/// </summary>
protected override void WndProc(ref Message m)
{
switch (m.MsgInternal)
{
case PInvokeCore.WM_LBUTTONDBLCLK:
_doubleClickFired = true;
base.WndProc(ref m);
break;
case MessageId.WM_REFLECT_COMMAND:
WmReflectCommand(ref m);
break;
case PInvokeCore.WM_GETDLGCODE:
WmGetDlgCode(ref m);
break;
case PInvokeCore.WM_SETFONT:
WmSetFont(ref m);
break;
case PInvokeCore.WM_CONTEXTMENU:
if (ShortcutsEnabled)
{
// Calling base will find ContextMenus in this order:
// 1) ContextMenuStrip 2) SystemMenu
base.WndProc(ref m);
}
else
{
// We'll handle this message so we can hide the
// SystemMenu if ContextMenuStrip menus are null
WmTextBoxContextMenu(ref m);
}
break;
case PInvokeCore.WM_DESTROY:
if (TryGetAccessibilityObject(out AccessibleObject? @object) && @object is TextBoxBaseAccessibleObject accessibleObject &&
!RecreatingHandle)
{
accessibleObject.ClearObjects();
}
base.WndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
}
|