// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Drawing.Design; using System.Globalization; using System.Windows.Forms.Design; using System.Windows.Forms.VisualStyles; using Microsoft.Win32; using Windows.Win32.System.Variant; using Windows.Win32.UI.Accessibility; using static System.Windows.Forms.ControlPaint; namespace System.Windows.Forms.PropertyGridInternal; internal sealed partial class PropertyGridView : Control, IWin32Window, IWindowsFormsEditorService, IServiceProvider { private static Point InvalidPoint { get; } = new(int.MinValue, int.MinValue); // Constants private const int EditIndent = 0; private const int OutlineIndent = 10; private const int LogicalOutlineSize = 9; private int _outlineSize = LogicalOutlineSize; private const int OutlineSizeExplorerTreeStyle = 16; private int _outlineSizeExplorerTreeStyle = OutlineSizeExplorerTreeStyle; private const int LogicalPaintWidth = 20; private int _paintWidth = LogicalPaintWidth; private const int LogicalPaintIndent = 26; private int _paintIndent = LogicalPaintIndent; private const int LogicalMaxListBoxHeight = 200; private int _maxListBoxHeight = LogicalMaxListBoxHeight; private const int RowLabel = 1; private const int RowValue = 2; internal const short GdiPlusSpace = 2; internal const int MaxRecurseExpand = 10; private const int DotDotDotIconWidth = 7; private const int DotDotDotIconHeight = 8; private const int DownArrowIconWidth = 16; private const int DownArrowIconHeight = 16; private const int Offset2Pixels = 2; private int _offset2Units = Offset2Pixels; private static Point InvalidPosition { get; } = new(int.MinValue, int.MinValue); private Font? _boldFont; private Color _grayTextColor; // For backwards compatibility of default colors private bool _grayTextColorModified; private GridEntryCollection? _allGridEntries; // Row information public int TotalProperties { get; private set; } = -1; private int _visibleRows = -1; private int _labelWidth = -1; public double _labelRatio = 2; // ratio of whole row to label width private short _requiredLabelPaintMargin = GdiPlusSpace; // current selected row and tooltip. private int _selectedRow = -1; private GridEntry? _selectedGridEntry; private int _tipInfo = -1; // Editors & controls private GridViewTextBox? _editTextBox; private DropDownButton? _dropDownButton; private DropDownButton? _dialogButton; private GridViewListBox? _listBox; private DropDownHolder? _dropDownHolder; private Rectangle _lastClientRect = Rectangle.Empty; private Control? _currentEditor; private VScrollBar? _scrollBar; private GridToolTip? _toolTip; private GridErrorDialog? _errorDialog; private Flags _flags = Flags.NeedsRefresh | Flags.IsNewSelection | Flags.NeedUpdateUIBasedOnFont; private ErrorState _errorState = ErrorState.None; private Point _location = new(1, 1); private string? _originalTextValue; // original text, in case of ESC private int _cumulativeVerticalWheelDelta; private long _rowSelectTime; private Point _rowSelectPos = Point.Empty; // the position that we clicked on a row to test for double clicks private Point _lastMouseDown = InvalidPosition; private int _lastMouseMove; private GridEntry? _lastClickedEntry; private IServiceProvider? _serviceProvider; private IHelpService? _topHelpService; private IHelpService? _helpService; private readonly EventHandler _valueClick; private readonly EventHandler _labelClick; private readonly EventHandler _outlineClick; private readonly EventHandler _valueDoubleClick; private readonly EventHandler _labelDoubleClick; private readonly GridEntryRecreateChildrenEventHandler _recreateChildren; private int _cachedRowHeight = -1; private GridPositionData? _positionData; public PropertyGridView(IServiceProvider? serviceProvider, PropertyGrid propertyGrid) : base() { _paintWidth = ScaleHelper.ScaleToInitialSystemDpi(LogicalPaintWidth); _paintIndent = ScaleHelper.ScaleToInitialSystemDpi(LogicalPaintIndent); _outlineSizeExplorerTreeStyle = ScaleHelper.ScaleToInitialSystemDpi(OutlineSizeExplorerTreeStyle); _outlineSize = ScaleHelper.ScaleToInitialSystemDpi(LogicalOutlineSize); _maxListBoxHeight = ScaleHelper.ScaleToInitialSystemDpi(LogicalMaxListBoxHeight); _valueClick = OnGridEntryValueClick; _labelClick = OnGridEntryLabelClick; _outlineClick = OnGridEntryOutlineClick; _valueDoubleClick = OnGridEntryValueDoubleClick; _labelDoubleClick = OnGridEntryLabelDoubleClick; _recreateChildren = OnRecreateChildren; OwnerGrid = propertyGrid; _serviceProvider = serviceProvider; SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.ResizeRedraw, false); SetStyle(ControlStyles.UserMouse, true); BackColor = SystemColors.Window; ForeColor = SystemColors.WindowText; _grayTextColor = SystemColors.GrayText; TabStop = true; Text = "PropertyGridView"; CreateUI(); LayoutWindow(invalidate: true); } public override Color BackColor { get => base.BackColor; set => base.BackColor = value; } [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Advanced)] public bool CanCopy => _selectedGridEntry is not null && (EditTextBox.Focused || !string.IsNullOrEmpty(_selectedGridEntry.GetPropertyTextValue())); [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Advanced)] public bool CanCut => CanCopy && _selectedGridEntry is not null && _selectedGridEntry.IsTextEditable; [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Advanced)] public bool CanPaste => _selectedGridEntry is not null && _selectedGridEntry.IsTextEditable; [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Advanced)] public bool CanUndo { get { if (!EditTextBox.Visible || !EditTextBox.Focused) { return false; } return PInvokeCore.SendMessage(EditTextBox, PInvokeCore.EM_CANUNDO) != 0; } } /// <summary> /// Shared drop-down button used to open the drop down editor (if applicable) for the currently selected row. /// </summary> /// <remarks> /// <para> /// The visibility of this button is primarily driven through associated <see cref="UITypeEditor"/>s for /// the selected row's <see cref="GridEntry"/>. /// </para> /// </remarks> internal DropDownButton DropDownButton { get { if (_dropDownButton is not null) { return _dropDownButton; } OwnerGrid.CheckInCreate(); _dropDownButton = new() { UseComboBoxTheme = true, RequestDarkModeRendering = Application.IsDarkModeEnabled, ControlButtonStyle = ModernControlButtonStyle.OpenDropDown | ModernControlButtonStyle.RoundedBorder }; Bitmap bitmap = CreateResizedBitmap( "Arrow", DownArrowIconWidth, DownArrowIconHeight); // For classic mode/backwards compatibility. _dropDownButton.Image = bitmap; _dropDownButton.BackColor = SystemColors.Control; _dropDownButton.ForeColor = SystemColors.ControlText; _dropDownButton.Click += OnButtonClick; _dropDownButton.GotFocus += OnDropDownButtonGotFocus; _dropDownButton.LostFocus += OnChildLostFocus; _dropDownButton.TabIndex = 2; CommonEditorSetup(_dropDownButton); _dropDownButton.Size = ScaleHelper.IsScalingRequirementMet ? new(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight) : new(SystemInformation.VerticalScrollBarArrowHeight, RowHeight); return _dropDownButton; } } /// <summary> /// Shared "..." button used to launch editor dialogs for the selected row if applicable. /// </summary> /// <remarks> /// <para> /// The visibility of this button is primarily driven through associated <see cref="UITypeEditor"/>s for /// the selected row's <see cref="GridEntry"/>. /// </para> /// </remarks> internal Button DialogButton { get { if (_dialogButton is not null) { return _dialogButton; } OwnerGrid.CheckInCreate(); _dialogButton = new DropDownButton { RequestDarkModeRendering = Application.IsDarkModeEnabled, ControlButtonStyle = ModernControlButtonStyle.Ellipse | ModernControlButtonStyle.RoundedBorder, BackColor = SystemColors.Control, ForeColor = SystemColors.ControlText, TabIndex = 3, // For classic mode/backwards compatibility. Image = CreateResizedBitmap( "dotdotdot", DotDotDotIconWidth, DotDotDotIconHeight) }; _dialogButton.Click += OnButtonClick; _dialogButton.KeyDown += OnButtonKeyDown; _dialogButton.GotFocus += OnDropDownButtonGotFocus; _dialogButton.LostFocus += OnChildLostFocus; _dialogButton.Size = ScaleHelper.IsScalingRequirementMet ? new Size(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight) : new Size(SystemInformation.VerticalScrollBarArrowHeight, RowHeight); CommonEditorSetup(_dialogButton); return _dialogButton; } } /// <summary> /// The common text box for editing values. /// </summary> private GridViewTextBox EditTextBox { get { if (_editTextBox is not null) { return _editTextBox; } OwnerGrid.CheckInCreate(); _editTextBox = new(this) { BorderStyle = BorderStyle.None, AutoSize = false, TabStop = false, AcceptsReturn = true, BackColor = BackColor, ForeColor = ForeColor }; _editTextBox.KeyDown += OnEditKeyDown; _editTextBox.KeyPress += OnEditKeyPress; _editTextBox.GotFocus += OnEditGotFocus; _editTextBox.LostFocus += OnEditLostFocus; _editTextBox.MouseDown += OnEditMouseDown; _editTextBox.TextChanged += OnEditChange; _editTextBox.TabIndex = 1; CommonEditorSetup(_editTextBox); return _editTextBox; } } /// <summary> /// Represents the Editor's control accessible object. /// </summary> internal AccessibleObject EditAccessibleObject => EditTextBox.AccessibilityObject; internal GridViewListBox DropDownListBox { get { if (_listBox is null) { OwnerGrid.CheckInCreate(); _listBox = new(this) { DrawMode = DrawMode.OwnerDrawFixed }; _listBox.MouseUp += OnListMouseUp; _listBox.DrawItem += OnListDrawItem; _listBox.SelectedIndexChanged += OnListChange; _listBox.KeyDown += OnListKeyDown; _listBox.LostFocus += OnChildLostFocus; _listBox.Visible = true; _listBox.ItemHeight = RowHeight; } return _listBox; } } /// <summary> /// Represents the DropDownListBox accessible object. /// </summary> internal AccessibleObject? DropDownListBoxAccessibleObject => DropDownListBox.Visible ? DropDownListBox.AccessibilityObject : null; internal bool DrawValuesRightToLeft => _editTextBox is not null && _editTextBox.IsHandleCreated && ((WINDOW_EX_STYLE)PInvokeCore.GetWindowLong(_editTextBox, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE)).HasFlag(WINDOW_EX_STYLE.WS_EX_RTLREADING); internal DropDownHolder? DropDownControlHolder => _dropDownHolder; [MemberNotNullWhen(true, nameof(DropDownControlHolder))] internal bool DropDownVisible => _dropDownHolder is not null && _dropDownHolder.Visible; public bool FocusInside => ContainsFocus || DropDownVisible; internal Color GrayTextColor { get { // If changed from the default, then the set value is returned. if (_grayTextColorModified) { return _grayTextColor; } if (ForeColor.ToArgb() == SystemColors.WindowText.ToArgb()) { return SystemColors.GrayText; } // Compute the new color by halving the value of the old one. int colorRGB = ForeColor.ToArgb(); int alphaValue = (colorRGB >> 24) & 0xff; if (alphaValue != 0) { alphaValue /= 2; colorRGB &= 0xFFFFFF; colorRGB |= (int)((alphaValue << 24) & 0xFF000000); } else { colorRGB /= 2; } return Color.FromArgb(colorRGB); } set { _grayTextColor = value; _grayTextColorModified = true; } } // This dialog's width is defined by the summary message // in the top pane. We don't restrict dialog width in any way. // Use caution and check at all DPI scaling factors if adding a new message // to be displayed in the top pane. private GridErrorDialog ErrorDialog => _errorDialog ??= new GridErrorDialog(OwnerGrid); private bool HasEntries => TopLevelGridEntries is not null && TopLevelGridEntries.Count > 0; internal int LabelPaintMargin { set => _requiredLabelPaintMargin = (short)Math.Max(Math.Max(value, _requiredLabelPaintMargin), GdiPlusSpace); } /// <summary> /// Returns 'true' if the <see cref="EditTextBox" /> has a change that needs committed. /// </summary> private bool EditTextBoxNeedsCommit { get { string text; if (_editTextBox is null || !EditTextBox.Visible) { return false; } text = EditTextBox.Text; if ((string.IsNullOrEmpty(text) && string.IsNullOrEmpty(_originalTextValue)) || text.Equals(_originalTextValue)) { return false; } return true; } } public PropertyGrid OwnerGrid { get; private set; } private int RowHeight { get { if (_cachedRowHeight == -1) { _cachedRowHeight = Font.Height + 2; } return _cachedRowHeight; } } /// <summary> /// Returns a default location for showing the context menu. This /// location is the center of the active property label in the grid, and /// is used to position the context menu when the menu is invoked /// via the keyboard. /// </summary> public Point ContextMenuDefaultLocation { get { // Get the rect for the currently selected property name, find the middle. Rectangle rect = GetRectangle(_selectedRow, RowLabel); Point point = PointToScreen(new(rect.X, rect.Y)); return new Point(point.X + (rect.Width / 2), point.Y + (rect.Height / 2)); } } private ScrollBar ScrollBar { get { if (_scrollBar is null) { OwnerGrid.CheckInCreate(); _scrollBar = new VScrollBar(); _scrollBar.Scroll += OnScroll; Controls.Add(_scrollBar); } return _scrollBar; } } [DisallowNull] internal GridEntry? SelectedGridEntry { get => _selectedGridEntry; set { if (_allGridEntries is not null) { foreach (GridEntry entry in _allGridEntries) { if (entry == value) { SelectGridEntry(value, pageIn: true); return; } } } GridEntry? equivalentEntry = FindEquivalentGridEntry(new GridEntryCollection(new[] { value }, disposeItems: false)); if (equivalentEntry is not null) { SelectGridEntry(equivalentEntry, pageIn: true); return; } throw new ArgumentException(SR.PropertyGridInvalidGridEntry); } } /// <summary> /// Returns or sets the <see cref="IServiceProvider"/> the <see cref="PropertyGridView"/> will use to obtain /// services. This may be null. /// </summary> public IServiceProvider? ServiceProvider { get => _serviceProvider; set { if (value != _serviceProvider) { _serviceProvider = value; _topHelpService = null; if (_helpService is IDisposable disposable) { disposable.Dispose(); } _helpService = null; } } } /// <summary> /// Indicates whether or not the control supports UIA Providers via /// IRawElementProviderFragment/IRawElementProviderFragmentRoot interfaces. /// </summary> internal override bool SupportsUiaProviders => true; private int TipColumn { get => (_tipInfo & unchecked((int)0xFFFF0000)) >> 16; set { // Clear the column _tipInfo &= 0xFFFF; // Set the row _tipInfo |= (value & 0xFFFF) << 16; } } private int TipRow { get => _tipInfo & 0xFFFF; set { // Clear the row _tipInfo &= unchecked((int)0xFFFF0000); // Set the row _tipInfo |= value & 0xFFFF; } } private GridToolTip ToolTip { get { if (_toolTip is null) { OwnerGrid.CheckInCreate(); _toolTip = new GridToolTip([this, EditTextBox]) { ToolTip = string.Empty, Font = Font }; } return _toolTip; } } /// <summary> /// Gets the top level grid entries. /// </summary> internal GridEntryCollection? TopLevelGridEntries { get; private set; } internal GridEntryCollection? AccessibilityGetGridEntries() => GetAllGridEntries(); internal Rectangle AccessibilityGetGridEntryBounds(GridEntry gridEntry) { int row = GetRowFromGridEntry(gridEntry); if (row < 0) { return Rectangle.Empty; } Rectangle rect = GetRectangle(row, RowValue | RowLabel); // Translate rect to screen coordinates Point pt = new(rect.X, rect.Y); PInvoke.ClientToScreen(this, ref pt); Rectangle parent = gridEntry.OwnerGrid.GridViewAccessibleObject.Bounds; int propertyGridViewBottom = parent.Bottom - 1; // - 1 is PropertyGridView bottom border if (pt.Y > propertyGridViewBottom) { return Rectangle.Empty; } if (pt.Y + rect.Height > propertyGridViewBottom) { rect.Height = propertyGridViewBottom - pt.Y; } return new Rectangle(pt.X, pt.Y, rect.Width, rect.Height); } internal int AccessibilityGetGridEntryChildID(GridEntry gridEntry) { GridEntryCollection? entries = GetAllGridEntries(); if (entries is null) { return -1; } // Find the grid entry and return its ID for (int index = 0; index < entries.Count; ++index) { if (entries[index].Equals(gridEntry)) { return index; } } return -1; } internal void AccessibilitySelect(GridEntry entry) { SelectGridEntry(entry, pageIn: true); Focus(); } private void AddGridEntryEvents(GridEntryCollection entries, int startIndex, int count) { if (entries is null) { return; } if (count == -1) { count = entries.Count - startIndex; } for (int i = startIndex; i < (startIndex + count); i++) { GridEntry entry = entries[i]; entry.AddOnValueClick(_valueClick); entry.AddOnLabelClick(_labelClick); entry.AddOnOutlineClick(_outlineClick); entry.AddOnOutlineDoubleClick(_outlineClick); entry.AddOnValueDoubleClick(_valueDoubleClick); entry.AddOnLabelDoubleClick(_labelDoubleClick); entry.AddOnRecreateChildren(_recreateChildren); } } private static void AdjustOrigin(Graphics g, Point newOrigin, ref Rectangle r) { g.ResetTransform(); g.TranslateTransform(newOrigin.X, newOrigin.Y); r.Offset(-newOrigin.X, -newOrigin.Y); } private void CancelSplitterMove() { if (_flags.HasFlag(Flags.IsSplitterMove)) { SetFlag(Flags.IsSplitterMove, false); Capture = false; if (_selectedRow != -1) { SelectRow(_selectedRow); } } } internal GridPositionData CaptureGridPositionData() => new(this); private void ClearGridEntryEvents(GridEntryCollection? entries, int startIndex, int count) { if (entries is null) { return; } if (count == -1) { count = entries.Count - startIndex; } for (int i = startIndex; i < (startIndex + count); i++) { GridEntry entry = entries[i]; entry.RemoveOnValueClick(_valueClick); entry.RemoveOnLabelClick(_labelClick); entry.RemoveOnOutlineClick(_outlineClick); entry.RemoveOnOutlineDoubleClick(_outlineClick); entry.RemoveOnValueDoubleClick(_valueDoubleClick); entry.RemoveOnLabelDoubleClick(_labelDoubleClick); entry.RemoveOnRecreateChildren(_recreateChildren); } } public void ClearGridEntries() { if (!HasEntries) { return; } CommonEditorHide(); TopLevelGridEntries = null; ClearGridEntryEvents(_allGridEntries, 0, -1); _allGridEntries = null; _selectedRow = -1; // Don't clear selectedGridEntry because then we can't save where we were on a Refresh() _tipInfo = -1; } /// <inheritdoc /> public void CloseDropDown() => CloseDropDownInternal(resetFocus: true); private void CloseDropDownInternal(bool resetFocus) { // The activation code in the DropDownHolder can cause this to recurse. if (_flags.HasFlag(Flags.DropDownClosing) || _dropDownHolder is null || !_dropDownHolder.Visible) { return; } try { SetFlag(Flags.DropDownClosing, true); if (_dropDownHolder.Component == DropDownListBox && _flags.HasFlag(Flags.DropDownCommit)) { OnListClick(sender: null, e: null); } EditTextBox.Filter = false; // Disable the drop down holder so it won't steal the focus back. _dropDownHolder.SetDropDownControl(control: null, resizable: false); _dropDownHolder.Visible = false; // When we disable the dropdown holder focus will be lost, so put it onto one of our children. if (resetFocus) { if (DialogButton.Visible) { DialogButton.Focus(); } else if (DropDownButton.Visible) { DropDownButton.Focus(); } else if (EditTextBox.Visible) { EditTextBox.Focus(); } else { Focus(); } if (_selectedRow != -1) { SelectRow(_selectedRow); } } if (_selectedRow != -1) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is not null && IsAccessibilityObjectCreated) { gridEntry.AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId); gridEntry.AccessibilityObject.RaiseAutomationPropertyChangedEvent( UIA_PROPERTY_ID.UIA_ExpandCollapseExpandCollapseStatePropertyId, (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Expanded, (VARIANT)(int)ExpandCollapseState.ExpandCollapseState_Collapsed); } } } finally { SetFlag(Flags.DropDownClosing, false); } } private void CommonEditorHide(bool always = false) { if (!always && !HasEntries) { return; } CloseDropDown(); bool gotFocus = false; if (EditTextBox.Focused || DialogButton.Focused || DropDownButton.Focused) { if (IsHandleCreated && Visible && Enabled) { gotFocus = !PInvoke.SetFocus(this).IsNull; } } try { // We do this because the Focus call above doesn't always stick, so // we make the EditTextBox think that it doesn't have focus. This prevents // ActiveControl code on the container control from moving focus elsewhere // when the dropdown closes. EditTextBox.HideFocusState = true; if (EditTextBox.Focused && !gotFocus) { gotFocus = Focus(); } EditTextBox.Visible = false; EditTextBox.SelectionStart = 0; EditTextBox.SelectionLength = 0; if (DialogButton.Focused && !gotFocus) { gotFocus = Focus(); } DialogButton.Visible = false; if (DropDownButton.Focused && !gotFocus) { gotFocus = Focus(); } DropDownButton.Visible = false; _currentEditor = null; } finally { EditTextBox.HideFocusState = false; } } private void CommonEditorSetup(Control control) { control.Visible = false; Controls.Add(control); } private void CommonEditorUse(Control control, Rectangle targetRectangle) { Debug.Assert(control is not null, "Null control passed to CommonEditorUse"); Rectangle rectCur = control.Bounds; // The client rect minus the border line. Rectangle clientRect = ClientRectangle; clientRect.Inflate(-1, -1); try { targetRectangle = Rectangle.Intersect(clientRect, targetRectangle); if (!targetRectangle.IsEmpty) { if (!targetRectangle.Equals(rectCur)) { control.SetBounds(targetRectangle.X, targetRectangle.Y, targetRectangle.Width, targetRectangle.Height); } control.Visible = true; } } catch { targetRectangle = Rectangle.Empty; } if (targetRectangle.IsEmpty) { control.Visible = false; } _currentEditor = control; } private static int CountPropertiesFromOutline(GridEntryCollection? entries) { if (entries is null) { return 0; } int propertyCount = entries.Count; for (int i = 0; i < entries.Count; i++) { if (entries[i].InternalExpanded) { propertyCount += CountPropertiesFromOutline(entries[i].Children); } } return propertyCount; } protected override AccessibleObject CreateAccessibilityInstance() => new PropertyGridViewAccessibleObject(this, OwnerGrid); private Bitmap CreateResizedBitmap(string icon, int width, int height) { Size size = new(width, height); size = ScaleHelper.ScaleToDpi(size, ScaleHelper.IsThreadPerMonitorV2Aware ? DeviceDpi : ScaleHelper.InitialSystemDpi); try { return ScaleHelper.GetIconResourceAsBitmap( typeof(PropertyGrid), icon, size); } catch (Exception e) { Debug.Fail(e.ToString()); return new Bitmap(size.Width, size.Height); } } private void CreateUI() => UpdateUIBasedOnFont(layoutRequired: false); protected override void Dispose(bool disposing) { if (disposing) { _scrollBar?.Dispose(); _listBox?.Dispose(); _dropDownHolder?.Dispose(); _scrollBar = null; _listBox = null; _dropDownHolder = null; OwnerGrid = null!; TopLevelGridEntries = null; _allGridEntries = null; _serviceProvider = null; _topHelpService = null; if (_helpService is not null and IDisposable disposable) { disposable.Dispose(); } _helpService = null; _editTextBox?.Dispose(); _editTextBox = null; _boldFont?.Dispose(); _boldFont = null; _dropDownButton?.Dispose(); _dropDownButton = null; _dialogButton?.Dispose(); _dialogButton = null; _toolTip?.Dispose(); _toolTip = null; _selectedGridEntry?.Dispose(); _selectedGridEntry = null; } base.Dispose(disposing); } public void DoCopyCommand() { if (CanCopy) { if (EditTextBox.Focused) { EditTextBox.Copy(); } else if (_selectedGridEntry is not null) { Clipboard.SetDataObject(_selectedGridEntry.GetPropertyTextValue()); } } } public void DoCutCommand() { if (CanCut) { DoCopyCommand(); if (EditTextBox.Visible) { EditTextBox.Cut(); } } } public void DoPasteCommand() { if (!CanPaste || !EditTextBox.Visible) { return; } if (EditTextBox.Focused) { EditTextBox.Paste(); } else { IDataObject? dataObj = Clipboard.GetDataObject(); if (dataObj is not null) { string? data = (string?)dataObj.GetData(typeof(string)); if (data is not null) { EditTextBox.Focus(); EditTextBox.Text = data; SetCommitError(ErrorState.None, capture: true); } } } } public void DoUndoCommand() { if (CanUndo && EditTextBox.Visible) { PInvokeCore.SendMessage(EditTextBox, PInvokeCore.WM_UNDO); } } private static int GetEntryLabelIndent(GridEntry gridEntry) => gridEntry.PropertyLabelIndent + 1; private int GetEntryLabelLength(Graphics g, GridEntry gridEntry) { SizeF sizeF = PropertyGrid.MeasureTextHelper.MeasureText(OwnerGrid, g, gridEntry.PropertyLabel, Font); var size = Size.Ceiling(sizeF); return _location.X + GetEntryLabelIndent(gridEntry) + size.Width; } private bool IsEntryLabelLong(Graphics g, GridEntry gridEntry) { if (gridEntry is null) { return false; } int length = GetEntryLabelLength(g, gridEntry); return length > _location.X + LabelWidth; } private void DrawLabel( Graphics g, int row, Rectangle rect, bool selected, bool longLabelrequest, Rectangle clipRect) { GridEntry? gridEntry = GetGridEntryFromRow(row); if (gridEntry is null || rect.IsEmpty) { return; } Point newOrigin = new(rect.X, rect.Y); clipRect = Rectangle.Intersect(rect, clipRect); if (clipRect.IsEmpty) { return; } AdjustOrigin(g, newOrigin, ref rect); clipRect.Offset(-newOrigin.X, -newOrigin.Y); try { try { bool fLongLabel = false; int labelEnd = 0; int labelIndent = GetEntryLabelIndent(gridEntry); if (longLabelrequest) { labelEnd = GetEntryLabelLength(g, gridEntry); fLongLabel = IsEntryLabelLong(g, gridEntry); } gridEntry.PaintLabel( g, rect, clipRect, selected, fLongLabel); } catch (Exception ex) { Debug.Fail(ex.ToString()); } } finally { ResetOrigin(g); } } /// <summary> /// Draw the value for the given row. /// </summary> private void DrawValue(Graphics g, int row, Rectangle clipRect) { GridEntry? gridEntry = GetGridEntryFromRow(row); if (gridEntry is null) { return; } Rectangle rect = GetRectangle(row, RowValue); Point newOrigin = new(rect.X, rect.Y); clipRect = Rectangle.Intersect(clipRect, rect); if (clipRect.IsEmpty) { return; } AdjustOrigin(g, newOrigin, ref rect); clipRect.Offset(-newOrigin.X, -newOrigin.Y); try { try { gridEntry.PaintValue( g, rect, clipRect, GridEntry.PaintValueFlags.PaintInPlace | GridEntry.PaintValueFlags.CheckShouldSerialize); } catch (Exception ex) { Debug.Fail(ex.ToString()); } } finally { ResetOrigin(g); } } /// <summary> /// Handle selection and/or editor invocation when F4 is pressed. If the current row has a modal dialog ("...") /// <paramref name="popupModalDialog"/> will cause it to be invoked. If not set to true, the ("...") button /// will just be focused. Drop-down editors will always be launched. /// </summary> private void F4Selection(bool popupModalDialog) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return; } // If we are in an errorState, just put the focus back on the Edit. if (_errorState != ErrorState.None && EditTextBox.Visible) { EditTextBox.Focus(); return; } if (DropDownButton.Visible) { PopupEditor(_selectedRow); } else if (DialogButton.Visible) { if (popupModalDialog) { PopupEditor(_selectedRow); } else { DialogButton.Focus(); } } else if (EditTextBox.Visible) { // No edit buttons, just focus and select the text value. EditTextBox.Focus(); EditTextBox.SelectAll(); } return; } public void DoubleClickRow(int row, bool toggleExpand, int type) { GridEntry? gridEntry = GetGridEntryFromRow(row); if (gridEntry is null) { return; } if (!toggleExpand || type == RowValue) { try { bool action = gridEntry.DoubleClickPropertyValue(); if (action) { SelectRow(row); return; } } catch (Exception ex) { SetCommitError(ErrorState.Thrown); ShowInvalidMessage(ex); return; } } SelectGridEntry(gridEntry, pageIn: true); if (type == RowLabel && toggleExpand && gridEntry.Expandable) { SetExpand(gridEntry, !gridEntry.InternalExpanded); return; } if (gridEntry.IsValueEditable && gridEntry.Enumerable) { int index = GetCurrentValueIndex(gridEntry); if (index != -1) { object[] values = gridEntry.GetPropertyValueList(); // If values.Length is 0, we can't cycle through anything. if (values.Length == 0) { return; } if (index >= (values.Length - 1)) { index = 0; } else { index++; } CommitValue(values[index]); SelectRow(_selectedRow); Refresh(); return; } } if (EditTextBox.Visible) { EditTextBox.Focus(); EditTextBox.SelectAll(); return; } } public Font GetBaseFont() => Font; public Font GetBoldFont() => _boldFont ??= new Font(Font, FontStyle.Bold); public Color LineColor => OwnerGrid.LineColor; public Color SelectedItemWithFocusForeColor => OwnerGrid.SelectedItemWithFocusForeColor; public Color SelectedItemWithFocusBackColor => OwnerGrid.SelectedItemWithFocusBackColor; public int LabelWidth { get { if (_flags.HasFlag(Flags.NeedUpdateUIBasedOnFont)) { UpdateUIBasedOnFont(true); } if (_labelWidth == -1) { SetConstants(); } return _labelWidth; } } internal bool IsExplorerTreeSupported => OwnerGrid.CanShowVisualStyleGlyphs && VisualStyleRenderer.IsSupported; public int OutlineIconSize => IsExplorerTreeSupported ? _outlineSizeExplorerTreeStyle : _outlineSize; internal bool IsEditTextBoxCreated => _editTextBox is not null && _editTextBox.IsHandleCreated; public int GridEntryHeight => RowHeight; internal int GetPropertyLocation(string? propertyName, bool getXY, bool rowValue) { if (_allGridEntries is null || _allGridEntries.Count <= 0) { return -1; } for (int i = 0; i < _allGridEntries.Count; i++) { GridEntry entry = _allGridEntries[i]; if (!string.Equals(propertyName, entry.PropertyLabel, StringComparison.InvariantCultureIgnoreCase)) { continue; } if (getXY) { int row = GetRowFromGridEntry(entry); if (row < 0 || row >= _visibleRows) { return -1; } else { Rectangle r = GetRectangle(row, rowValue ? RowValue : RowLabel); return PARAM.ToInt(r.X, r.Y); } } else { return i; } } return -1; } public new object? GetService(Type classService) { if (classService == typeof(IWindowsFormsEditorService)) { return this; } if (_serviceProvider is not null) { return _serviceProvider.GetService(classService); } return null; } public static int SplitterWidth => 1; /// <summary> /// The width of the label, splitter, and value. /// </summary> public int TotalWidth => LabelWidth + SplitterWidth + ValueWidth; public int ValuePaintIndent => _paintIndent; public int ValuePaintWidth => _paintWidth; public static int ValueStringIndent => EditIndent; public int ValueWidth => (int)(LabelWidth * (_labelRatio - 1)); public void DropDownControl(Control control) { if (control is null) { return; } _dropDownHolder ??= new(this); _dropDownHolder.Visible = false; _dropDownHolder.SetDropDownControl(control, _flags.HasFlag(Flags.ResizableDropDown)); Rectangle rect = GetRectangle(_selectedRow, RowValue); Size size = _dropDownHolder.Size; Point location = PointToScreen(new Point(0, 0)); Rectangle rectScreen = Screen.FromControl(EditTextBox).WorkingArea; size.Width = Math.Max(rect.Width + 1, size.Width); location.X = Math.Min( rectScreen.X + rectScreen.Width - size.Width, Math.Max(rectScreen.X, location.X + rect.X + rect.Width - size.Width)); location.Y += rect.Y; if (rectScreen.Y + rectScreen.Height < (size.Height + location.Y + EditTextBox.Height)) { location.Y -= size.Height; _dropDownHolder.ResizeUp = true; } else { location.Y += rect.Height + 1; _dropDownHolder.ResizeUp = false; } var gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is not null && IsAccessibilityObjectCreated) { gridEntry.AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId); gridEntry.AccessibilityObject.InternalRaiseAutomationNotification( Automation.AutomationNotificationKind.Other, Automation.AutomationNotificationProcessing.ImportantMostRecent, SR.ExpandedStateName); } // Control is a top level window. Standard way of setting parent on the control is prohibited for top-level controls. // It is unknown why this control was created as a top-level control. Windows does not recommend this way of setting parent. // We are not touching this for this release. We may revisit it in next release. PInvokeCore.SetWindowLong(_dropDownHolder, WINDOW_LONG_PTR_INDEX.GWL_HWNDPARENT, this); _dropDownHolder.SetBounds(location.X, location.Y, size.Width, size.Height); PInvoke.ShowWindow(_dropDownHolder, SHOW_WINDOW_CMD.SW_SHOWNA); EditTextBox.Filter = true; _dropDownHolder.Visible = true; _dropDownHolder.FocusComponent(); EditTextBox.SelectAll(); try { DropDownButton.IgnoreMouse = true; _dropDownHolder.DoModalLoop(); } finally { DropDownButton.IgnoreMouse = false; } if (_selectedRow != -1) { Focus(); SelectRow(_selectedRow); } } public void DropDownUpdate() { if (_dropDownHolder is not null && _dropDownHolder.GetUsed()) { int row = _selectedRow; GridEntry? gridEntry = GetGridEntryFromRow(row); EditTextBox.Text = gridEntry?.GetPropertyTextValue(); } } public bool EnsurePendingChangesCommitted() { CloseDropDown(); return CommitEditTextBox(); } private bool FilterEditWndProc(ref Message m) { // If it's the TAB key, we keep it since we'll give them focus with it. if (_dropDownHolder?.Visible == true && m.MsgInternal == PInvokeCore.WM_KEYDOWN && (Keys)(nint)m.WParamInternal != Keys.Tab) { Control? control = _dropDownHolder.Component; if (control is not null) { m.ResultInternal = PInvokeCore.SendMessage(control, m.MsgInternal, m.WParamInternal, m.LParamInternal); return true; } } return false; } private bool FilterReadOnlyEditKeyPress(char keyChar) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is not null && gridEntry.Enumerable && gridEntry.IsValueEditable) { int index = GetCurrentValueIndex(gridEntry); object[] values = gridEntry.GetPropertyValueList(); string letter = new([keyChar]); for (int i = 0; i < values.Length; i++) { object currentValue = values[(i + index + 1) % values.Length]; string text = gridEntry.GetPropertyTextValue(currentValue); if (text is not null && text.Length > 0 && string.Equals(text[..1], letter, StringComparison.InvariantCultureIgnoreCase)) { CommitValue(currentValue); if (EditTextBox.Focused) { EditTextBox.SelectAll(); } return true; } } } return false; } public bool WillFilterKeyPress(char charPressed) { if (!EditTextBox.Visible) { return false; } Keys modifiers = ModifierKeys; if ((modifiers & ~Keys.Shift) != 0) { return false; } // Try to activate the Edit. // We don't activate for +, -, or * on expandable items because they have special meaning for the tree. if (_selectedGridEntry is not null) { switch (charPressed) { case '+': case '-': case '*': return !_selectedGridEntry.Expandable; case unchecked((char)(int)(long)Keys.Tab): return false; } } return true; } public void FilterKeyPress(char keyChar) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return; } EditTextBox.FilterKeyPress(keyChar); } private GridEntry? FindEquivalentGridEntry(GridEntryCollection? gridEntries) { if (gridEntries is null || gridEntries.Count == 0) { return null; } GridEntryCollection? allGridEntries = GetAllGridEntries(); if (allGridEntries is null || allGridEntries.Count == 0) { return null; } GridEntry? targetEntry = null; int row = 0; int count = allGridEntries.Count; for (int i = 0; i < gridEntries.Count; i++) { if (gridEntries[i] is null) { continue; } // If we've got one above, and it's expandable, expand it. if (targetEntry is not null) { // Expand and get the new count. if (!targetEntry.InternalExpanded) { SetExpand(targetEntry, true); allGridEntries = GetAllGridEntries(); } count = targetEntry.VisibleChildCount; } int start = row; targetEntry = null; // Now, we will only go as many as were expanded. for (; row < allGridEntries!.Count && ((row - start) <= count); row++) { if (gridEntries[i].EqualsIgnoreParent(allGridEntries[row])) { targetEntry = allGridEntries[row]; row++; break; } } // Didn't find it. if (targetEntry is null) { break; } } return targetEntry; } private Point FindPosition(int x, int y) { if (RowHeight == -1) { return InvalidPosition; } Size size = GetOurSize(); if (x < 0 || x > size.Width + _location.X) { return InvalidPosition; } Point pt = new(RowLabel, 0); if (x > LabelWidth + _location.X) { pt.X = RowValue; } pt.Y = (y - _location.Y) / (1 + RowHeight); return pt; } private GridEntryCollection? GetAllGridEntries(bool updateCache = false) { if (_visibleRows == -1 || TotalProperties == -1 || !HasEntries) { return null; } if (_allGridEntries is not null && !updateCache) { return _allGridEntries; } var newEntries = new GridEntry[TotalProperties]; try { GetGridEntriesFromOutline(TopLevelGridEntries, 0, 0, newEntries); } catch (Exception ex) { Debug.Fail(ex.ToString()); } _allGridEntries = new GridEntryCollection(newEntries, disposeItems: false); AddGridEntryEvents(_allGridEntries, 0, -1); return _allGridEntries; } private static int GetCurrentValueIndex(GridEntry gridEntry) { if (!gridEntry.Enumerable) { return -1; } try { object[] values = gridEntry.GetPropertyValueList(); object? value = gridEntry.PropertyValue; string? textValue = gridEntry.TypeConverter.ConvertToString(gridEntry, value); if (values.Length == 0) { return -1; } string? itemTextValue; int stringMatch = -1; int equalsMatch = -1; for (int i = 0; i < values.Length; i++) { object currentValue = values[i]; // Check real values against string values. itemTextValue = gridEntry.TypeConverter.ConvertToString(currentValue); if (value == currentValue || string.Compare(textValue, itemTextValue, true, CultureInfo.InvariantCulture) == 0) { stringMatch = i; } // Now try .equals if they are both non-null if (value is not null && currentValue is not null && currentValue.Equals(value)) { equalsMatch = i; } if (stringMatch == equalsMatch && stringMatch != -1) { return stringMatch; } } if (stringMatch != -1) { return stringMatch; } if (equalsMatch != -1) { return equalsMatch; } return -1; } catch (Exception e) { Debug.Fail(e.ToString()); } return -1; } public static int DefaultOutlineIndent => OutlineIndent; public int GetScrollOffset() { if (_scrollBar is null) { return 0; } int pos = ScrollBar.Value; return pos; } /// <summary> /// Returns an array of entries specifying the current hierarchy of entries from the given /// <paramref name="gridEntry"/> through its parents to the root. /// </summary> [return: NotNullIfNotNull(nameof(gridEntry))] private static GridEntryCollection? GetGridEntryHierarchy(GridEntry? gridEntry) { if (gridEntry is null) { return null; } int depth = gridEntry.PropertyDepth; if (depth > 0) { var entries = new GridEntry[depth + 1]; while (gridEntry is not null && depth >= 0) { entries[depth] = gridEntry; gridEntry = gridEntry.ParentGridEntry; depth = gridEntry?.PropertyDepth ?? 0; } return new GridEntryCollection(entries, disposeItems: false); } return new GridEntryCollection(new GridEntry[] { gridEntry }, disposeItems: false); } private GridEntry? GetGridEntryFromRow(int row) => GetGridEntryFromOffset(row + GetScrollOffset()); private GridEntry? GetGridEntryFromOffset(int offset) { GridEntryCollection? allGridEntries = GetAllGridEntries(); if (allGridEntries is not null) { if (offset >= 0 && offset < allGridEntries.Count) { return allGridEntries[offset]; } } return null; } private static int GetGridEntriesFromOutline(GridEntryCollection? entries, int current, int target, GridEntry[] targetEntries) { if (entries is null || entries.Count == 0) { return current; } current--; // Want to account for each entry as we find it. for (int i = 0; i < entries.Count; i++) { current++; if (current >= target + targetEntries.Length) { break; } GridEntry currentEntry = entries[i]; if (current >= target) { targetEntries[current - target] = currentEntry; } if (currentEntry.InternalExpanded) { GridEntryCollection childEntries = currentEntry.Children; if (childEntries.Count > 0) { current = GetGridEntriesFromOutline(childEntries, current + 1, target, targetEntries); } } } return current; } private Size GetOurSize() { Size size = ClientSize; if (size.Width == 0) { Size sizeWindow = Size; if (sizeWindow.Width > 10) { Debug.Fail("We have a bad client width!"); size.Width = sizeWindow.Width; size.Height = sizeWindow.Height; } } if (!GetScrollbarHidden()) { Size sizeScroll = ScrollBar.Size; size.Width -= sizeScroll.Width; } size.Width -= 2; size.Height -= 2; return size; } public Rectangle GetRectangle(int row, int flRow) { Rectangle rect = new(0, 0, 0, 0); Size size = GetOurSize(); rect.X = _location.X; bool fLabel = (flRow & RowLabel) != 0; bool fValue = (flRow & RowValue) != 0; if (fLabel && fValue) { rect.X = 1; rect.Width = size.Width - 1; } else if (fLabel) { rect.X = 1; rect.Width = LabelWidth - 1; } else if (fValue) { rect.X = _location.X + LabelWidth; rect.Width = size.Width - LabelWidth; } rect.Y = row * (RowHeight + 1) + 1 + _location.Y; rect.Height = RowHeight; return rect; } internal int GetRowFromGridEntry(GridEntry? gridEntry) { GridEntryCollection? allGridEntries = GetAllGridEntries(); if (gridEntry is null || allGridEntries is null) { return -1; } int bestMatch = -1; for (int i = 0; i < allGridEntries.Count; i++) { // Try for an exact match. Semantics of equals are a bit loose here. if (gridEntry == allGridEntries[i]) { return i - GetScrollOffset(); } else if (bestMatch == -1 && gridEntry.Equals(allGridEntries[i])) { bestMatch = i - GetScrollOffset(); } } if (bestMatch != -1) { return bestMatch; } return -1 - GetScrollOffset(); } public bool InPropertySet { get => _flags.HasFlag(Flags.InPropertySet); private set => SetFlag(Flags.InPropertySet, value); } private bool GetScrollbarHidden() => _scrollBar is null || !ScrollBar.Visible; /// <summary> /// Returns a string containing test info about a given GridEntry. Requires an offset into the top-level /// entry collection (ie. nested entries are not accessible). Or specify -1 to get info for the current /// selected entry (which can be any entry, top-level or nested). /// </summary> public string GetTestingInfo(int entry) { GridEntry? gridEntry = (entry < 0) ? GetGridEntryFromRow(_selectedRow) : GetGridEntryFromOffset(entry); return gridEntry is null ? string.Empty : gridEntry.GetTestingInfo(); } public Color TextColor => ForeColor; private void LayoutWindow(bool invalidate) { Rectangle rect = ClientRectangle; Size sizeWindow = new(rect.Width, rect.Height); if (_scrollBar is not null) { Rectangle boundsScroll = ScrollBar.Bounds; boundsScroll.X = sizeWindow.Width - boundsScroll.Width - 1; boundsScroll.Y = 1; boundsScroll.Height = sizeWindow.Height - 2; ScrollBar.Bounds = boundsScroll; } if (invalidate) { Invalidate(); } } internal void InvalidateGridEntryValue(GridEntry ge) { int row = GetRowFromGridEntry(ge); if (row != -1) { InvalidateRows(row, row, RowValue); } } private void InvalidateRow(int row) => InvalidateRows(row, row, RowValue | RowLabel); private void InvalidateRows(int startRow, int endRow) => InvalidateRows(startRow, endRow, RowValue | RowLabel); private void InvalidateRows(int startRow, int endRow, int type) { Rectangle rect; // Invalidate from the start row down. if (endRow == -1) { rect = GetRectangle(startRow, type); rect.Height = Size.Height - rect.Y - 1; Invalidate(rect); } else { for (int i = startRow; i <= endRow; i++) { rect = GetRectangle(i, type); Invalidate(rect); } } } /// <summary> /// Overridden to handle TAB key. /// </summary> protected override bool IsInputKey(Keys keyData) { switch (keyData & Keys.KeyCode) { case Keys.Escape: case Keys.Tab: case Keys.F4: return false; case Keys.Return: if (EditTextBox.Focused) { return false; } break; } return base.IsInputKey(keyData); } private bool IsMyChild(Control? control) { if (control == this || control is null) { return false; } Control? parent = control.ParentInternal; while (parent is not null) { if (parent == this) { return true; } parent = parent.ParentInternal; } return false; } private bool IsScrollValueValid(int newValue) { // Is this move valid? if (newValue == ScrollBar.Value || newValue < 0 || newValue > ScrollBar.Maximum || (newValue + (ScrollBar.LargeChange - 1) >= TotalProperties)) { return false; } return true; } internal static bool IsSiblingControl(Control control1, Control control2) { Control? parent1 = control1.ParentInternal; Control? parent2 = control2.ParentInternal; while (parent2 is not null) { if (parent1 == parent2) { return true; } parent2 = parent2.ParentInternal; } return false; } private void MoveSplitterTo(int xPosition) { int widthPS = GetOurSize().Width; int startPS = _location.X; int pos = Math.Max(Math.Min(xPosition, widthPS - 10), OutlineIconSize * 2); int oldLabelWidth = LabelWidth; _labelRatio = widthPS / (double)(pos - startPS); SetConstants(); if (_selectedRow != -1) { // Do this to move any editor we have. SelectRow(_selectedRow); } Rectangle r = ClientRectangle; // If we're moving to the left, just invalidate the values. if (oldLabelWidth > LabelWidth) { int left = LabelWidth - _requiredLabelPaintMargin; Invalidate(new Rectangle(left, 0, Size.Width - left, Size.Height)); } else { // To the right, just invalidate from where the splitter was. r.X = oldLabelWidth - _requiredLabelPaintMargin; r.Width -= r.X; Invalidate(r); } } /// <summary> /// Shared click handler for the dialog and drop-down button. Commits any pending edits in the /// shared text box before launching the relevant editor for the currently selected row. /// </summary> private void OnButtonClick(object? sender, EventArgs e) { if (_flags.HasFlag(Flags.ButtonLaunchedEditor)) { return; } if (sender == DialogButton && !CommitEditTextBox()) { return; } SetCommitError(ErrorState.None); try { CommitEditTextBox(); SetFlag(Flags.ButtonLaunchedEditor, true); PopupEditor(_selectedRow); } finally { SetFlag(Flags.ButtonLaunchedEditor, false); } } private void OnButtonKeyDown(object? sender, KeyEventArgs ke) => OnKeyDown(sender, ke); private void OnChildLostFocus(object? sender, EventArgs e) { InvokeLostFocus(this, e); } private void OnDropDownButtonGotFocus(object? sender, EventArgs e) { if (sender is DropDownButton dropDownButton && dropDownButton.IsAccessibilityObjectCreated) { dropDownButton.AccessibilityObject.SetFocus(); } } protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); if (e is not null && !InPropertySet) { if (!CommitEditTextBox()) { EditTextBox.Focus(); return; } } if (_selectedGridEntry is not null && GetRowFromGridEntry(_selectedGridEntry) != -1) { _selectedGridEntry.HasFocus = true; SelectGridEntry(_selectedGridEntry, pageIn: false); } else { SelectRow(0); } if (_selectedGridEntry is not null && _selectedGridEntry.GetValueOwner() is not null) { UpdateHelpAttributes(oldEntry: null, _selectedGridEntry); } // For empty GridView, draw a focus-indicator rectangle, just inside GridView borders. if (TotalProperties <= 0) { int doubleOffset = 2 * _offset2Units; if ((Size.Width > doubleOffset) && (Size.Height > doubleOffset)) { using Graphics g = CreateGraphicsInternal(); DrawFocusRectangle(g, new Rectangle(_offset2Units, _offset2Units, Size.Width - doubleOffset, Size.Height - doubleOffset)); } } } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); SystemEvents.UserPreferenceChanged += OnSysColorChange; } protected override void OnHandleDestroyed(EventArgs e) { SystemEvents.UserPreferenceChanged -= OnSysColorChange; // We can leak this if we aren't disposed. if (_toolTip is not null && !RecreatingHandle) { _toolTip.Dispose(); _toolTip = null; } base.OnHandleDestroyed(e); } private void OnListChange(object? sender, EventArgs e) { if (!DropDownListBox.InSetSelectedIndex()) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); EditTextBox.Text = gridEntry?.GetPropertyTextValue(DropDownListBox.SelectedItem); EditTextBox.Focus(); EditTextBox.SelectAll(); } SetFlag(Flags.DropDownCommit, true); } private void OnListMouseUp(object? sender, MouseEventArgs me) { OnListClick(sender, me); } private void OnListClick(object? sender, EventArgs? e) { _ = GetGridEntryFromRow(_selectedRow); if (DropDownListBox.Items.Count == 0) { CommonEditorHide(); SetCommitError(ErrorState.None); SelectRow(_selectedRow); return; } else { object? value = DropDownListBox.SelectedItem; // Don't need the commit because we're committing anyway. SetFlag(Flags.DropDownCommit, false); if (value is not null && !CommitText((string)value)) { SetCommitError(ErrorState.None); SelectRow(_selectedRow); } } } private void OnListDrawItem(object? sender, DrawItemEventArgs e) { int index = e.Index; if (index < 0 || _selectedGridEntry is null) { return; } string text = (string)DropDownListBox.Items[e.Index]; e.DrawBackground(); e.DrawFocusRectangle(); Rectangle drawBounds = e.Bounds; drawBounds.Y += 1; drawBounds.X -= 1; GridEntry gridEntry = GetGridEntryFromRow(_selectedRow)!; try { gridEntry.PaintValue( e.GraphicsInternal, drawBounds, drawBounds, e.State.HasFlag(DrawItemState.Selected) ? GridEntry.PaintValueFlags.DrawSelected : default, text); } catch (FormatException ex) { ShowFormatExceptionMessage(ex); if (DropDownListBox.IsHandleCreated) { DropDownListBox.Visible = false; } } } private void OnListKeyDown(object? sender, KeyEventArgs e) { if (e.KeyCode == Keys.Return) { OnListClick(sender: null, e: null); _selectedGridEntry?.OnValueReturnKey(); } OnKeyDown(sender, e); } protected override void OnLostFocus(EventArgs e) { if (e is not null) { base.OnLostFocus(e); } if (FocusInside) { // Because the code has been like that since long time, we assume that e is not null. base.OnLostFocus(e!); return; } GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is not null) { gridEntry.HasFocus = false; CommonEditorHide(); InvalidateRow(_selectedRow); } // Because the code has been like that since long time, we assume that e is not null. base.OnLostFocus(e!); // For empty GridView, clear the focus indicator that was painted in OnGotFocus() if (TotalProperties <= 0) { Rectangle clearRect = new(1, 1, Size.Width - 2, Size.Height - 2); Color color = BackColor; if (color.HasTransparency()) { using Graphics g = CreateGraphicsInternal(); using var brush = color.GetCachedSolidBrushScope(); g.FillRectangle(brush, clearRect); } else { using GetDcScope hdc = new(HWND); using CreateBrushScope hbrush = new(color); hdc.FillRectangle(hbrush, clearRect); } } } private void OnEditChange(object? sender, EventArgs e) { SetCommitError(ErrorState.None, EditTextBox.Focused); ToolTip.ToolTip = string.Empty; ToolTip.Visible = false; } private void OnEditGotFocus(object? sender, EventArgs e) { if (!EditTextBox.Visible) { Focus(); return; } switch (_errorState) { case ErrorState.MessageBoxUp: return; case ErrorState.Thrown: if (EditTextBox.Visible) { EditTextBox.HookMouseDown = true; } break; default: if (EditTextBoxNeedsCommit) { SetCommitError(ErrorState.None, capture: true); } break; } if (_selectedGridEntry is not null && GetRowFromGridEntry(_selectedGridEntry) != -1) { _selectedGridEntry.HasFocus = true; InvalidateRow(_selectedRow); if (EditTextBox.IsAccessibilityObjectCreated) { ((ControlAccessibleObject)EditTextBox.AccessibilityObject).NotifyClients(AccessibleEvents.Focus); EditTextBox.AccessibilityObject.SetFocus(); } } else { SelectRow(0); } } private bool ProcessEnumUpAndDown(GridEntry entry, Keys keyCode, bool closeDropDown = true) { object? value = entry.PropertyValue; object[] values = entry.GetPropertyValueList(); for (int i = 0; i < values.Length; i++) { object? currentValue = values[i]; if (value is not null && currentValue is not null && value.GetType() != currentValue.GetType() && entry.TypeConverter.CanConvertTo(entry, value.GetType())) { currentValue = entry.TypeConverter.ConvertTo(entry, CultureInfo.CurrentCulture, currentValue, value.GetType()); } bool equal = (value == currentValue) || (value is not null && value.Equals(currentValue)); if (!equal && value is string @string && currentValue is not null) { equal = string.Compare(@string, currentValue.ToString(), true, CultureInfo.CurrentCulture) == 0; } if (!equal) { continue; } object valueNew; if (keyCode == Keys.Up) { if (i == 0) { return true; } valueNew = values[i - 1]; } else { if (i == values.Length - 1) { return true; } valueNew = values[i + 1]; } CommitValue(entry, valueNew, closeDropDown); EditTextBox.HookMouseDown = false; EditTextBox.SelectAll(); return true; } return false; } private void OnEditKeyDown(object? sender, KeyEventArgs e) { if (!e.Alt && (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)) { GridEntry gridEntry = GetGridEntryFromRow(_selectedRow)!; if (!gridEntry.Enumerable || !gridEntry.IsValueEditable) { return; } e.Handled = true; bool processed = ProcessEnumUpAndDown(gridEntry, e.KeyCode); if (processed) { return; } } else if ((e.KeyCode == Keys.Left || e.KeyCode == Keys.Right) && (e.Modifiers & ~Keys.Shift) != 0) { // Handle non-expand/collapse case of left & right as up & down return; } OnKeyDown(sender, e); } private void OnEditKeyPress(object? sender, KeyPressEventArgs e) { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return; } if (!gridEntry.IsTextEditable) { e.Handled = FilterReadOnlyEditKeyPress(e.KeyChar); } } private void OnEditLostFocus(object? sender, EventArgs e) { // Believe it or not this can actually happen. if (EditTextBox.Focused || (_errorState == ErrorState.MessageBoxUp) || (_errorState == ErrorState.Thrown) || InPropertySet) { return; } // Check to see if the focus is on the drop down or one of it's children. If so, return. if (_dropDownHolder is not null && _dropDownHolder.Visible) { bool found = false; for (HWND hwnd = PInvokeCore.GetForegroundWindow(); !hwnd.IsNull; hwnd = PInvoke.GetParent(hwnd)) { if (hwnd == _dropDownHolder.Handle) { found = true; } } if (found) { return; } } if (FocusInside) { return; } // If the focus isn't going to a child of the view if (!CommitEditTextBox()) { EditTextBox.Focus(); return; } // Change our focus state. InvokeLostFocus(this, EventArgs.Empty); } internal bool EditMouseDown { get => _flags.HasFlag(Flags.EditMouseDown); private set => SetFlag(Flags.EditMouseDown, value); } private void OnEditMouseDown(object? sender, MouseEventArgs e) { try { EditMouseDown = true; if (!FocusInside) { SelectGridEntry(_selectedGridEntry, pageIn: false); } if (e.Clicks % 2 == 0) { DoubleClickRow(_selectedRow, toggleExpand: false, RowValue); EditTextBox.SelectAll(); } if (_rowSelectTime == 0) { return; } // Check if the click happened within the double click time since the row was selected. // This allows the edits to be selected with two clicks instead of 3 (select row, double click). long timeStamp = DateTime.Now.Ticks; int delta = (int)((timeStamp - _rowSelectTime) / 10000); // make it milliseconds if (delta < SystemInformation.DoubleClickTime) { Point screenPoint = EditTextBox.PointToScreen(e.Location); if (Math.Abs(screenPoint.X - _rowSelectPos.X) < SystemInformation.DoubleClickSize.Width && Math.Abs(screenPoint.Y - _rowSelectPos.Y) < SystemInformation.DoubleClickSize.Height) { DoubleClickRow(_selectedRow, toggleExpand: false, RowValue); PInvokeCore.SendMessage(EditTextBox, PInvokeCore.WM_LBUTTONUP, (WPARAM)0, (LPARAM)e.Location); EditTextBox.SelectAll(); } _rowSelectPos = Point.Empty; _rowSelectTime = 0; } } finally { EditMouseDown = false; } } private bool OnEscape(Control sender) { if ((ModifierKeys & (Keys.Alt | Keys.Control)) != 0) { return false; } SetFlag(Flags.DropDownCommit, false); if (sender != EditTextBox || !EditTextBox.Focused) { if (sender != this) { CloseDropDown(); Focus(); } return false; } // If we aren't in an error state, just quit. if (_errorState == ErrorState.None) { EditTextBox.Text = _originalTextValue; Focus(); return true; } if (EditTextBoxNeedsCommit) { bool success = false; EditTextBox.Text = _originalTextValue; bool needReset = true; if (_selectedGridEntry is not null) { string currentTextValue = _selectedGridEntry.GetPropertyTextValue(); needReset = _originalTextValue != currentTextValue && !(string.IsNullOrEmpty(_originalTextValue) && string.IsNullOrEmpty(currentTextValue)); } if (needReset) { try { success = CommitText(_originalTextValue!); } catch { } } else { success = true; } if (!success) { EditTextBox.Focus(); EditTextBox.SelectAll(); return true; } } SetCommitError(ErrorState.None); Focus(); return true; } protected override void OnKeyDown(KeyEventArgs e) => OnKeyDown(this, e); private void OnKeyDown(object? sender, KeyEventArgs e) { GridEntry? entry = GetGridEntryFromRow(_selectedRow); if (entry is null) { return; } e.Handled = true; bool controlPressed = e.Control; bool shiftPressed = e.Shift; bool controlShiftPressed = controlPressed && shiftPressed; bool altPressed = e.Alt; Keys keyCode = e.KeyCode; bool fallingThrough = false; // We have to do this here because if we are hosted in a non-windows forms dialog, we never get a chance to // peek at the messages, we just get called. if (keyCode == Keys.Tab) { if (ProcessDialogKey(e.KeyData)) { e.Handled = true; return; } } // Alt-Arrow support. if (keyCode == Keys.Down && altPressed && DropDownButton.Visible) { F4Selection(popupModalDialog: false); return; } if (keyCode == Keys.Up && altPressed && DropDownButton.Visible && (_dropDownHolder is not null) && _dropDownHolder.Visible) { UnfocusSelection(); return; } if (ToolTip.Visible) { ToolTip.ToolTip = string.Empty; } if (controlShiftPressed || sender == this || sender == OwnerGrid) { switch (keyCode) { case Keys.Up: case Keys.Down: int position = keyCode == Keys.Up ? _selectedRow - 1 : _selectedRow + 1; SelectGridEntry(GetGridEntryFromRow(position), pageIn: true); SetFlag(Flags.NoDefault, false); return; case Keys.Left: if (controlPressed) { // Move the splitter 3 pixels to the left MoveSplitterTo(LabelWidth - 3); return; } if (entry.InternalExpanded) { SetExpand(entry, false); } else { // Handle non-expand/collapse case of left & right as up & down. SelectGridEntry(GetGridEntryFromRow(_selectedRow - 1), pageIn: true); } return; case Keys.Right: if (controlPressed) { // Move the splitter 3 pixels to the right. MoveSplitterTo(LabelWidth + 3); return; } if (entry.Expandable) { if (entry.InternalExpanded) { SelectGridEntry(entry.Children[0], pageIn: true); } else { SetExpand(entry, true); } } else { // Handle non-expand/collapse case of left & right as up & down. SelectGridEntry(GetGridEntryFromRow(_selectedRow + 1), pageIn: true); } return; case Keys.Return: if (entry.Expandable) { SetExpand(entry, !entry.InternalExpanded); } else { entry.OnValueReturnKey(); } return; case Keys.Home: case Keys.End: GridEntryCollection allEntries = GetAllGridEntries()!; SelectGridEntry(allEntries[keyCode == Keys.Home ? 0 : allEntries.Count - 1], pageIn: true); return; case Keys.Add: case Keys.Oemplus: case Keys.OemMinus: case Keys.Subtract: if (!entry.Expandable) { break; } SetFlag(Flags.IsSpecialKey, true); bool expand = keyCode is Keys.Add or Keys.Oemplus; SetExpand(entry, expand); Invalidate(); e.Handled = true; return; case Keys.D8: if (shiftPressed) { goto case Keys.Multiply; } break; case Keys.Multiply: SetFlag(Flags.IsSpecialKey, true); RecursivelyExpand(entry, true, true, MaxRecurseExpand); e.Handled = false; return; case Keys.Prior: // PAGE_UP case Keys.Next: // PAGE_DOWN bool next = keyCode == Keys.Next; int offset = next ? _visibleRows - 1 : 1 - _visibleRows; int row = _selectedRow; if (controlPressed && !shiftPressed) { return; } if (_selectedRow != -1) { // Actual paging. int start = GetScrollOffset(); SetScrollOffset(start + offset); SetConstants(); if (GetScrollOffset() != (start + offset)) { // We didn't make a full page. row = next ? _visibleRows - 1 : 0; } } SelectRow(row); Refresh(); return; // Copy/paste support. case Keys.Insert: if (shiftPressed && !controlPressed && !altPressed) { fallingThrough = true; goto case Keys.V; } goto case Keys.C; case Keys.C: // Copy text in current property. if (controlPressed && !altPressed && !shiftPressed) { DoCopyCommand(); return; } break; case Keys.Delete: // Cut text in current property. if (shiftPressed && !controlPressed && !altPressed) { fallingThrough = true; goto case Keys.X; } break; case Keys.X: // Cut text in current property. if (fallingThrough || (controlPressed && !altPressed && !shiftPressed)) { Clipboard.SetDataObject(entry.GetPropertyTextValue()); CommitText(""); return; } break; case Keys.V: // Paste the text. if (fallingThrough || (controlPressed && !altPressed && !shiftPressed)) { DoPasteCommand(); } break; case Keys.A: if (controlPressed && !altPressed && !shiftPressed && EditTextBox.Visible) { EditTextBox.Focus(); EditTextBox.SelectAll(); } break; } } if (e.KeyData == (Keys.C | Keys.Alt | Keys.Shift | Keys.Control)) { Clipboard.SetDataObject(entry.GetTestingInfo()); return; } if (_selectedGridEntry is not null && _selectedGridEntry.Enumerable && _dropDownHolder is not null && _dropDownHolder.Visible && (keyCode == Keys.Up || keyCode == Keys.Down)) { ProcessEnumUpAndDown(_selectedGridEntry, keyCode, closeDropDown: false); } e.Handled = false; return; } protected override void OnKeyPress(KeyPressEventArgs e) { if (WillFilterKeyPress(e.KeyChar)) { // Find next property with letter typed. FilterKeyPress(e.KeyChar); } SetFlag(Flags.IsSpecialKey, false); } protected override void OnMouseDown(MouseEventArgs e) { // Check for a splitter if (e.Button == MouseButtons.Left && SplitterInside(e.X) && TotalProperties != 0) { if (!CommitEditTextBox()) { return; } if (e.Clicks == 2) { MoveSplitterTo(Width / 2); return; } UnfocusSelection(); SetFlag(Flags.IsSplitterMove, true); _tipInfo = -1; Capture = true; return; } // Are we on a property? Point pos = FindPosition(e.X, e.Y); if (pos == InvalidPosition) { return; } // Notify that prop entry of the click, but normalize it's coords first. We really just need the x, y. GridEntry? gridEntry = GetGridEntryFromRow(pos.Y); if (gridEntry is not null) { // Get the origin of this pe. Rectangle r = GetRectangle(pos.Y, RowLabel); _lastMouseDown = new Point(e.X, e.Y); // Offset the mouse points & notify the prop entry. if (e.Button == MouseButtons.Left) { gridEntry.OnMouseClick(e.X - r.X, e.Y - r.Y, e.Clicks, e.Button); } else { SelectGridEntry(gridEntry, pageIn: false); } _lastMouseDown = InvalidPosition; gridEntry.HasFocus = true; SetFlag(Flags.NoDefault, false); } } protected override void OnMouseLeave(EventArgs e) { if (!_flags.HasFlag(Flags.IsSplitterMove)) { Cursor = Cursors.Default; } base.OnMouseLeave(e); } protected override void OnMouseMove(MouseEventArgs e) { int rowMoveCurrent; Point point; bool onLabel = false; if (e is null) { rowMoveCurrent = -1; point = InvalidPosition; } else { point = FindPosition(e.X, e.Y); if (point == InvalidPosition || (point.X != RowLabel && point.X != RowValue)) { rowMoveCurrent = -1; ToolTip.ToolTip = string.Empty; } else { rowMoveCurrent = point.Y; onLabel = point.X == RowLabel; } } if (point == InvalidPosition || e is null) { return; } if (_flags.HasFlag(Flags.IsSplitterMove)) { MoveSplitterTo(e.X); } if ((rowMoveCurrent != TipRow || point.X != TipColumn) && !_flags.HasFlag(Flags.IsSplitterMove)) { GridEntry? gridItem = GetGridEntryFromRow(rowMoveCurrent); string? tip = string.Empty; _tipInfo = -1; if (gridItem is not null) { Rectangle itemRect = GetRectangle(point.Y, point.X); if (onLabel && gridItem.GetLabelToolTipLocation(e.X - itemRect.X, e.Y - itemRect.Y) != InvalidPoint) { tip = gridItem.LabelToolTipText; TipRow = rowMoveCurrent; TipColumn = point.X; } else if (!onLabel && gridItem.ValueToolTipLocation != InvalidPoint && !EditTextBox.Focused) { if (!EditTextBoxNeedsCommit) { tip = gridItem.GetPropertyTextValue(); } TipRow = rowMoveCurrent; TipColumn = point.X; } } // Ensure that tooltips don't display when host application is not foreground app. // Assume that we don't want to display the tooltips if (PInvoke.IsChild(PInvokeCore.GetForegroundWindow(), this)) { // Don't show the tips if a dropdown is showing if (_dropDownHolder is null || _dropDownHolder.Component is null || rowMoveCurrent == _selectedRow) { ToolTip.ToolTip = tip; } } else { ToolTip.ToolTip = string.Empty; } } if (TotalProperties != 0 && (SplitterInside(e.X) || _flags.HasFlag(Flags.IsSplitterMove))) { Cursor = Cursors.VSplit; } else { Cursor = Cursors.Default; } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) => CancelSplitterMove(); protected override void OnMouseWheel(MouseEventArgs e) { OwnerGrid.OnGridViewMouseWheel(e); if (e is HandledMouseEventArgs handledArgs) { if (handledArgs.Handled) { return; } handledArgs.Handled = true; } if ((ModifierKeys & (Keys.Shift | Keys.Alt)) != 0 || MouseButtons != MouseButtons.None) { return; // Do not scroll when Shift or Alt key is down, or when a mouse button is down. } int wheelScrollLines = SystemInformation.MouseWheelScrollLines; if (wheelScrollLines == 0) { return; // Do not scroll when the user system setting is 0 lines per notch } Debug.Assert(_cumulativeVerticalWheelDelta > -PInvoke.WHEEL_DELTA, "cumulativeVerticalWheelDelta is too small"); Debug.Assert(_cumulativeVerticalWheelDelta < PInvoke.WHEEL_DELTA, "cumulativeVerticalWheelDelta is too big"); // Should this only work if the Edit has focus? // We use the mouse wheel to change the values in the dropdown if it's an enumerable value. if (_selectedGridEntry is not null && _selectedGridEntry.Enumerable && EditTextBox.Focused && _selectedGridEntry.IsValueEditable) { int index = GetCurrentValueIndex(_selectedGridEntry); if (index != -1) { int delta = e.Delta > 0 ? -1 : 1; object[] values = _selectedGridEntry.GetPropertyValueList(); // Prevent out-of-range access if (values.Length == 0) { return; } if (delta > 0 && index >= (values.Length - 1)) { index = 0; } else if (delta < 0 && index == 0) { index = values.Length - 1; } else { index += delta; } CommitValue(values[index]); SelectGridEntry(_selectedGridEntry, pageIn: true); EditTextBox.Focus(); return; } } int initialOffset = GetScrollOffset(); _cumulativeVerticalWheelDelta += e.Delta; float partialNotches = _cumulativeVerticalWheelDelta / (float)PInvoke.WHEEL_DELTA; int fullNotches = (int)partialNotches; if (wheelScrollLines == -1) { // Equivalent to large change scrolls if (fullNotches != 0) { Debug.Assert(_scrollBar is not null); int originalOffset = initialOffset; int large = fullNotches * _scrollBar.LargeChange; int newOffset = Math.Max(0, initialOffset - large); newOffset = Math.Min(newOffset, TotalProperties - _visibleRows + 1); initialOffset -= fullNotches * _scrollBar.LargeChange; if (Math.Abs(initialOffset - originalOffset) >= Math.Abs(fullNotches * _scrollBar.LargeChange)) { _cumulativeVerticalWheelDelta -= fullNotches * (int)PInvoke.WHEEL_DELTA; } else { _cumulativeVerticalWheelDelta = 0; } if (!ScrollRows(newOffset)) { _cumulativeVerticalWheelDelta = 0; return; } } } else { // SystemInformation.MouseWheelScrollLines doesn't work under terminal server, // it defaults to the notches per scroll. int scrollBands = (int)(wheelScrollLines * partialNotches); if (scrollBands != 0) { if (ToolTip.Visible) { ToolTip.ToolTip = string.Empty; } int newOffset = Math.Max(0, initialOffset - scrollBands); newOffset = Math.Min(newOffset, TotalProperties - _visibleRows + 1); if (scrollBands > 0) { if (_scrollBar!.Value <= _scrollBar.Minimum) { _cumulativeVerticalWheelDelta = 0; } else { _cumulativeVerticalWheelDelta -= (int)(scrollBands * (PInvoke.WHEEL_DELTA / (float)wheelScrollLines)); } } else { if (_scrollBar!.Value > (_scrollBar.Maximum - _visibleRows + 1)) { _cumulativeVerticalWheelDelta = 0; } else { _cumulativeVerticalWheelDelta -= (int)(scrollBands * (PInvoke.WHEEL_DELTA / (float)wheelScrollLines)); } } if (!ScrollRows(newOffset)) { _cumulativeVerticalWheelDelta = 0; return; } } else { _cumulativeVerticalWheelDelta = 0; } } } protected override void OnMove(EventArgs e) => CloseDropDown(); protected override void OnPaintBackground(PaintEventArgs pevent) { } protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; int yPosition = 0; int startRow = 0; int endRow = _visibleRows - 1; Rectangle clipRect = e.ClipRectangle; // Give ourselves a little breathing room to account for lines, etc., as well as the entries themselves. clipRect.Inflate(0, 2); try { Size sizeWindow = Size; // Figure out what rows we're painting. Point startPosition = FindPosition(clipRect.X, clipRect.Y); Point endPosition = FindPosition(clipRect.X, clipRect.Y + clipRect.Height); if (startPosition != InvalidPosition) { startRow = Math.Max(0, startPosition.Y); } if (endPosition != InvalidPosition) { endRow = endPosition.Y; } int visibleCount = Math.Min(TotalProperties - GetScrollOffset(), 1 + _visibleRows); SetFlag(Flags.NeedsRefresh, false); Size size = GetOurSize(); Point location = _location; if (GetGridEntryFromRow(visibleCount - 1) is null) { visibleCount--; } // If we actually have some properties, then start drawing the grid. if (TotalProperties > 0) { // Draw splitter. visibleCount = Math.Min(visibleCount, endRow + 1); using var splitterPen = OwnerGrid.LineColor.GetCachedPenScope(SplitterWidth); g.DrawLine(splitterPen, _labelWidth, location.Y, _labelWidth, visibleCount * (RowHeight + 1) + location.Y); // Draw lines. using var linePen = g.FindNearestColor(OwnerGrid.LineColor).GetCachedPenScope(); int currentRowHeight = 0; int lineEnd = location.X + size.Width; int lineStart = location.X; // Draw values. int totalWidth = TotalWidth + 1; // Draw labels. set clip rect. for (int i = startRow; i < visibleCount; i++) { try { // Draw the line. currentRowHeight = i * (RowHeight + 1) + location.Y; g.DrawLine(linePen, lineStart, currentRowHeight, lineEnd, currentRowHeight); // Draw the value. DrawValue(g, i, clipRect); // Draw the label. Rectangle rect = GetRectangle(i, RowLabel); yPosition = rect.Y + rect.Height; DrawLabel(g, i, rect, i == _selectedRow, false, clipRect); if (i == _selectedRow) { EditTextBox.Invalidate(); } } catch (Exception ex) when (!ex.IsCriticalException()) { Debug.Fail(ex.Message); } } // Draw the bottom line currentRowHeight = visibleCount * (RowHeight + 1) + location.Y; g.DrawLine(linePen, lineStart, currentRowHeight, lineEnd, currentRowHeight); } // Fill anything left. if (yPosition < Size.Height) { yPosition++; Rectangle clearRect = new(1, yPosition, Size.Width - 2, Size.Height - yPosition - 1); using var backBrush = BackColor.GetCachedSolidBrushScope(); g.FillRectangle(backBrush, clearRect); } // Draw outside border. using var borderPen = OwnerGrid.ViewBorderColor.GetCachedPenScope(); g.DrawRectangle(borderPen, 0, 0, sizeWindow.Width - 1, sizeWindow.Height - 1); _boldFont = null; } catch (Exception ex) when (!ex.IsCriticalException()) { Debug.Fail(ex.Message); } } private void OnGridEntryLabelDoubleClick(object? s, EventArgs e) { GridEntry gridEntry = (GridEntry)s!; // If we've changed since the click (probably because we moved a row into view), bail. if (gridEntry != _lastClickedEntry) { return; } int row = GetRowFromGridEntry(gridEntry); DoubleClickRow(row, gridEntry.Expandable, RowLabel); } private void OnGridEntryValueDoubleClick(object? s, EventArgs e) { GridEntry gridEntry = (GridEntry)s!; // If we've changed since the click (probably because we moved a row into view), bail. if (gridEntry != _lastClickedEntry) { return; } int row = GetRowFromGridEntry(gridEntry); DoubleClickRow(row, gridEntry.Expandable, RowValue); } private void OnGridEntryLabelClick(object? sender, EventArgs e) { _lastClickedEntry = (GridEntry?)sender; SelectGridEntry(_lastClickedEntry, pageIn: true); } private void OnGridEntryOutlineClick(object? sender, EventArgs e) { var gridEntry = (GridEntry)sender!; Debug.Assert(gridEntry.Expandable, "non-expandable IPE firing outline click"); Cursor? oldCursor = Cursor; if (!ShouldSerializeCursor()) { oldCursor = null; } Cursor = Cursors.WaitCursor; try { SetExpand(gridEntry, !gridEntry.InternalExpanded); SelectGridEntry(gridEntry, pageIn: false); } finally { Cursor = oldCursor; } } private void OnGridEntryValueClick(object? sender, EventArgs e) { _lastClickedEntry = (GridEntry?)sender; bool setSelectTime = sender != _selectedGridEntry; SelectGridEntry(_lastClickedEntry, pageIn: true); EditTextBox.Focus(); if (_lastMouseDown != InvalidPosition) { // Clear the row select time so we don't interpret this as a double click. _rowSelectTime = 0; Point editPoint = PointToScreen(_lastMouseDown); editPoint = EditTextBox.PointToClient(editPoint); PInvokeCore.SendMessage(EditTextBox, PInvokeCore.WM_LBUTTONDOWN, 0, PARAM.FromPoint(editPoint)); PInvokeCore.SendMessage(EditTextBox, PInvokeCore.WM_LBUTTONUP, (WPARAM)0, (LPARAM)editPoint); } if (setSelectTime) { _rowSelectTime = DateTime.Now.Ticks; _rowSelectPos = PointToScreen(_lastMouseDown); } else { _rowSelectTime = 0; _rowSelectPos = Point.Empty; } } protected override void OnFontChanged(EventArgs e) { _cachedRowHeight = -1; if (Disposing || ParentInternal is null || ParentInternal.Disposing) { return; } _boldFont = null; // fontBold is cached based on Font ToolTip.Font = Font; SetFlag(Flags.NeedUpdateUIBasedOnFont, true); UpdateUIBasedOnFont(true); base.OnFontChanged(e); if (_selectedGridEntry is not null) { SelectGridEntry(_selectedGridEntry, true); } } protected override void OnVisibleChanged(EventArgs e) { if (Disposing || ParentInternal is null || ParentInternal.Disposing) { return; } if (Visible && ParentInternal is not null) { SetConstants(); if (_selectedGridEntry is not null) { SelectGridEntry(_selectedGridEntry, true); } if (_toolTip is not null) { ToolTip.Font = Font; } } base.OnVisibleChanged(e); } private void OnRecreateChildren(object s, GridEntryRecreateChildrenEventArgs e) { var parent = (GridEntry)s; if (parent.Expanded) { var entries = new GridEntry[_allGridEntries!.Count]; _allGridEntries.CopyTo(entries, 0); // Find the index of the gridEntry that fired the event in our main list. int parentIndex = -1; for (int i = 0; i < entries.Length; i++) { if (entries[i] == parent) { parentIndex = i; break; } } Debug.Assert(parentIndex != -1, "parent GridEntry not found in allGridEntries"); // Clear our existing handlers. ClearGridEntryEvents(_allGridEntries, parentIndex + 1, e.OldChildCount); // Resize the array if it's changed. if (e.OldChildCount != e.NewChildCount) { int newArraySize = entries.Length + (e.NewChildCount - e.OldChildCount); var newEntries = new GridEntry[newArraySize]; // Copy the existing entries up to the parent. Array.Copy(entries, 0, newEntries, 0, parentIndex + 1); // Copy the entries after the spot we'll be putting the new ones. Array.Copy( entries, parentIndex + e.OldChildCount + 1, newEntries, parentIndex + e.NewChildCount + 1, entries.Length - (parentIndex + e.OldChildCount + 1)); entries = newEntries; } // From that point, replace the children with the new children. GridEntryCollection children = parent.Children; int childCount = children.Count; Debug.Assert(childCount == e.NewChildCount, $"parent reports {childCount} new children, event reports {e.NewChildCount}"); // Replace the changed items. for (int i = 0; i < childCount; i++) { entries[parentIndex + i + 1] = children[i]; } // Reset the array, rehook the handlers. _allGridEntries.Clear(); _allGridEntries.AddRange(entries); AddGridEntryEvents(_allGridEntries, parentIndex + 1, childCount); } if (e.OldChildCount != e.NewChildCount) { TotalProperties = CountPropertiesFromOutline(TopLevelGridEntries); SetConstants(); } Invalidate(); } protected override void OnResize(EventArgs e) { Rectangle newRect = ClientRectangle; int yDelta = _lastClientRect == Rectangle.Empty ? 0 : newRect.Height - _lastClientRect.Height; // If we are changing widths, update the selected row. if (!_lastClientRect.IsEmpty && newRect.Width > _lastClientRect.Width) { Rectangle rectInvalidate = new(_lastClientRect.Width - 1, 0, newRect.Width - _lastClientRect.Width + 1, _lastClientRect.Height); Invalidate(rectInvalidate); } if (!_lastClientRect.IsEmpty && yDelta > 0) { Rectangle rectInvalidate = new(0, _lastClientRect.Height - 1, _lastClientRect.Width, newRect.Height - _lastClientRect.Height + 1); Invalidate(rectInvalidate); } int scroll = GetScrollOffset(); SetScrollOffset(0); SetConstants(); SetScrollOffset(scroll); if (ScaleHelper.IsScalingRequirementMet) { SetFlag(Flags.NeedUpdateUIBasedOnFont, true); UpdateUIBasedOnFont(true); base.OnFontChanged(e); } CommonEditorHide(); LayoutWindow(false); bool selectionVisible = _selectedGridEntry is not null && _selectedRow >= 0 && _selectedRow <= _visibleRows; SelectGridEntry(_selectedGridEntry, selectionVisible); _lastClientRect = newRect; } private void OnScroll(object? sender, ScrollEventArgs e) { if (!CommitEditTextBox() || !IsScrollValueValid(e.NewValue)) { // Cancel the move e.NewValue = ScrollBar.Value; return; } int oldRow = -1; GridEntry? oldGridEntry = _selectedGridEntry; if (_selectedGridEntry is not null) { oldRow = GetRowFromGridEntry(oldGridEntry); } ScrollBar.Value = e.NewValue; if (oldGridEntry is not null) { // We need to zero out the selected row so we don't try to commit again since selectedRow is now bogus. _selectedRow = -1; SelectGridEntry(oldGridEntry, pageIn: ScrollBar.Value == TotalProperties); int newRow = GetRowFromGridEntry(oldGridEntry); if (oldRow != newRow) { Invalidate(); } } else { Invalidate(); } } private void OnSysColorChange(object sender, UserPreferenceChangedEventArgs e) { if (e.Category is UserPreferenceCategory.Color or UserPreferenceCategory.Accessibility) { SetFlag(Flags.NeedUpdateUIBasedOnFont, true); } } /// <summary> /// Displays the appropriate editor for the given <paramref name="row"/>. /// </summary> public unsafe void PopupEditor(int row) { GridEntry? gridEntry = GetGridEntryFromRow(row); if (gridEntry is null) { return; } if (_dropDownHolder is not null && _dropDownHolder.GetUsed()) { CloseDropDown(); return; } bool needsDropDownButton = gridEntry.NeedsDropDownButton; bool enumerable = gridEntry.Enumerable; bool needsCustomEditorButton = gridEntry.NeedsModalEditorButton; if (enumerable && !needsDropDownButton) { // Just a simple selection of possible values, fill our common listbox with the values and show it. DropDownListBox.Items.Clear(); _ = gridEntry.PropertyValue; object[] rgItems = gridEntry.GetPropertyValueList(); int maxWidth = 0; // The listbox draws with GDI, not GDI+. So we use a normal DC here. using GetDcScope hdc = new(DropDownListBox.HWND); TEXTMETRICW tm = default; int selectionIndex = -1; // This creates a copy of the given Font, and as such we need to delete it var hFont = (HFONT)Font.ToHfont(); using (ObjectScope fontScope = new(hFont)) { using SelectObjectScope fontSelection = new(hdc, hFont); selectionIndex = GetCurrentValueIndex(gridEntry); if (rgItems is not null && rgItems.Length > 0) { for (int i = 0; i < rgItems.Length; i++) { Size textSize = default; string value = gridEntry.GetPropertyTextValue(rgItems[i]); DropDownListBox.Items.Add(value); PInvoke.GetTextExtentPoint32W(hdc.HDC, value, value.Length, textSize); maxWidth = Math.Max(textSize.Width, maxWidth); } } PInvoke.GetTextMetrics(hdc, &tm); // border + padding + scrollbar maxWidth += 2 + tm.tmMaxCharWidth + SystemInformation.VerticalScrollBarWidth; } if (selectionIndex != -1) { DropDownListBox.SelectedIndex = selectionIndex; } SetFlag(Flags.DropDownCommit, false); DropDownListBox.Height = Math.Max(tm.tmHeight + 2, Math.Min(_maxListBoxHeight, DropDownListBox.PreferredHeight)); DropDownListBox.Width = Math.Max(maxWidth, GetRectangle(row, RowValue).Width); try { bool resizable = DropDownListBox.Items.Count > (DropDownListBox.Height / DropDownListBox.ItemHeight); SetFlag(Flags.ResizableDropDown, resizable); DropDownControl(DropDownListBox); } finally { SetFlag(Flags.ResizableDropDown, false); } Refresh(); return; } if (!needsCustomEditorButton && !needsDropDownButton) { return; } // The current grid entry supports editing, invoke the editor. try { InPropertySet = true; EditTextBox.DisableMouseHook = true; try { SetFlag(Flags.ResizableDropDown, gridEntry.UITypeEditor?.IsDropDownResizable ?? false); gridEntry.EditPropertyValue(this); } finally { SetFlag(Flags.ResizableDropDown, false); } } finally { InPropertySet = false; EditTextBox.DisableMouseHook = false; } Refresh(); if (FocusInside) { SelectGridEntry(gridEntry, pageIn: false); } } internal static void PositionTooltip(Control parent, GridToolTip toolTip, Rectangle itemRect) { toolTip.Visible = false; RECT rect = itemRect; PInvokeCore.SendMessage(toolTip, PInvoke.TTM_ADJUSTRECT, (WPARAM)1, ref rect); // Now offset it back to screen coords. Point location = parent.PointToScreen(new(rect.left, rect.top)); // Set the position once so it updates it's size with it's real width. toolTip.Location = location; int overhang = toolTip.Location.X + toolTip.Size.Width - SystemInformation.VirtualScreen.Width; if (overhang > 0) { location.X -= overhang; toolTip.Location = location; } // Tell the control we've repositioned it. toolTip.Visible = true; } protected override bool ProcessDialogKey(Keys keyData) { if (HasEntries) { Keys keyCode = keyData & Keys.KeyCode; switch (keyCode) { case Keys.F4: if (FocusInside) { if (ModifierKeys != 0) { return false; } F4Selection(popupModalDialog: true); return true; } break; case Keys.Tab: if (((keyData & Keys.Control) != 0) || ((keyData & Keys.Alt) != 0)) { break; } bool forward = (keyData & Keys.Shift) == 0; Control? focusedControl = FromHandle(PInvoke.GetFocus()); if (focusedControl is null || !IsMyChild(focusedControl)) { if (forward) { TabSelection(); focusedControl = FromHandle(PInvoke.GetFocus()); // Make sure the value actually took the focus if (IsMyChild(focusedControl)) { return true; } else { return base.ProcessDialogKey(keyData); } } else { break; } } else { // One of our editors has focus. if (EditTextBox.Focused) { if (forward) { if (DropDownButton.Visible) { DropDownButton.Focus(); return true; } else if (DialogButton.Visible) { DialogButton.Focus(); return true; } } else { SelectGridEntry(GetGridEntryFromRow(_selectedRow), pageIn: false); return true; } } else if (DialogButton.Focused || DropDownButton.Focused) { if (!forward && EditTextBox.Visible) { EditTextBox.Focus(); return true; } } } break; case Keys.Up: case Keys.Down: case Keys.Left: case Keys.Right: return false; case Keys.Return: if (DialogButton.Focused || DropDownButton.Focused) { OnButtonClick(DialogButton.Focused ? DialogButton : DropDownButton, EventArgs.Empty); return true; } else if (_selectedGridEntry is not null && _selectedGridEntry.Expandable) { SetExpand(_selectedGridEntry, !_selectedGridEntry.InternalExpanded); return true; } break; } } return base.ProcessDialogKey(keyData); } private void RecalculateProperties() { int propertyCount = CountPropertiesFromOutline(TopLevelGridEntries); if (TotalProperties != propertyCount) { TotalProperties = propertyCount; ClearGridEntryEvents(_allGridEntries, 0, -1); _allGridEntries = null; } } internal void RecursivelyExpand(GridEntry? gridEntry, bool initialize, bool expand, int maxExpands) { if (gridEntry is null || (expand && --maxExpands < 0)) { return; } SetExpand(gridEntry, expand); GridEntryCollection children = gridEntry.Children; for (int i = 0; i < children.Count; i++) { RecursivelyExpand(children[i], initialize: false, expand, maxExpands); } if (initialize) { GridEntry? selectedEntry = _selectedGridEntry; Refresh(); SelectGridEntry(selectedEntry, pageIn: false); Invalidate(); } } public override void Refresh() { Refresh(fullRefresh: false, startRow: -1, endRow: -1); // Resetting gridoutline rect to recalculate before repaint when viewsort property changed. // This is necessary especially when user changes sort and move to a secondary monitor with different // DPI and change view sort back to original. if (TopLevelGridEntries is not null && ScaleHelper.IsScalingRequirementMet) { int outlineRectIconSize = OutlineIconSize; foreach (GridEntry entry in TopLevelGridEntries) { if (entry.OutlineRectangle.Height != outlineRectIconSize || entry.OutlineRectangle.Width != outlineRectIconSize) { entry.ResetOutlineRectangle(); } } } // Make sure we got everything Invalidate(); } public void Refresh(bool fullRefresh) => Refresh(fullRefresh, startRow: -1, endRow: -1); private void Refresh(bool fullRefresh, int startRow, int endRow) { SetFlag(Flags.NeedsRefresh, true); GridEntry? gridEntry = null; if (IsDisposed) { return; } bool pageInGridEntry = true; if (startRow == -1) { startRow = 0; } if (fullRefresh || OwnerGrid.HavePropertyEntriesChanged()) { if (HasEntries && !InPropertySet && !CommitEditTextBox()) { OnEscape(this); } int oldLength = TotalProperties; object? oldObject = TopLevelGridEntries is null || TopLevelGridEntries.Count == 0 ? null : TopLevelGridEntries[0].GetValueOwner(); // Walk up to the main IPE and refresh it. if (fullRefresh) { OwnerGrid.RefreshProperties(true); } if (oldLength > 0 && !_flags.HasFlag(Flags.NoDefault)) { _positionData = CaptureGridPositionData(); if (!fullRefresh) { CommonEditorHide(true); } } UpdateHelpAttributes(_selectedGridEntry, newEntry: null); _selectedGridEntry = null; SetFlag(Flags.IsNewSelection, true); TopLevelGridEntries = OwnerGrid.GetCurrentEntries(); ClearGridEntryEvents(_allGridEntries, 0, -1); _allGridEntries = null; RecalculateProperties(); int newLength = TotalProperties; if (newLength > 0) { if (newLength < oldLength) { SetScrollbarLength(); SetScrollOffset(0); } SetConstants(); if (_positionData is not null) { gridEntry = _positionData.Restore(this); // Upon restoring the grid entry position, we don't want to page it in. object? newObject = TopLevelGridEntries is null || TopLevelGridEntries.Count == 0 ? null : TopLevelGridEntries[0].GetValueOwner(); pageInGridEntry = (gridEntry is null) || oldLength != newLength || newObject != oldObject; } if (gridEntry is null) { gridEntry = OwnerGrid.GetDefaultGridEntry(); SetFlag(Flags.NoDefault, gridEntry is null && TotalProperties > 0); } InvalidateRows(startRow, endRow); if (gridEntry is null) { _selectedRow = 0; _selectedGridEntry = GetGridEntryFromRow(_selectedRow); } } else if (oldLength == 0) { return; } else { SetConstants(); } // Release the old positionData which contains reference to previous selected objects. _positionData = null; _lastClickedEntry = null; } if (!HasEntries) { CommonEditorHide(_selectedRow != -1); OwnerGrid.SetStatusBox(title: null, description: null); SetScrollOffset(0); _selectedRow = -1; Invalidate(); return; } // In case we added or removed properties. OwnerGrid.ClearCachedValues(); InvalidateRows(startRow, endRow); if (gridEntry is not null) { SelectGridEntry(gridEntry, pageInGridEntry); } } public void Reset() { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return; } gridEntry.ResetPropertyValue(); SelectRow(_selectedRow); } private static void ResetOrigin(Graphics g) => g.ResetTransform(); internal void RestoreHierarchyState(List<GridEntryCollection> expandedItems) { if (expandedItems is null) { return; } foreach (GridEntryCollection gec in expandedItems) { FindEquivalentGridEntry(gec); } } internal static List<GridEntryCollection> SaveHierarchyState(GridEntryCollection? entries, List<GridEntryCollection>? expandedItems = null) { if (entries is null) { return []; } expandedItems ??= []; for (int i = 0; i < entries.Count; i++) { if (entries[i].InternalExpanded) { GridEntry entry = entries[i]; expandedItems.Add(GetGridEntryHierarchy(entry.Children[0])); SaveHierarchyState(entry.Children, expandedItems); } } return expandedItems; } // Scroll to the new offset private bool ScrollRows(int newOffset) { GridEntry? currentEntry = _selectedGridEntry; if (!IsScrollValueValid(newOffset) || !CommitEditTextBox()) { return false; } bool editVisible = EditTextBox.Visible; bool dropDownButtonVisible = DropDownButton.Visible; bool dialogButtonVisible = DialogButton.Visible; EditTextBox.Visible = false; DialogButton.Visible = false; DropDownButton.Visible = false; SetScrollOffset(newOffset); if (currentEntry is not null) { int currentRow = GetRowFromGridEntry(currentEntry); if (currentRow >= 0 && currentRow < _visibleRows - 1) { EditTextBox.Visible = editVisible; DialogButton.Visible = dialogButtonVisible; DropDownButton.Visible = dropDownButtonVisible; SelectGridEntry(currentEntry, pageIn: true); } else { CommonEditorHide(); } } else { CommonEditorHide(); } Invalidate(); return true; } internal void SelectGridEntry(GridEntry? entry, bool pageIn) { if (entry is null) { return; } int row = GetRowFromGridEntry(entry); if (row + GetScrollOffset() < 0) { return; } int maxRows = (int)Math.Ceiling(((double)GetOurSize().Height) / (1 + RowHeight)); // Determine whether or not we need to page-in this GridEntry if (!pageIn || (row >= 0 && row < (maxRows - 1))) { // No need to page-in: either fPageIn is false or the row is already in view SelectRow(row); } else { // Page-in the selected GridEntry _selectedRow = -1; // clear the selected row since it's no longer a valid number int offset = GetScrollOffset(); if (row < 0) { SetScrollOffset(row + offset); Invalidate(); SelectRow(0); } else { // Try to put it one row up from the bottom. int newOffset = row + offset - (maxRows - 2); if (newOffset >= ScrollBar.Minimum && newOffset < ScrollBar.Maximum) { SetScrollOffset(newOffset); } Invalidate(); SelectGridEntry(entry, pageIn: false); } } } private void SelectRow(int row) { if (!_flags.HasFlag(Flags.IsNewSelection)) { if (FocusInside) { // If we're in an error state, we want to bail out of this. if (_errorState != ErrorState.None || (row != _selectedRow && !CommitEditTextBox())) { return; } } else { Focus(); } } GridEntry? gridEntry = GetGridEntryFromRow(row); // Update our reset command. if (row != _selectedRow) { UpdateResetCommand(gridEntry); } if (_flags.HasFlag(Flags.IsNewSelection) && GetGridEntryFromRow(_selectedRow) is null) { CommonEditorHide(); } UpdateHelpAttributes(_selectedGridEntry, gridEntry); // Tell the old selection it's not focused any more. _selectedGridEntry?.HasFocus = false; // Selection not visible. if (row < 0 || row >= _visibleRows) { CommonEditorHide(); _selectedRow = row; _selectedGridEntry = gridEntry; Refresh(); return; } // Leave current selection. if (gridEntry is null) { return; } int oldSelectedRow = _selectedRow; if (_selectedRow != row || (_selectedGridEntry is not null && !gridEntry.Equals(_selectedGridEntry))) { CommonEditorHide(); } Rectangle rect = GetRectangle(row, RowValue); string s = gridEntry.GetPropertyTextValue(); // What components are we using? bool needsDropDownButton = gridEntry.NeedsDropDownButton | gridEntry.Enumerable; bool needsCustomEditorButton = gridEntry.NeedsModalEditorButton; bool customPaint = gridEntry.IsCustomPaint; rect.X += 1; rect.Width -= 1; // We want to allow builders on read-only properties if ((needsCustomEditorButton || needsDropDownButton) && !gridEntry.ShouldRenderReadOnly && FocusInside) { Control button = needsDropDownButton ? DropDownButton : DialogButton; Size sizeBtn = ScaleHelper.IsScalingRequirementMet ? new Size(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight) : new Size(SystemInformation.VerticalScrollBarArrowHeight, RowHeight); Rectangle rectTarget = new( rect.X + rect.Width - sizeBtn.Width, rect.Y, sizeBtn.Width, rect.Height); CommonEditorUse(button, rectTarget); sizeBtn = button.Size; rect.Width -= sizeBtn.Width; button.Invalidate(); } // If we're painting the value, size the rect between the button and the painted value if (customPaint) { rect.X += _paintIndent + 1; rect.Width -= _paintIndent + 1; } else { rect.X += EditIndent + 1; // +1 to compensate for where GDI+ draws it's string relative to the rect. rect.Width -= EditIndent + 1; } if ((_flags.HasFlag(Flags.IsNewSelection) || !EditTextBox.Focused) && s is not null && !s.Equals(EditTextBox.Text)) { EditTextBox.Text = s; _originalTextValue = s; EditTextBox.SelectionStart = 0; EditTextBox.SelectionLength = 0; } EditTextBox.AccessibleName = gridEntry.Label; if (gridEntry.ShouldSerializePropertyValue()) { EditTextBox.Font = GetBoldFont(); } else { EditTextBox.Font = Font; } if (_flags.HasFlag(Flags.IsSplitterMove) || !gridEntry.HasValue || !FocusInside) { EditTextBox.Visible = false; } else { rect.Offset(1, 1); rect.Height -= 1; rect.Width -= 1; CommonEditorUse(EditTextBox, rect); bool drawReadOnly = gridEntry.ShouldRenderReadOnly; EditTextBox.ForeColor = drawReadOnly ? GrayTextColor : ForeColor; EditTextBox.BackColor = BackColor; EditTextBox.ReadOnly = drawReadOnly || !gridEntry.IsTextEditable; EditTextBox.UseSystemPasswordChar = gridEntry.ShouldRenderPassword; } GridEntry? oldSelectedGridEntry = _selectedGridEntry; _selectedRow = row; _selectedGridEntry = gridEntry; OwnerGrid.SetStatusBox(gridEntry.PropertyLabel, gridEntry.PropertyDescription); // Tell the new focused item that it now has focus _selectedGridEntry?.HasFocus = FocusInside; if (!_flags.HasFlag(Flags.IsNewSelection) && !_flags.HasFlag(Flags.InPropertySet)) { Focus(); } InvalidateRow(oldSelectedRow); InvalidateRow(row); if (FocusInside) { SetFlag(Flags.IsNewSelection, false); } try { if (_selectedGridEntry != oldSelectedGridEntry) { OwnerGrid.OnSelectedGridItemChanged(oldSelectedGridEntry, _selectedGridEntry); } } catch { } } public void SetConstants() { Size size = GetOurSize(); _visibleRows = (int)Math.Ceiling(((double)size.Height) / (1 + RowHeight)); size = GetOurSize(); if (size.Width >= 0) { _labelRatio = Math.Max(Math.Min(_labelRatio, 9), 1.1); _labelWidth = _location.X + (int)(size.Width / _labelRatio); } int oldWidth = _labelWidth; bool adjustWidth = SetScrollbarLength(); GridEntryCollection? rgipesAll = GetAllGridEntries(); if (rgipesAll is not null) { int scroll = GetScrollOffset(); if ((scroll + _visibleRows) >= rgipesAll.Count) { _visibleRows = rgipesAll.Count - scroll; } } if (adjustWidth && size.Width >= 0) { _labelRatio = GetOurSize().Width / (double)(oldWidth - _location.X); } } private void SetCommitError(ErrorState error) { SetCommitError(error, error == ErrorState.Thrown); } private void SetCommitError(ErrorState error, bool capture) { _errorState = error; if (error != ErrorState.None) { CancelSplitterMove(); } EditTextBox.HookMouseDown = capture; } internal void SetExpand(GridEntry entry, bool value) { if (entry is null || !entry.Expandable) { return; } int row = GetRowFromGridEntry(entry); int currentRow = _selectedRow; // If the currently selected row is below us, we need to commit now or the offsets will be wrong. if (_selectedRow != -1 && row < _selectedRow && EditTextBox.Visible) { // This will cause the commit. Focus(); } int offset = GetScrollOffset(); int items = TotalProperties; entry.InternalExpanded = value; if (_selectedGridEntry is not null && IsAccessibilityObjectCreated) { var oldExpandedState = value ? ExpandCollapseState.ExpandCollapseState_Collapsed : ExpandCollapseState.ExpandCollapseState_Expanded; var newExpandedState = value ? ExpandCollapseState.ExpandCollapseState_Expanded : ExpandCollapseState.ExpandCollapseState_Collapsed; _selectedGridEntry.AccessibilityObject.RaiseAutomationPropertyChangedEvent( UIA_PROPERTY_ID.UIA_ExpandCollapseExpandCollapseStatePropertyId, (VARIANT)(int)oldExpandedState, (VARIANT)(int)newExpandedState); } RecalculateProperties(); GridEntry? selectedEntry = _selectedGridEntry; if (!value) { for (GridEntry? currentEntry = selectedEntry; currentEntry is not null; currentEntry = currentEntry.ParentGridEntry) { if (currentEntry.Equals(entry)) { selectedEntry = entry; } } } row = GetRowFromGridEntry(entry); SetConstants(); int newItems = TotalProperties - items; if (value && newItems > 0 && newItems < _visibleRows && (row + newItems) >= _visibleRows && newItems < currentRow) { // Scroll to show the newly opened items. SetScrollOffset(TotalProperties - items + offset); } Invalidate(); SelectGridEntry(selectedEntry, pageIn: false); int scroll = GetScrollOffset(); SetScrollOffset(0); SetConstants(); SetScrollOffset(scroll); } private void SetFlag(Flags flag, bool value) { if (value) { _flags |= flag; } else { _flags &= ~flag; } } public void SetScrollOffset(int offset) { int newPosition = Math.Max(0, Math.Min(TotalProperties - _visibleRows + 1, offset)); int oldPosition = ScrollBar.Value; if (newPosition != oldPosition && IsScrollValueValid(newPosition) && _visibleRows > 0) { ScrollBar.Value = newPosition; Invalidate(); _selectedRow = GetRowFromGridEntry(_selectedGridEntry); } } /// <summary> /// Commits any needed changes in the <see cref="EditTextBox" />. Returns false if there is a change that /// could not be committed. /// </summary> internal bool CommitEditTextBox() { if (_errorState == ErrorState.MessageBoxUp) { return false; } if (!EditTextBoxNeedsCommit) { SetCommitError(ErrorState.None); return true; } if (InPropertySet) { return false; } if (GetGridEntryFromRow(_selectedRow) is null) { return true; } bool success = false; try { success = CommitText(EditTextBox.Text); } finally { if (!success) { EditTextBox.Focus(); EditTextBox.SelectAll(); } else { SetCommitError(ErrorState.None); } } return success; } private bool CommitValue(object? value) { GridEntry? currentEntry = _selectedGridEntry; if (_selectedGridEntry is null && _selectedRow != -1) { currentEntry = GetGridEntryFromRow(_selectedRow); } if (currentEntry is null) { Debug.Fail("Committing with no selected row!"); return true; } return CommitValue(currentEntry, value); } internal bool CommitValue(GridEntry entry, object? value, bool closeDropDown = true) { int propCount = entry.ChildCount; bool capture = EditTextBox.HookMouseDown; object? originalValue = null; try { originalValue = entry.PropertyValue; } catch { // If the getter is failing, we still want to let the set happen. } try { try { InPropertySet = true; // If this entry is enumerable, then once a value is selected from the editor, // we'll want to close the drop down (like true/false). Otherwise, if we're // working with Anchor for ex., then we should be able to select different values // from the editor, without having it close every time. if (entry is not null && entry.Enumerable && closeDropDown) { CloseDropDown(); } try { EditTextBox.DisableMouseHook = true; entry!.PropertyValue = value; } finally { EditTextBox.DisableMouseHook = false; EditTextBox.HookMouseDown = capture; } } catch (Exception ex) { SetCommitError(ErrorState.Thrown); ShowInvalidMessage(ex); return false; } } finally { InPropertySet = false; } SetCommitError(ErrorState.None); string text = entry.GetPropertyTextValue(); if (!string.Equals(text, EditTextBox.Text)) { EditTextBox.Text = text; EditTextBox.SelectionStart = 0; EditTextBox.SelectionLength = 0; } _originalTextValue = text; // Update our reset command. UpdateResetCommand(entry); if (entry.ChildCount != propCount) { ClearGridEntryEvents(_allGridEntries, 0, -1); _allGridEntries = null; SelectGridEntry(entry, pageIn: true); } if (entry.Disposed) { bool editfocused = _editTextBox is not null && _editTextBox.Focused; // Reselect the row to find the replacement. SelectGridEntry(entry, pageIn: true); entry = _selectedGridEntry!; if (editfocused && _editTextBox is not null) { _editTextBox.Focus(); } } OwnerGrid.OnPropertyValueSet(entry, originalValue); return true; } private bool CommitText(string text) { GridEntry? currentEntry = _selectedGridEntry; if (_selectedGridEntry is null && _selectedRow != -1) { currentEntry = GetGridEntryFromRow(_selectedRow); } if (currentEntry is null) { Debug.Fail("Committing with no selected row!"); return true; } object? value; try { value = currentEntry.ConvertTextToValue(text); } catch (Exception ex) { SetCommitError(ErrorState.Thrown); ShowInvalidMessage(ex); return false; } SetCommitError(ErrorState.None); return CommitValue(value); } internal override void ReleaseUiaProvider(HWND handle) { if (_allGridEntries?.Count > 0) { foreach (GridEntry gridEntry in _allGridEntries) { gridEntry.ReleaseUiaProvider(); } } _scrollBar?.ReleaseUiaProvider(HWND.Null); _listBox?.ReleaseUiaProvider(HWND.Null); _dropDownHolder?.ReleaseUiaProvider(HWND.Null); _editTextBox?.ReleaseUiaProvider(HWND.Null); _dropDownButton?.ReleaseUiaProvider(HWND.Null); _dialogButton?.ReleaseUiaProvider(HWND.Null); base.ReleaseUiaProvider(handle); } internal void ReverseFocus() { if (_selectedGridEntry is null) { Focus(); } else { SelectGridEntry(_selectedGridEntry, pageIn: true); if (DialogButton.Visible) { DialogButton.Focus(); } else if (DropDownButton.Visible) { DropDownButton.Focus(); } else if (EditTextBox.Visible) { EditTextBox.SelectAll(); EditTextBox.Focus(); } } } private bool SetScrollbarLength() { if (TotalProperties == -1) { return false; } if (TotalProperties < _visibleRows) { SetScrollOffset(0); } else if (GetScrollOffset() > TotalProperties) { SetScrollOffset(TotalProperties + 1 - _visibleRows); } bool hidden = !ScrollBar.Visible; if (_visibleRows > 0) { ScrollBar.LargeChange = _visibleRows - 1; } bool scrollBarChanged = false; ScrollBar.Maximum = Math.Max(0, TotalProperties - 1); if (hidden != (TotalProperties < _visibleRows)) { scrollBarChanged = true; ScrollBar.Visible = hidden; Size size = GetOurSize(); if (_labelWidth != -1 && size.Width > 0) { if (_labelWidth > _location.X + size.Width) { _labelWidth = _location.X + (int)(size.Width / _labelRatio); } else { _labelRatio = GetOurSize().Width / (double)(_labelWidth - _location.X); } } Invalidate(); } return scrollBarChanged; } /// <inheritdoc /> public DialogResult ShowDialog(Form dialog) { ArgumentNullException.ThrowIfNull(dialog); // Try to shift down if sitting right on top of existing owner. if (dialog.StartPosition == FormStartPosition.CenterScreen) { Control topControl = this; if (topControl is not null) { while (topControl.ParentInternal is not null) { topControl = topControl.ParentInternal; } if (topControl.Size.Equals(dialog.Size)) { dialog.StartPosition = FormStartPosition.Manual; Point location = topControl.Location; location.Offset(25, 25); dialog.Location = location; } } } HWND priorFocus = PInvoke.GetFocus(); DialogResult result; if (TryGetService(out IUIService? uiService)) { result = uiService.ShowDialog(dialog); } else { result = dialog.ShowDialog(this); } if (!priorFocus.IsNull) { PInvoke.SetFocus(priorFocus); } return result; } private unsafe void ShowFormatExceptionMessage(Exception? ex) { // We have to uninstall our hook so the user can push the button! bool hooked = EditTextBox.HookMouseDown; EditTextBox.DisableMouseHook = true; SetCommitError(ErrorState.MessageBoxUp, false); // Before invoking the error dialog, flush all mouse messages in the message queue. // Otherwise the click that triggered the error will still be in the queue, and will get eaten by the dialog, // potentially causing an accidental button click. Problem occurs because we trap clicks using a system hook, // which usually discards the message by returning 1 to GetMessage(). But this won't occur until after the // error dialog gets closed, which is much too late. MSG mouseMessage = default; while (PInvokeCore.PeekMessage( &mouseMessage, HWND.Null, PInvokeCore.WM_MOUSEFIRST, PInvokeCore.WM_MOUSELAST, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE)) { // No-op. } if (ex is Reflection.TargetInvocationException) { ex = ex.InnerException; } // Try to find an exception message to display string? exMessage = ex?.Message; bool revert; while (exMessage is null || exMessage.Length == 0) { ex = ex?.InnerException; if (ex is null) { break; } exMessage = ex.Message; } ErrorDialog.Message = SR.PBRSFormatExceptionMessage; ErrorDialog.Text = SR.PBRSErrorTitle; ErrorDialog.Details = exMessage; if (TryGetService(out IUIService? uiService)) { revert = uiService.ShowDialog(ErrorDialog) == DialogResult.Cancel; } else { revert = ShowDialog(ErrorDialog) == DialogResult.Cancel; } EditTextBox.DisableMouseHook = false; if (hooked) { SelectGridEntry(_selectedGridEntry, pageIn: true); } SetCommitError(ErrorState.Thrown, hooked); if (revert) { OnEscape(EditTextBox); } } internal unsafe void ShowInvalidMessage(Exception? ex) { // We have to uninstall our hook so the user can push the button. bool hooked = EditTextBox.HookMouseDown; EditTextBox.DisableMouseHook = true; SetCommitError(ErrorState.MessageBoxUp, capture: false); // Before invoking the error dialog, flush all mouse messages in the message queue. // // Otherwise the click that triggered the error will still be in the queue, and will get eaten by the dialog, // potentially causing an accidental button click. Problem occurs because we trap clicks using a system hook, // which usually discards the message by returning 1 to GetMessage(). But this won't occur until after the // error dialog gets closed, which is much too late. MSG mouseMsg = default; while (PInvokeCore.PeekMessage( &mouseMsg, HWND.Null, PInvokeCore.WM_MOUSEFIRST, PInvokeCore.WM_MOUSELAST, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE)) { // No-op. } if (ex is Reflection.TargetInvocationException) { ex = ex.InnerException; } // Try to find an exception message to display. string? message = ex?.Message; bool revert; while (message is null || message.Length == 0) { ex = ex?.InnerException; if (ex is null) { break; } message = ex.Message; } ErrorDialog.Message = SR.PBRSErrorInvalidPropertyValue; ErrorDialog.Text = SR.PBRSErrorTitle; ErrorDialog.Details = message; if (TryGetService(out IUIService? uiService)) { revert = uiService.ShowDialog(ErrorDialog) == DialogResult.Cancel; } else { revert = ShowDialog(ErrorDialog) == DialogResult.Cancel; } EditTextBox.DisableMouseHook = false; if (hooked) { SelectGridEntry(_selectedGridEntry, pageIn: true); } SetCommitError(ErrorState.Thrown, capture: hooked); if (revert) { OnEscape(EditTextBox); } } private bool SplitterInside(int x) => Math.Abs(x - LabelWidth) < 4; private void TabSelection() { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return; } if (EditTextBox.Visible) { EditTextBox.Focus(); EditTextBox.SelectAll(); } else if (_dropDownHolder is not null && _dropDownHolder.Visible) { _dropDownHolder.FocusComponent(); return; } else { _currentEditor?.Focus(); } return; } internal void RemoveSelectedEntryHelpAttributes() { UpdateHelpAttributes(_selectedGridEntry, newEntry: null); } private void UpdateHelpAttributes(GridEntry? oldEntry, GridEntry? newEntry) { // Update the help context with the current property. if (_helpService is null && ServiceProvider.TryGetService(out _topHelpService!)) { _helpService = _topHelpService.CreateLocalContext(HelpContextType.ToolWindowSelection); } if (_helpService is null || oldEntry == newEntry) { return; } GridEntry? temp = oldEntry; if (oldEntry is not null && !oldEntry.Disposed) { while (temp is not null) { if (temp.HelpKeyword is not null) { _helpService.RemoveContextAttribute("Keyword", temp.HelpKeyword); } temp = temp.ParentGridEntry; } } if (newEntry is not null) { temp = newEntry; UpdateHelpAttributes(_helpService, temp, true); } } private static void UpdateHelpAttributes(IHelpService helpService, GridEntry? entry, bool addAsF1) { if (entry is null) { return; } UpdateHelpAttributes(helpService, entry.ParentGridEntry, false); string? helpKeyword = entry.HelpKeyword; if (helpKeyword is not null) { helpService.AddContextAttribute("Keyword", helpKeyword, addAsF1 ? HelpKeywordType.F1Keyword : HelpKeywordType.GeneralKeyword); } } private void UpdateUIBasedOnFont(bool layoutRequired) { if (!IsHandleCreated || !_flags.HasFlag(Flags.NeedUpdateUIBasedOnFont)) { return; } try { if (_listBox is not null) { DropDownListBox.ItemHeight = RowHeight + 2; } if (_dropDownButton is not null) { bool isScalingRequirementMet = ScaleHelper.IsScalingRequirementMet; if (isScalingRequirementMet) { _dropDownButton.Size = new(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight); } else { _dropDownButton.Size = new(SystemInformation.VerticalScrollBarArrowHeight, RowHeight); } if (_dialogButton is not null) { DialogButton.Size = DropDownButton.Size; if (isScalingRequirementMet) { _dialogButton.Image = CreateResizedBitmap("dotdotdot", DotDotDotIconWidth, DotDotDotIconHeight); } } if (isScalingRequirementMet) { _dropDownButton.Image = CreateResizedBitmap("Arrow", DownArrowIconWidth, DownArrowIconHeight); } } if (layoutRequired) { LayoutWindow(invalidate: true); } } finally { SetFlag(Flags.NeedUpdateUIBasedOnFont, false); } } private bool UnfocusSelection() { GridEntry? gridEntry = GetGridEntryFromRow(_selectedRow); if (gridEntry is null) { return true; } bool commit = CommitEditTextBox(); if (commit && FocusInside) { Focus(); } return commit; } private void UpdateResetCommand(GridEntry? gridEntry) { if (TotalProperties > 0 && TryGetService(out IMenuCommandService? menuCommandService)) { MenuCommand? reset = menuCommandService.FindCommand(PropertyGridCommands.Reset); reset?.Enabled = gridEntry is not null && gridEntry.CanResetPropertyValue(); } } internal bool WantsTab(bool forward) { if (forward) { if (Focused) { // We want a tab if the grid has focus and we have a button or an Edit. if (DropDownButton.Visible || DialogButton.Visible || EditTextBox.Visible) { return true; } } else if (EditTextBox.Focused && (DropDownButton.Visible || DialogButton.Visible)) { // If the Edit has focus, and we have a button, we want the tab as well. return true; } return OwnerGrid.WantsTab(forward); } else { if (EditTextBox.Focused || DropDownButton.Focused || DialogButton.Focused) { return true; } return OwnerGrid.WantsTab(forward); } } private unsafe bool WmNotify(ref Message m) { if (m.LParamInternal == 0) { return false; } var nmhdr = (NMHDR*)(nint)m.LParamInternal; if (nmhdr->hwndFrom == ToolTip.Handle) { switch (nmhdr->code) { case PInvoke.TTN_POP: break; case PInvoke.TTN_SHOW: // We want to move the tooltip over where our text would be. Point mouseLoc = Cursor.Position; mouseLoc = PointToClient(mouseLoc); mouseLoc = FindPosition(mouseLoc.X, mouseLoc.Y); if (mouseLoc == InvalidPosition) { break; } GridEntry? curEntry = GetGridEntryFromRow(mouseLoc.Y); if (curEntry is null) { break; } // Get the proper rectangle. Rectangle itemRect = GetRectangle(mouseLoc.Y, mouseLoc.X); Point tipPt = Point.Empty; // If we need a tooltip, move the tooltip control to that point. if (mouseLoc.X == RowLabel) { tipPt = curEntry.GetLabelToolTipLocation(mouseLoc.X - itemRect.X, mouseLoc.Y - itemRect.Y); } else if (mouseLoc.X == RowValue) { tipPt = curEntry.ValueToolTipLocation; } else { break; } if (tipPt != InvalidPoint) { itemRect.Offset(tipPt); PositionTooltip(this, ToolTip, itemRect); m.ResultInternal = (LRESULT)1; return true; } break; } } return false; } protected override void WndProc(ref Message m) { switch (m.Msg) { case (int)PInvokeCore.WM_SYSCOLORCHANGE: Invalidate(); break; // If we get focus in the error state, make sure we push it back to the // Edit or bad bad things can happen with our state. case (int)PInvokeCore.WM_SETFOCUS: if (!InPropertySet && EditTextBox.Visible && (_errorState != ErrorState.None || !CommitEditTextBox())) { base.WndProc(ref m); EditTextBox.Focus(); return; } break; case (int)PInvokeCore.WM_IME_STARTCOMPOSITION: EditTextBox.Focus(); EditTextBox.Clear(); PInvokeCore.PostMessage(EditTextBox, PInvokeCore.WM_IME_STARTCOMPOSITION); return; case (int)PInvokeCore.WM_IME_COMPOSITION: EditTextBox.Focus(); PInvokeCore.PostMessage(EditTextBox, PInvokeCore.WM_IME_COMPOSITION, m.WParamInternal, m.LParamInternal); return; case (int)PInvokeCore.WM_GETDLGCODE: uint flags = PInvoke.DLGC_WANTCHARS | PInvoke.DLGC_WANTARROWS; if (_selectedGridEntry is not null && (ModifierKeys & Keys.Shift) == 0) { // If we're going backwards, we don't want the tab. // Otherwise we only want it if we have an edit. Debug.Assert(_editTextBox is not null); if (_editTextBox.Visible) { flags |= PInvoke.DLGC_WANTTAB; } } m.ResultInternal = (LRESULT)(nint)flags; return; case (int)PInvokeCore.WM_MOUSEMOVE: // Check if it's the same position, of so eat the message. if (m.LParamInternal == _lastMouseMove) { return; } _lastMouseMove = (int)m.LParamInternal; break; case (int)PInvokeCore.WM_NOTIFY: if (WmNotify(ref m)) { return; } break; } base.WndProc(ref m); } /// <summary> /// Rescale constants for the DPI change /// </summary> protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew) { base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew); RescaleConstants(); } /// <summary> /// Rescale constants on this object. /// </summary> private void RescaleConstants() { if (ScaleHelper.IsScalingRequirementMet) { _cachedRowHeight = -1; _paintWidth = LogicalToDeviceUnits(LogicalPaintWidth); _paintIndent = LogicalToDeviceUnits(LogicalPaintIndent); _outlineSizeExplorerTreeStyle = LogicalToDeviceUnits(OutlineSizeExplorerTreeStyle); _outlineSize = LogicalToDeviceUnits(LogicalOutlineSize); _maxListBoxHeight = LogicalToDeviceUnits(LogicalMaxListBoxHeight); _offset2Units = LogicalToDeviceUnits(Offset2Pixels); if (TopLevelGridEntries is not null) { foreach (GridEntry entry in TopLevelGridEntries) { entry.ResetOutlineRectangle(); } } } } }