From f61fe460b2cf23d363431c4f8e90b052a2aab436 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Thu, 2 Jun 2022 15:52:47 -0700 Subject: [PATCH 001/132] Mac: Allow interaction with FloatingForm when it has WindowStyle.None or Default. --- src/Eto.Mac/Forms/FloatingFormHandler.cs | 115 +++++++++++- src/Eto.Mac/Forms/MacWindow.cs | 6 +- src/Eto.Mac/Forms/NativeFormHandler.cs | 18 +- src/Eto.Wpf/WpfConversions.cs | 1 + .../Sections/Behaviors/WindowsSection.cs | 167 ++++++++++-------- 5 files changed, 229 insertions(+), 78 deletions(-) diff --git a/src/Eto.Mac/Forms/FloatingFormHandler.cs b/src/Eto.Mac/Forms/FloatingFormHandler.cs index e017b22532..f08a6c62e3 100644 --- a/src/Eto.Mac/Forms/FloatingFormHandler.cs +++ b/src/Eto.Mac/Forms/FloatingFormHandler.cs @@ -1,3 +1,4 @@ +using System; using Eto.Forms; namespace Eto.Mac.Forms @@ -26,25 +27,129 @@ protected override void Initialize() ShowInTaskbar = false; } + + static readonly object LastOwner_Key = new object(); + + protected override NSWindowLevel TopmostWindowLevel => NSWindowLevel.Floating; + public override void SetOwner(Window owner) { base.SetOwner(owner); - + // When this is true, the NSPanel would hide the panel and owner if they aren't key. // So, only hide on deactivate if it is an ownerless form. Control.HidesOnDeactivate = owner == null; + + SetLevelAdjustment(); + } + + void SetLevelAdjustment() + { + // only need to adjust level when window style is not utility and we actually want it to be topmost (default for FloatingForm). + var wantsTopmost = Widget.Properties.Get(Topmost_Key, true); + var owner = Widget.Owner; + var needsLevelAdjust = wantsTopmost && WindowStyle != WindowStyle.Utility && owner != null; + + var lastOwner = Widget.Properties.Get(LastOwner_Key); + + if (!needsLevelAdjust) + { + if (lastOwner != null) + { + // no longer need window level adjustments, unregister + lastOwner.GotFocus -= Owner_GotFocus; + lastOwner.LostFocus -= Owner_LostFocus; + Widget.Closed -= Widget_Closed; + Widget.Properties.Set(LastOwner_Key, null); + } + if (wantsTopmost) + { + SetAsTopmost(); + } + return; + } + + if (!ReferenceEquals(lastOwner, owner)) + { + Widget.Properties.Set(LastOwner_Key, owner); + if (lastOwner != null) + { + lastOwner.GotFocus -= Owner_GotFocus; + lastOwner.LostFocus -= Owner_LostFocus; + Widget.Closed -= Widget_Closed; + } + if (owner != null) + { + owner.GotFocus += Owner_GotFocus; + owner.LostFocus += Owner_LostFocus; + Widget.Closed += Widget_Closed; + } + } + + if (lastOwner == null || lastOwner.HasFocus) + SetAsTopmost(); + } + + private void Widget_Closed(object sender, EventArgs e) + { + var lastOwner = Widget.Properties.Get(LastOwner_Key); + if (lastOwner != null) + { + // when closed we need to disconnect from owner to prevent leaks + lastOwner.GotFocus -= Owner_GotFocus; + lastOwner.LostFocus -= Owner_LostFocus; + } + } + + static readonly object Topmost_Key = new object(); + + public override bool Topmost + { + get => base.Topmost; + set + { + base.Topmost = value; + // need to remember the preferred state as it can be changed on us when setting the owner + Widget.Properties.Set(Topmost_Key, value, true); + SetLevelAdjustment(); + } + } + + public override WindowStyle WindowStyle + { + get => base.WindowStyle; + set + { + base.WindowStyle = value; + SetLevelAdjustment(); + } + } + + private void Owner_GotFocus(object sender, EventArgs e) => SetAsTopmost(); + + void SetAsTopmost() + { + Control.Level = TopmostWindowLevel; + Control.HidesOnDeactivate = true; + } + + private void Owner_LostFocus(object sender, EventArgs e) + { + Control.Level = NSWindowLevel.Normal; + if (Control.IsVisible) + Control.OrderFront(Control); } protected override NSPanel CreateControl() { - var panel = new EtoPanel(new CGRect(0, 0, 200, 200), - NSWindowStyle.Resizable | NSWindowStyle.Closable | NSWindowStyle.Titled, + var panel = new EtoPanel(new CGRect(0, 0, 200, 200), + NSWindowStyle.Resizable | NSWindowStyle.Closable | NSWindowStyle.Titled, NSBackingStore.Buffered, false); + panel.CanFocus = true; - panel.FloatingPanel = true; panel.BecomesKeyOnlyIfNeeded = true; - + return panel; } } diff --git a/src/Eto.Mac/Forms/MacWindow.cs b/src/Eto.Mac/Forms/MacWindow.cs index 8594a5e850..011801b1a9 100644 --- a/src/Eto.Mac/Forms/MacWindow.cs +++ b/src/Eto.Mac/Forms/MacWindow.cs @@ -599,7 +599,7 @@ public bool ShowInTaskbar protected virtual NSWindowLevel TopmostWindowLevel => NSWindowLevel.PopUpMenu; - public bool Topmost + public virtual bool Topmost { get => Control.Level >= NSWindowLevel.Floating; set @@ -678,7 +678,7 @@ protected bool SetAutoSize() } var ret = AutoSize || setInitialSize; - + if (Widget.Loaded) { PerformAutoSize(); @@ -1122,7 +1122,7 @@ public Screen Screen } } - public WindowStyle WindowStyle + public virtual WindowStyle WindowStyle { get { return Control.StyleMask.ToEtoWindowStyle(); } set diff --git a/src/Eto.Mac/Forms/NativeFormHandler.cs b/src/Eto.Mac/Forms/NativeFormHandler.cs index 0676fd0d5f..4f8a5a6a44 100644 --- a/src/Eto.Mac/Forms/NativeFormHandler.cs +++ b/src/Eto.Mac/Forms/NativeFormHandler.cs @@ -1,5 +1,6 @@ using System; using Eto.Drawing; +using Eto.Forms; namespace Eto.Mac.Forms { @@ -16,7 +17,22 @@ public NativeFormHandler(NSWindowController windowController) public override void AttachEvent(string id) { - // can't attach any events, this is a native window! + // native window, so attach notifications instead of using the delegate so we don't clobber existing functionality + switch (id) + { + case Window.ClosedEvent: + NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.WillCloseNotification, n => Callback.OnClosed(Widget, EventArgs.Empty)); + break; + case Window.SizeChangedEvent: + NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResizeNotification, n => Callback.OnSizeChanged(Widget, EventArgs.Empty)); + break; + case Window.GotFocusEvent: + NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidBecomeKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); + break; + case Window.LostFocusEvent: + NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResignKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); + break; + } return; } diff --git a/src/Eto.Wpf/WpfConversions.cs b/src/Eto.Wpf/WpfConversions.cs index 58c6d0c57d..17d59dd59c 100755 --- a/src/Eto.Wpf/WpfConversions.cs +++ b/src/Eto.Wpf/WpfConversions.cs @@ -521,6 +521,7 @@ public static WindowStyle ToEto(this sw.WindowStyle style) case sw.WindowStyle.None: return WindowStyle.None; case sw.WindowStyle.ThreeDBorderWindow: + case sw.WindowStyle.SingleBorderWindow: return WindowStyle.Default; case sw.WindowStyle.ToolWindow: return WindowStyle.Utility; diff --git a/test/Eto.Test/Sections/Behaviors/WindowsSection.cs b/test/Eto.Test/Sections/Behaviors/WindowsSection.cs index 91ced8c7f2..5ac700a375 100644 --- a/test/Eto.Test/Sections/Behaviors/WindowsSection.cs +++ b/test/Eto.Test/Sections/Behaviors/WindowsSection.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Eto.Test.Sections.Behaviors { @@ -13,7 +14,7 @@ public class WindowsSection : Panel, INotifyPropertyChanged Button bringToFrontButton; EnumRadioButtonList styleCombo; EnumRadioButtonList stateCombo; - RadioButtonList typeRadio; + EnumRadioButtonList typeRadio; CheckBox resizableCheckBox; CheckBox maximizableCheckBox; CheckBox minimizableCheckBox; @@ -27,7 +28,7 @@ public class WindowsSection : Panel, INotifyPropertyChanged CheckBox createMenuBar; EnumCheckBoxList systemMenuItems; EnumDropDown dialogDisplayModeDropDown; - + static readonly object CancelCloseKey = new object(); public bool CancelClose { @@ -58,23 +59,50 @@ public WindowsSection() layout.Add(null); Content = layout; - + DataContext = settings = new SettingsWindow(); } - + SettingsWindow settings; - - class SettingsWindow + + enum WindowType + { + Form, + FloatingForm, + Dialog + } + + class SettingsWindow : INotifyPropertyChanged { public bool ThreeState => true; // enable three state for these settings public bool? Resizable { get; set; } public bool? CanFocus { get; set; } - public bool? Minimizable { get; set;} - public bool? Maximizable { get; set;} + public bool? Minimizable { get; set; } + public bool? Maximizable { get; set; } public bool? MovableByWindowBackground { get; set; } - public bool? ShowInTaskbar { get; set;} + public bool? ShowInTaskbar { get; set; } public bool? ShowActivated { get; set; } public bool? Topmost { get; set; } + public WindowStyle WindowStyle { get; set; } + public WindowType WindowType { get; set; } + + bool windowStyleEnabled; + public bool WindowStyleEnabled + { + get => windowStyleEnabled; + set => Set(ref windowStyleEnabled, value); + } + + public event PropertyChangedEventHandler PropertyChanged; + + void Set(ref T prop, T value, [CallerMemberName] string propertyName = null) + { + if (!Equals(prop, value)) + { + prop = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } } protected override void OnUnLoad(EventArgs e) @@ -130,18 +158,12 @@ Control CreateMenuBarControls() Control CreateTypeControls() { - typeRadio = new RadioButtonList - { - Items = - { - new ListItem { Text = "Form (modeless)", Key = "form" }, - new ListItem { Text = "Floating Form (modeless)", Key = "floating" }, - new ListItem { Text = "Dialog (modal)", Key = "dialog" } - }, - SelectedKey = "form" - }; + typeRadio = new EnumRadioButtonList(); + + typeRadio.SelectedValueBinding.BindDataContext((SettingsWindow m) => m.WindowType); + typeRadio.BindDataContext(c => c.Enabled, Binding.Delegate((object m) => m is SettingsWindow)); - setOwnerCheckBox = new CheckBox { Text = "Set Owner", Checked = false }; + setOwnerCheckBox = new CheckBox { Text = "Set Owner" }; setOwnerCheckBox.CheckedChanged += (sender, e) => { if (child != null) @@ -154,21 +176,20 @@ Control CreateTypeControls() Items = { typeRadio, setOwnerCheckBox } }; } - + Control WindowStyle() { var enableStyle = new CheckBox { Checked = false }; - styleCombo = new EnumRadioButtonList - { - SelectedValue = Forms.WindowStyle.Default - }; - styleCombo.SelectedIndexChanged += (sender, e) => - { - if (child != null) - child.WindowStyle = styleCombo.SelectedValue; - }; - - styleCombo.Bind(c => c.Enabled, enableStyle, c => c.Checked); + styleCombo = new EnumRadioButtonList(); + styleCombo.SelectedValueBinding.BindDataContext((SettingsWindow w) => w.WindowStyle); + + var enabledBinding = Binding.Property(w => w.WindowStyleEnabled); + var enabledBindingElseTrue = enabledBinding.Convert(r => r ?? true, r => r); + var enabledBindingCanToggle = enabledBinding.Convert(r => r != null); + + styleCombo.BindDataContext(c => c.Enabled, enabledBindingElseTrue); + enableStyle.CheckedBinding.BindDataContext(enabledBindingElseTrue); + enableStyle.BindDataContext(c => c.Enabled, enabledBindingCanToggle); return new TableLayout(new TableRow(enableStyle, styleCombo)); } @@ -418,43 +439,50 @@ void CreateChild() layout.AddCentered(CreateCancelClose()); layout.AddCentered(CloseButton()); - if (typeRadio.SelectedKey == "form") + switch (settings.WindowType) { - var form = new Form(); - child = form; - show = form.Show; - if (showActivatedCheckBox.Checked != null) - form.ShowActivated = showActivatedCheckBox.Checked == true; - if (canFocusCheckBox.Checked != null) - form.CanFocus = canFocusCheckBox.Checked == true; - } - else if (typeRadio.SelectedKey == "floating") - { - var form = new FloatingForm(); - child = form; - show = form.Show; - if (showActivatedCheckBox.Checked != null) - form.ShowActivated = showActivatedCheckBox.Checked == true; - if (canFocusCheckBox.Checked != null) - form.CanFocus = canFocusCheckBox.Checked == true; - } - else - { - var dialog = new Dialog(); - - dialog.DefaultButton = new Button { Text = "Default" }; - dialog.DefaultButton.Click += (sender, e) => Log.Write(dialog, "Default button clicked"); - - dialog.AbortButton = new Button { Text = "Abort" }; - dialog.AbortButton.Click += (sender, e) => Log.Write(dialog, "Abort button clicked"); - - layout.AddSeparateRow(null, dialog.DefaultButton, dialog.AbortButton, null); - - child = dialog; - show = dialog.ShowModal; - - if (dialogDisplayModeDropDown.SelectedValue != null) - dialog.DisplayMode = dialogDisplayModeDropDown.SelectedValue ?? DialogDisplayMode.Default; + default: + case WindowType.Form: + { + var form = new Form(); + child = form; + show = form.Show; + if (showActivatedCheckBox.Checked != null) + form.ShowActivated = showActivatedCheckBox.Checked == true; + if (canFocusCheckBox.Checked != null) + form.CanFocus = canFocusCheckBox.Checked == true; + } + break; + case WindowType.FloatingForm: + { + var form = new FloatingForm(); + child = form; + show = form.Show; + if (showActivatedCheckBox.Checked != null) + form.ShowActivated = showActivatedCheckBox.Checked == true; + if (canFocusCheckBox.Checked != null) + form.CanFocus = canFocusCheckBox.Checked == true; + } + break; + case WindowType.Dialog: + { + var dialog = new Dialog(); + + dialog.DefaultButton = new Button { Text = "Default" }; + dialog.DefaultButton.Click += (sender, e) => Log.Write(dialog, "Default button clicked"); + + dialog.AbortButton = new Button { Text = "Abort" }; + dialog.AbortButton.Click += (sender, e) => Log.Write(dialog, "Abort button clicked"); + + layout.AddSeparateRow(null, dialog.DefaultButton, dialog.AbortButton, null); + + child = dialog; + show = dialog.ShowModal; + + if (dialogDisplayModeDropDown.SelectedValue != null) + dialog.DisplayMode = dialogDisplayModeDropDown.SelectedValue ?? DialogDisplayMode.Default; + } + break; } layout.Add(null); @@ -563,10 +591,11 @@ static void child_GotFocus(object sender, EventArgs e) Log.Write(child, "GotFocus"); } - static void child_Shown(object sender, EventArgs e) + void child_Shown(object sender, EventArgs e) { var child = (Window)sender; Log.Write(child, "Shown"); + OnDataContextChanged(EventArgs.Empty); } static void child_OwnerChanged(object sender, EventArgs e) From 75d2d2f3afc182c72ca523e5d237b15ccbb82af8 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Fri, 3 Jun 2022 12:05:34 -0700 Subject: [PATCH 002/132] Mac: FloatingForm should not hide owner when deactivated - NativeFormHandler didn't wire up LostFocusEvent properly - Only look at non-disposed windows in Window.FromPoint() --- src/Eto.Mac/Forms/FloatingFormHandler.cs | 9 ++++----- src/Eto.Mac/Forms/NativeFormHandler.cs | 2 +- src/Eto.Mac/Forms/WindowHandler.cs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Eto.Mac/Forms/FloatingFormHandler.cs b/src/Eto.Mac/Forms/FloatingFormHandler.cs index f08a6c62e3..c562031f62 100644 --- a/src/Eto.Mac/Forms/FloatingFormHandler.cs +++ b/src/Eto.Mac/Forms/FloatingFormHandler.cs @@ -36,10 +36,6 @@ public override void SetOwner(Window owner) { base.SetOwner(owner); - // When this is true, the NSPanel would hide the panel and owner if they aren't key. - // So, only hide on deactivate if it is an ownerless form. - Control.HidesOnDeactivate = owner == null; - SetLevelAdjustment(); } @@ -130,7 +126,10 @@ public override WindowStyle WindowStyle void SetAsTopmost() { Control.Level = TopmostWindowLevel; - Control.HidesOnDeactivate = true; + + // When this is true, the NSPanel would hide the panel and owner if they aren't key. + // So, only hide on deactivate if it is an ownerless form. + Control.HidesOnDeactivate = Widget.Owner == null; } private void Owner_LostFocus(object sender, EventArgs e) diff --git a/src/Eto.Mac/Forms/NativeFormHandler.cs b/src/Eto.Mac/Forms/NativeFormHandler.cs index 4f8a5a6a44..313318e858 100644 --- a/src/Eto.Mac/Forms/NativeFormHandler.cs +++ b/src/Eto.Mac/Forms/NativeFormHandler.cs @@ -30,7 +30,7 @@ public override void AttachEvent(string id) NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidBecomeKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); break; case Window.LostFocusEvent: - NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResignKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); + NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResignKeyNotification, n => Callback.OnLostFocus(Widget, EventArgs.Empty)); break; } return; diff --git a/src/Eto.Mac/Forms/WindowHandler.cs b/src/Eto.Mac/Forms/WindowHandler.cs index a098369c12..8747708b3b 100644 --- a/src/Eto.Mac/Forms/WindowHandler.cs +++ b/src/Eto.Mac/Forms/WindowHandler.cs @@ -13,7 +13,7 @@ public Window FromPoint(PointF point) var windowNumber = NSWindow.WindowNumberAtPoint(nspoint, 0); foreach (var window in Application.Instance.Windows) { - if (window.Handler is IMacWindow handler && handler.Control.WindowNumber == windowNumber) + if (!window.IsDisposed && window.Handler is IMacWindow handler && handler.Control.WindowNumber == windowNumber) { return window; } From 9d2d0057b722b116e76f5375b3cf45a65d2c5f2b Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Fri, 3 Jun 2022 13:27:38 -0700 Subject: [PATCH 003/132] Mac: Respect Window.Topmost even when Owner is set --- src/Eto.Mac/Forms/FloatingFormHandler.cs | 19 ++++++++----------- src/Eto.Mac/Forms/MacWindow.cs | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Eto.Mac/Forms/FloatingFormHandler.cs b/src/Eto.Mac/Forms/FloatingFormHandler.cs index c562031f62..6cc80f5eca 100644 --- a/src/Eto.Mac/Forms/FloatingFormHandler.cs +++ b/src/Eto.Mac/Forms/FloatingFormHandler.cs @@ -32,17 +32,10 @@ protected override void Initialize() protected override NSWindowLevel TopmostWindowLevel => NSWindowLevel.Floating; - public override void SetOwner(Window owner) - { - base.SetOwner(owner); - - SetLevelAdjustment(); - } - void SetLevelAdjustment() { // only need to adjust level when window style is not utility and we actually want it to be topmost (default for FloatingForm). - var wantsTopmost = Widget.Properties.Get(Topmost_Key, true); + var wantsTopmost = WantsTopmost; var owner = Widget.Owner; var needsLevelAdjust = wantsTopmost && WindowStyle != WindowStyle.Utility && owner != null; @@ -97,7 +90,7 @@ private void Widget_Closed(object sender, EventArgs e) } } - static readonly object Topmost_Key = new object(); + internal override bool DefaultTopmost => true; public override bool Topmost { @@ -105,8 +98,6 @@ public override bool Topmost set { base.Topmost = value; - // need to remember the preferred state as it can be changed on us when setting the owner - Widget.Properties.Set(Topmost_Key, value, true); SetLevelAdjustment(); } } @@ -151,5 +142,11 @@ protected override NSPanel CreateControl() return panel; } + + internal override void OnSetAsChildWindow() + { + // Don't call base, we do our own window level logic for FloatingForm. + SetLevelAdjustment(); + } } } diff --git a/src/Eto.Mac/Forms/MacWindow.cs b/src/Eto.Mac/Forms/MacWindow.cs index 011801b1a9..5db0e808fe 100644 --- a/src/Eto.Mac/Forms/MacWindow.cs +++ b/src/Eto.Mac/Forms/MacWindow.cs @@ -196,6 +196,7 @@ static class MacWindow internal static readonly IntPtr selIsVisible_Handle = Selector.GetHandle("isVisible"); internal static readonly object AnimateSizeChanges_Key = new object(); internal static readonly object DisableAutoSize_Key = new object(); + internal static readonly object Topmost_Key = new object(); } public abstract class MacWindow : MacPanel, Window.IHandler, IMacWindow @@ -604,12 +605,22 @@ public virtual bool Topmost get => Control.Level >= NSWindowLevel.Floating; set { - if (Topmost != value) + // need to remember the preferred state as it can be changed on us when setting the owner + if (WantsTopmost != value) { + WantsTopmost = value; Control.Level = value ? TopmostWindowLevel : NSWindowLevel.Normal; } } } + + internal virtual bool DefaultTopmost => false; + + internal bool WantsTopmost + { + get => Widget.Properties.Get(MacWindow.Topmost_Key, DefaultTopmost); + set => Widget.Properties.Set(MacWindow.Topmost_Key, value, DefaultTopmost); + } public override Size Size { @@ -1212,7 +1223,10 @@ public virtual void SetOwner(Window owner) { var macWindow = owner.Handler as IMacWindow; if (macWindow != null) + { macWindow.Control.AddChildWindow(Control, NSWindowOrderingMode.Above); + OnSetAsChildWindow(); + } } else { @@ -1223,6 +1237,12 @@ public virtual void SetOwner(Window owner) } } + internal virtual void OnSetAsChildWindow() + { + if (WantsTopmost && Control.Level != TopmostWindowLevel) + Control.Level = TopmostWindowLevel; + } + public float LogicalPixelSize => Screen?.LogicalPixelSize ?? 1f; } } From 57544a36ddf7c47ee63c489f7cfdc13347e13b01 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Fri, 3 Jun 2022 15:45:05 -0700 Subject: [PATCH 004/132] Mac: More FloatingForm/NativeForm fixes - When FloatingForm is shown active it didn't set level properly when losing focus - new NativeForm observed events were not set specific to that window --- src/Eto.Mac/Forms/FloatingFormHandler.cs | 14 +++++++++++++- src/Eto.Mac/Forms/NativeFormHandler.cs | 10 +++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Eto.Mac/Forms/FloatingFormHandler.cs b/src/Eto.Mac/Forms/FloatingFormHandler.cs index 6cc80f5eca..a19bebf48a 100644 --- a/src/Eto.Mac/Forms/FloatingFormHandler.cs +++ b/src/Eto.Mac/Forms/FloatingFormHandler.cs @@ -65,12 +65,16 @@ void SetLevelAdjustment() { lastOwner.GotFocus -= Owner_GotFocus; lastOwner.LostFocus -= Owner_LostFocus; + Widget.GotFocus -= Owner_GotFocus; + Widget.LostFocus -= Owner_LostFocus; Widget.Closed -= Widget_Closed; } if (owner != null) { owner.GotFocus += Owner_GotFocus; owner.LostFocus += Owner_LostFocus; + Widget.GotFocus += Owner_GotFocus; + Widget.LostFocus += Owner_LostFocus; Widget.Closed += Widget_Closed; } } @@ -87,6 +91,8 @@ private void Widget_Closed(object sender, EventArgs e) // when closed we need to disconnect from owner to prevent leaks lastOwner.GotFocus -= Owner_GotFocus; lastOwner.LostFocus -= Owner_LostFocus; + Widget.GotFocus -= Owner_GotFocus; + Widget.LostFocus -= Owner_LostFocus; } } @@ -125,9 +131,15 @@ void SetAsTopmost() private void Owner_LostFocus(object sender, EventArgs e) { + if (HasFocus || Widget.Owner.HasFocus) + return; Control.Level = NSWindowLevel.Normal; + + // Window will still be topmost until we order the window explicitly. + // If there are multiple app windows, we use this instead of OrderFront otherwise + // the original parent window steals focus again. if (Control.IsVisible) - Control.OrderFront(Control); + Control.OrderWindow(NSWindowOrderingMode.Above, Control.ParentWindow?.WindowNumber ?? 0); } protected override NSPanel CreateControl() diff --git a/src/Eto.Mac/Forms/NativeFormHandler.cs b/src/Eto.Mac/Forms/NativeFormHandler.cs index 313318e858..fe49d9ae7a 100644 --- a/src/Eto.Mac/Forms/NativeFormHandler.cs +++ b/src/Eto.Mac/Forms/NativeFormHandler.cs @@ -17,20 +17,20 @@ public NativeFormHandler(NSWindowController windowController) public override void AttachEvent(string id) { - // native window, so attach notifications instead of using the delegate so we don't clobber existing functionality + // native window, so attach observers instead of using the delegate so we don't clobber existing functionality switch (id) { case Window.ClosedEvent: - NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.WillCloseNotification, n => Callback.OnClosed(Widget, EventArgs.Empty)); + AddObserver(NSWindow.WillCloseNotification, n => Callback.OnClosed(Widget, EventArgs.Empty)); break; case Window.SizeChangedEvent: - NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResizeNotification, n => Callback.OnSizeChanged(Widget, EventArgs.Empty)); + AddObserver(NSWindow.DidResizeNotification, n => Callback.OnSizeChanged(Widget, EventArgs.Empty)); break; case Window.GotFocusEvent: - NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidBecomeKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); + AddObserver(NSWindow.DidBecomeKeyNotification, n => Callback.OnGotFocus(Widget, EventArgs.Empty)); break; case Window.LostFocusEvent: - NSNotificationCenter.DefaultCenter.AddObserver(NSWindow.DidResignKeyNotification, n => Callback.OnLostFocus(Widget, EventArgs.Empty)); + AddObserver(NSWindow.DidResignKeyNotification, n => Callback.OnLostFocus(Widget, EventArgs.Empty)); break; } return; From 1d67324d60b8e89d436baa6480e89a5937404527 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Fri, 10 Jun 2022 12:45:01 -0700 Subject: [PATCH 005/132] Tree/GridView column width and cell types for GetCellAt - Added new Grid.ColumnWidthChanged event - Added GridCellType to GridCell and TreeGridCell for GetCellAt() APIs, which can be used to determine if the mouse is over the header or not - Mac/Gtk: Enable mouse events on the header - Gtk: Fix location in mouse events for Tree/GridView - Mac: Fix context menu selected rows when not right clicking on a row - Mac: Show Grid.ContextMenu for the header --- .../Forms/Controls/GridColumnHandler.cs | 57 +++--- src/Eto.Gtk/Forms/Controls/GridHandler.cs | 171 ++++++++++-------- src/Eto.Gtk/Forms/Controls/GridViewHandler.cs | 89 ++++++--- .../Forms/Controls/TreeGridViewHandler.cs | 109 ++++++----- src/Eto.Gtk/Forms/GtkControl.cs | 24 ++- src/Eto.Gtk/GtkConversions.cs | 15 +- src/Eto.Mac/Forms/Controls/GridHandler.cs | 65 +++++-- src/Eto.Mac/Forms/Controls/GridViewHandler.cs | 65 +++++-- .../Forms/Controls/TreeGridViewHandler.cs | 67 +++++-- src/Eto.Mac/Forms/MacView.cs | 2 +- .../Forms/Controls/GridColumnHandler.cs | 11 +- .../Forms/Controls/GridHandler.cs | 42 ++++- .../Forms/Controls/GridViewHandler.cs | 10 +- .../Forms/Controls/TreeGridViewHandler.cs | 9 +- src/Eto.WinForms/WinConversions.cs | 13 ++ src/Eto.Wpf/Forms/Cells/CellHandler.cs | 13 ++ .../Forms/Controls/GridColumnHandler.cs | 6 + src/Eto.Wpf/Forms/Controls/GridHandler.cs | 65 ++++++- src/Eto.Wpf/Forms/Controls/GridViewHandler.cs | 24 +-- .../Forms/Controls/TreeGridViewHandler.cs | 20 +- src/Eto/Forms/Controls/Grid.cs | 38 ++++ src/Eto/Forms/Controls/GridView.cs | 49 ++--- src/Eto/Forms/Controls/TreeGridView.cs | 59 ++++-- .../Sections/Controls/GridViewSection.cs | 71 +++++--- .../Sections/Controls/TreeGridViewSection.cs | 2 +- test/Eto.Test/Settings.cs | 5 +- 26 files changed, 761 insertions(+), 340 deletions(-) diff --git a/src/Eto.Gtk/Forms/Controls/GridColumnHandler.cs b/src/Eto.Gtk/Forms/Controls/GridColumnHandler.cs index 85814a3c55..b589b21da6 100644 --- a/src/Eto.Gtk/Forms/Controls/GridColumnHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/GridColumnHandler.cs @@ -13,6 +13,7 @@ public interface IGridHandler void ColumnClicked(GridColumnHandler column); int GetColumnDisplayIndex(GridColumnHandler column); void SetColumnDisplayIndex(GridColumnHandler column, int index); + void ColumnWidthChanged(GridColumnHandler h); } public class GridColumnHandler : WidgetHandler, GridColumn.IHandler @@ -31,32 +32,32 @@ public GridColumnHandler() AutoSize = true; Resizable = true; DataCell = new TextBoxCell(); + Control.Clickable = true; } public string HeaderText { - get { return Control.Title; } - set { Control.Title = value; } + get => Control.Title; + set => Control.Title = value; } public bool Resizable { - get { return Control.Resizable; } - set { Control.Resizable = value; } + get => Control.Resizable; + set => Control.Resizable = value; } + static readonly object Sortable_Key = new object(); + public bool Sortable { - get { return Control.Clickable; } - set { Control.Clickable = value; } + get => Widget.Properties.Get(Sortable_Key); + set => Widget.Properties.Set(Sortable_Key, value); } public bool AutoSize { - get - { - return autoSize; - } + get => Control.Sizing == Gtk.TreeViewColumnSizing.Fixed ? false : true; set { autoSize = value; @@ -90,7 +91,7 @@ public bool Editable public int Width { - get { return Control.Width; } + get => Control.Width; set { autoSize = value == -1; @@ -101,20 +102,14 @@ public int Width public Cell DataCell { - get - { - return dataCell; - } - set - { - dataCell = value; - } + get => dataCell; + set => dataCell = value; } public bool Visible { - get { return Control.Visible; } - set { Control.Visible = value; } + get => Control.Visible; + set => Control.Visible = value; } public void SetupCell(IGridHandler grid, ICellDataSource source, int columnIndex, ref int dataIndex) @@ -146,6 +141,8 @@ public void SetupEvents() HandleEvent(Grid.ColumnHeaderClickEvent); if (grid.IsEventHandled(Grid.CellFormattingEvent)) HandleEvent(Grid.CellFormattingEvent); + if (grid.IsEventHandled(Grid.ColumnWidthChangedEvent)) + HandleEvent(Grid.ColumnWidthChangedEvent); } public override void AttachEvent(string id) @@ -157,10 +154,24 @@ public override void AttachEvent(string id) Control.Clicked += (sender, e) => { var h = ((GridColumnHandler)handler.Target); - if (h != null && h.grid != null) + if (h != null && h.grid != null && h.Sortable) h.grid.ColumnClicked(h); }; break; + case Grid.ColumnWidthChangedEvent: + var handler2 = new WeakReference(this); + var lastWidth = -1; + Control.AddNotification("width", (o, args) => + { + var h = (GridColumnHandler)handler2.Target; + if (h == null) + return; + if (lastWidth == h.Width) + return; + lastWidth = h.Width; + h.grid?.ColumnWidthChanged(h); + }); + break; default: ((ICellHandler)dataCell.Handler).HandleEvent(id); break; @@ -202,7 +213,7 @@ public int MaxWidth GridHandler?.Tree?.ColumnsAutosize(); } } - + int? displayIndex; public int DisplayIndex diff --git a/src/Eto.Gtk/Forms/Controls/GridHandler.cs b/src/Eto.Gtk/Forms/Controls/GridHandler.cs index 3e1dc79cd3..c9a19b651c 100644 --- a/src/Eto.Gtk/Forms/Controls/GridHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/GridHandler.cs @@ -17,7 +17,7 @@ class GridHandler internal static readonly object AllowEmptySelection_Key = new object(); } - public abstract class GridHandler : GtkControl, Grid.IHandler, ICellDataSource, IGridHandler + public abstract class GridHandler : GtkControl, Grid.IHandler, ICellDataSource, IGridHandler where TWidget : Grid where TCallback : Grid.ICallback { @@ -28,15 +28,17 @@ public abstract class GridHandler : GtkControl Tree; + Gtk.TreeView IGridHandler.Tree => Control; protected Dictionary ColumnMap { get { return columnMap; } } - public override Gtk.Widget EventControl => Tree; + public override Gtk.Widget ContainerControl => ScrolledWindow; + + public override bool ShouldTranslatePoints => true; - public override Gtk.Widget DragControl => Tree; class EtoScrolledWindow : Gtk.ScrolledWindow { @@ -80,7 +82,7 @@ protected override void OnGetPreferredHeight(out int minimum_height, out int nat protected GridHandler() { - Control = new EtoScrolledWindow + ScrolledWindow = new EtoScrolledWindow { Handler = this, ShadowType = Gtk.ShadowType.In, @@ -89,6 +91,8 @@ protected GridHandler() PropagateNaturalWidth = true #endif }; + // Box = new EtoEventBox(); + // ScrolledWindow.Add(Box); } protected abstract ITreeModelImplementor CreateModelImplementor(); @@ -97,15 +101,15 @@ protected void UpdateModel() { SkipSelectedChange = true; var selected = SelectedRows; - Tree.Model = new Gtk.TreeModelAdapter(CreateModelImplementor()); + Control.Model = new Gtk.TreeModelAdapter(CreateModelImplementor()); SetSelectedRows(selected); SkipSelectedChange = false; } protected (double? hscroll, double? vscroll) SaveScrollState() { - var hscrollbar = Control.HScrollbar as Gtk.Scrollbar; - var vscrollbar = Control.VScrollbar as Gtk.Scrollbar; + var hscrollbar = ScrolledWindow.HScrollbar as Gtk.Scrollbar; + var vscrollbar = ScrolledWindow.VScrollbar as Gtk.Scrollbar; var hscroll = hscrollbar?.Value; var vscroll = vscrollbar?.Value; return (hscroll, vscroll); @@ -114,8 +118,8 @@ protected void UpdateModel() protected void RestoreScrollState((double? hscroll, double? vscroll) state) { - var hscrollbar = Control.HScrollbar as Gtk.Scrollbar; - var vscrollbar = Control.VScrollbar as Gtk.Scrollbar; + var hscrollbar = ScrolledWindow.HScrollbar as Gtk.Scrollbar; + var vscrollbar = ScrolledWindow.VScrollbar as Gtk.Scrollbar; if (state.hscroll != null) hscrollbar.Value = state.hscroll.Value; if (state.vscroll != null) @@ -124,17 +128,17 @@ protected void RestoreScrollState((double? hscroll, double? vscroll) state) protected override void Initialize() { - Tree = new Gtk.TreeView(); + Control = new Gtk.TreeView(); // always need this one so the last column doesn't expand - Tree.AppendColumn(spacingColumn = new Gtk.TreeViewColumn()); - Tree.ColumnDragFunction = Connector.HandleColumnDrag; + Control.AppendColumn(spacingColumn = new Gtk.TreeViewColumn()); + Control.ColumnDragFunction = Connector.HandleColumnDrag; UpdateModel(); - Tree.HeadersVisible = true; - Control.Add(Tree); + Control.HeadersVisible = true; + ScrolledWindow.Child = Control; - Tree.Events |= Gdk.EventMask.ButtonPressMask; - Tree.ButtonPressEvent += Connector.HandleButtonPress; + Control.Events |= Gdk.EventMask.ButtonPressMask; + Control.ButtonPressEvent += Connector.HandleButtonPress; columns = new ColumnCollection { Handler = this }; columns.Register(Widget.Columns); @@ -146,10 +150,7 @@ protected override void Initialize() protected new GridConnector Connector => (GridConnector)base.Connector; - protected override WeakConnector CreateConnector() - { - return new GridConnector(); - } + protected override WeakConnector CreateConnector() => new GridConnector(); protected class GridConnector : GtkControlConnector { @@ -249,20 +250,20 @@ public virtual void OnTreeButtonPress(object sender, Gtk.ButtonPressEventArgs e) Gtk.TreeViewColumn clickedColumn; // Get path and column from mouse position - h.Tree.GetPathAtPos((int)e.Event.X, (int)e.Event.Y, out path, out clickedColumn); + h.Control.GetPathAtPos((int)e.Event.X, (int)e.Event.Y, out path, out clickedColumn); if (path == null || clickedColumn == null) return; var rowIndex = h.GetRowIndexOfPath(path); - var columnIndex = h.GetColumnOfItem(clickedColumn); + var columnIndex = h.GetColumnIndex(clickedColumn); var item = h.GetItem(path); var column = columnIndex == -1 || columnIndex >= h.Widget.Columns.Count ? null : h.Widget.Columns[columnIndex]; var loc = h.PointFromScreen(new PointF((float)e.Event.XRoot, (float)e.Event.YRoot)); - if (ReferenceEquals(h.Tree.ExpanderColumn, clickedColumn)) + if (ReferenceEquals(h.Control.ExpanderColumn, clickedColumn)) { - var cellArea = h.Tree.GetCellArea(path, clickedColumn); + var cellArea = h.Control.GetCellArea(path, clickedColumn); if (loc.X < cellArea.Left && loc.X >= cellArea.Left - 18) // how do we get the size of the expander? { // clicked on the expander, don't fire the CellClick event @@ -299,26 +300,27 @@ public override void AttachEvent(string id) case Grid.CellEditingEvent: case Grid.CellEditedEvent: case Grid.CellFormattingEvent: + case Grid.ColumnWidthChangedEvent: SetupColumnEvents(); break; case Grid.CellClickEvent: - Tree.ButtonPressEvent += Connector.OnTreeButtonPress; + Control.ButtonPressEvent += Connector.OnTreeButtonPress; break; case Grid.CellDoubleClickEvent: - Tree.RowActivated += (sender, e) => + Control.RowActivated += (sender, e) => { var rowIndex = GetRowIndexOfPath(e.Path); - var columnIndex = GetColumnOfItem(e.Column); + var columnIndex = GetColumnIndex(e.Column); var item = GetItem(e.Path); var column = columnIndex == -1 ? null : Widget.Columns[columnIndex]; Callback.OnCellDoubleClick(Widget, new GridCellMouseEventArgs(column, rowIndex, columnIndex, item, Mouse.Buttons, Keyboard.Modifiers, PointFromScreen(Mouse.Position))); }; break; case Grid.SelectionChangedEvent: - Tree.Selection.Changed += Connector.HandleGridSelectionChanged; + Control.Selection.Changed += Connector.HandleGridSelectionChanged; break; case Grid.ColumnOrderChangedEvent: - Tree.ColumnsChanged += Connector.HandleGridColumnsChanged; + Control.ColumnsChanged += Connector.HandleGridColumnsChanged; break; default: base.AttachEvent(id); @@ -361,7 +363,7 @@ protected virtual void UpdateColumns() colHandler.Control.Reorderable = AllowColumnReordering; colHandler.SetupCell(this, this, columnIndex++, ref dataIndex); if (i == 0) - Tree.ExpanderColumn = colHandler.Control; + Control.ExpanderColumn = colHandler.Control; } foreach (var col in Widget.Columns.OrderBy(r => r.DisplayIndex)) @@ -379,9 +381,9 @@ public override void AddItem(GridColumn item) { var colhandler = (GridColumnHandler)item.Handler; #if GTKCORE - Handler.Tree.InsertColumn(colhandler.Control, (int)Handler.Tree.NColumns - 1); + Handler.Control.InsertColumn(colhandler.Control, (int)Handler.Control.NColumns - 1); #else - Handler.Tree.InsertColumn(colhandler.Control, Count); + Handler.Control.InsertColumn(colhandler.Control, Count); #endif Handler.UpdateColumns(); } @@ -389,24 +391,24 @@ public override void AddItem(GridColumn item) public override void InsertItem(int index, GridColumn item) { var colhandler = (GridColumnHandler)item.Handler; - Handler.Tree.InsertColumn(colhandler.Control, index); + Handler.Control.InsertColumn(colhandler.Control, index); Handler.UpdateColumns(); } public override void RemoveItem(int index) { var colhandler = (GridColumnHandler)Handler.Widget.Columns[index].Handler; - Handler.Tree.RemoveColumn(colhandler.Control); + Handler.Control.RemoveColumn(colhandler.Control); Handler.UpdateColumns(); } public override void RemoveAllItems() { - foreach (var col in Handler.Tree.Columns) + foreach (var col in Handler.Control.Columns) { - Handler.Tree.RemoveColumn(col); + Handler.Control.RemoveColumn(col); } - Handler.Tree.AppendColumn(Handler.spacingColumn); + Handler.Control.AppendColumn(Handler.spacingColumn); Handler.UpdateColumns(); } @@ -414,8 +416,8 @@ public override void RemoveAllItems() public bool ShowHeader { - get { return Tree.HeadersVisible; } - set { Tree.HeadersVisible = value; } + get { return Control.HeadersVisible; } + set { Control.HeadersVisible = value; } } @@ -439,9 +441,17 @@ public int NumberOfColumns public abstract object GetItem(Gtk.TreePath path); - public int GetColumnOfItem(Gtk.TreeViewColumn item) + public int GetColumnIndex(Gtk.TreeViewColumn item) { - return Widget.Columns.Select(r => r.Handler as GridColumnHandler).Select(r => r.Control).ToList().IndexOf(item); + for (int i = 0; i < Widget.Columns.Count; i++) + { + GridColumn col = Widget.Columns[i]; + if (col.Handler is GridColumnHandler handler && ReferenceEquals(handler.Control, item)) + { + return i; + } + } + return -1; } public virtual int GetRowIndexOfPath(Gtk.TreePath path) @@ -496,15 +506,15 @@ public void ColumnClicked(GridColumnHandler column) public bool AllowMultipleSelection { - get { return Tree.Selection.Mode == Gtk.SelectionMode.Multiple; } - set { Tree.Selection.Mode = value ? Gtk.SelectionMode.Multiple : Gtk.SelectionMode.Browse; } + get { return Control.Selection.Mode == Gtk.SelectionMode.Multiple; } + set { Control.Selection.Mode = value ? Gtk.SelectionMode.Multiple : Gtk.SelectionMode.Browse; } } public virtual IEnumerable SelectedRows { get { - return Tree.Selection.GetSelectedRows().Select(r => r.Indices[0]); + return Control.Selection.GetSelectedRows().Select(r => r.Indices[0]); } set { @@ -525,44 +535,44 @@ public int RowHeight public void SelectAll() { - Tree.Selection.SelectAll(); + Control.Selection.SelectAll(); } public void SelectRow(int row) { - Tree.Selection.SelectIter(GetIterAtRow(row)); + Control.Selection.SelectIter(GetIterAtRow(row)); } public void UnselectRow(int row) { - Tree.Selection.UnselectIter(GetIterAtRow(row)); + Control.Selection.UnselectIter(GetIterAtRow(row)); } public void UnselectAll() { - Tree.Selection.UnselectAll(); + Control.Selection.UnselectAll(); } public void BeginEdit(int row, int column) { - var nameColumn = Tree.Columns[column]; + var nameColumn = Control.Columns[column]; var cellRenderer = nameColumn.Cells[0]; - var path = Tree.Model.GetPath(GetIterAtRow(row)); - Tree.Model.IterNChildren(); - Tree.SetCursorOnCell(path, nameColumn, cellRenderer, true); + var path = Control.Model.GetPath(GetIterAtRow(row)); + Control.Model.IterNChildren(); + Control.SetCursorOnCell(path, nameColumn, cellRenderer, true); } public bool CommitEdit() { Gtk.TreePath path; Gtk.TreeViewColumn column; - Tree.GetCursor(out path, out column); + Control.GetCursor(out path, out column); if (path == null || column == null) return true; // This is a hack, but it works to commit editing. Is there a better way? - if (Tree.FocusChild?.HasFocus == true) - Tree.ChildFocus(Gtk.DirectionType.TabForward); + if (Control.FocusChild?.HasFocus == true) + Control.ChildFocus(Gtk.DirectionType.TabForward); return true; } @@ -570,13 +580,13 @@ public bool CancelEdit() { Gtk.TreePath path; Gtk.TreeViewColumn column; - Tree.GetCursor(out path, out column); + Control.GetCursor(out path, out column); if (path == null || column == null) return true; // This is a hack, but it works to abort editing. Is there a better way? - if (Tree.FocusChild?.HasFocus == true) - Tree.GrabFocus(); + if (Control.FocusChild?.HasFocus == true) + Control.GrabFocus(); return true; } @@ -588,15 +598,15 @@ public void OnCellFormatting(GridCellFormatEventArgs args) public void ScrollToRow(int row) { var path = this.GetPathAtRow(row); - var column = Tree.Columns.First(); - Tree.ScrollToCell(path, column, false, 0, 0); + var column = Control.Columns.First(); + Control.ScrollToCell(path, column, false, 0, 0); } public GridLines GridLines { get { - switch (Tree.EnableGridLines) + switch (Control.EnableGridLines) { case Gtk.TreeViewGridLines.None: return GridLines.None; @@ -615,16 +625,16 @@ public GridLines GridLines switch (value) { case GridLines.None: - Tree.EnableGridLines = Gtk.TreeViewGridLines.None; + Control.EnableGridLines = Gtk.TreeViewGridLines.None; break; case GridLines.Horizontal: - Tree.EnableGridLines = Gtk.TreeViewGridLines.Horizontal; + Control.EnableGridLines = Gtk.TreeViewGridLines.Horizontal; break; case GridLines.Vertical: - Tree.EnableGridLines = Gtk.TreeViewGridLines.Vertical; + Control.EnableGridLines = Gtk.TreeViewGridLines.Vertical; break; case GridLines.Both: - Tree.EnableGridLines = Gtk.TreeViewGridLines.Both; + Control.EnableGridLines = Gtk.TreeViewGridLines.Both; break; default: throw new NotSupportedException(); @@ -635,7 +645,7 @@ public GridLines GridLines public BorderType Border { get { return Widget.Properties.Get(GridHandler.Border_Key, BorderType.Bezel); } - set { Widget.Properties.Set(GridHandler.Border_Key, value, () => Control.ShadowType = value.ToGtk(), BorderType.Bezel); } + set { Widget.Properties.Set(GridHandler.Border_Key, value, () => ScrolledWindow.ShadowType = value.ToGtk(), BorderType.Bezel); } } public bool IsEditing @@ -644,7 +654,7 @@ public bool IsEditing { Gtk.TreePath path; Gtk.TreeViewColumn focus_column; - Tree.GetCursor(out path, out focus_column); + Control.GetCursor(out path, out focus_column); #if GTK2 var cells = focus_column?.CellRenderers; @@ -671,7 +681,7 @@ private void Widget_MouseDown(object sender, MouseEventArgs e) if (AllowEmptySelection) { // clicked on an empty area - if (!Tree.GetPathAtPos((int)location.X, (int)location.Y, out _, out _)) + if (!Control.GetPathAtPos((int)location.X, (int)location.Y, out _, out _)) { UnselectAll(); e.Handled = true; @@ -680,12 +690,12 @@ private void Widget_MouseDown(object sender, MouseEventArgs e) else { // cancel ctrl+clicking to remove last selected item - if (Tree.GetPathAtPos((int)location.X, (int)location.Y, out var path, out _)) + if (Control.GetPathAtPos((int)location.X, (int)location.Y, out var path, out _)) { - if (Tree.Model.GetIter(out var iter, path)) + if (Control.Model.GetIter(out var iter, path)) { - var isSelected = Tree.Selection.IterIsSelected(iter); - if (e.Modifiers == Keys.Control && isSelected && Tree.Selection.CountSelectedRows() == 1) + var isSelected = Control.Selection.IterIsSelected(iter); + if (e.Modifiers == Keys.Control && isSelected && Control.Selection.CountSelectedRows() == 1) { e.Handled = true; } @@ -697,7 +707,7 @@ private void Widget_MouseDown(object sender, MouseEventArgs e) protected void EnsureSelection() { - if (!AllowEmptySelection && Tree.Selection.CountSelectedRows() == 0) + if (!AllowEmptySelection && Control.Selection.CountSelectedRows() == 0) { SelectRow(0); } @@ -705,16 +715,21 @@ protected void EnsureSelection() public int GetColumnDisplayIndex(GridColumnHandler column) { - var columns = Tree.Columns; + var columns = Control.Columns; return Array.IndexOf(columns, column.Control); } public void SetColumnDisplayIndex(GridColumnHandler column, int index) { - var columns = Tree.Columns; + var columns = Control.Columns; var currentIndex = Array.IndexOf(columns, column.Control); if (index != currentIndex) - Tree.MoveColumnAfter(column.Control, columns[index]); + Control.MoveColumnAfter(column.Control, columns[index]); + } + + public void ColumnWidthChanged(GridColumnHandler h) + { + Callback.OnColumnWidthChanged(Widget, new GridColumnEventArgs(h.Widget)); } } } diff --git a/src/Eto.Gtk/Forms/Controls/GridViewHandler.cs b/src/Eto.Gtk/Forms/Controls/GridViewHandler.cs index f14f4ca772..f3cb82b9ae 100755 --- a/src/Eto.Gtk/Forms/Controls/GridViewHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/GridViewHandler.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Linq; using Eto.Drawing; +using System; namespace Eto.GtkSharp.Forms.Controls { @@ -32,7 +33,7 @@ public override void AddItem(object item) var iter = Handler.model.GetIterAtRow(count); var path = Handler.model.GetPathAtRow(count); Handler.model.Count++; - Handler.Tree.Model.EmitRowInserted(path, iter); + Handler.Control.Model.EmitRowInserted(path, iter); } public override void InsertItem(int index, object item) @@ -40,14 +41,14 @@ public override void InsertItem(int index, object item) var iter = Handler.model.GetIterAtRow(index); var path = Handler.model.GetPathAtRow(index); Handler.model.Count++; - Handler.Tree.Model.EmitRowInserted(path, iter); + Handler.Control.Model.EmitRowInserted(path, iter); } public override void RemoveItem(int index) { var path = Handler.model.GetPathAtRow(index); Handler.model.Count--; - Handler.Tree.Model.EmitRowDeleted(path); + Handler.Control.Model.EmitRowDeleted(path); } public override void RemoveAllItems() @@ -104,7 +105,7 @@ public override Gtk.TreePath GetPathAtRow(int row) protected override void SetSelectedRows(IEnumerable value) { - Tree.Selection.UnselectAll(); + Control.Selection.UnselectAll(); if (value != null && collection != null) { int start = -1; @@ -120,18 +121,18 @@ protected override void SetSelectedRows(IEnumerable value) else { if (start == end) - Tree.Selection.SelectIter(GetIterAtRow(start)); + Control.Selection.SelectIter(GetIterAtRow(start)); else - Tree.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); + Control.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); start = end = row; } } if (start != -1) { if (start == end) - Tree.Selection.SelectIter(GetIterAtRow(start)); + Control.Selection.SelectIter(GetIterAtRow(start)); else - Tree.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); + Control.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); } } } @@ -174,28 +175,72 @@ public void ReloadData(IEnumerable rows) { var iter = GetIterAtRow(row); var path = GetPathAtRow(row); - Tree.Model.EmitRowChanged(path, iter); + Control.Model.EmitRowChanged(path, iter); } } else UpdateModel(); } - public object GetCellAt(PointF location, out int column, out int row) + + public GridCell GetCellAt(PointF location) { - Gtk.TreePath path; - Gtk.TreeViewColumn col; - if (Tree.GetPathAtPos((int)location.X, (int)location.Y, out path, out col)) + int columnIndex; + int rowIndex; + object item; + GridCellType cellType; + int headerIndex = -1; + +#if !GTK2 + if (ShowHeader) { - column = GetColumnOfItem(col); - row = GetRowIndexOfPath(path); - return model.GetItemAtPath(path); + int headerHeight = 0; + for (int i = 0; i < Control.Columns.Length; i++) + { + Gtk.TreeViewColumn col = Control.Columns[i]; + var header = col.Button?.GetWindow(); + if (header != null) + { + var bounds = header.GetBounds(); + if (bounds.Contains(Point.Round(location))) + { + headerIndex = GetColumnIndex(col); + } + headerHeight = Math.Max(headerHeight, bounds.Height); + } + } + location.Y -= headerHeight; } - column = -1; - row = -1; - return null; - } +#endif + if (headerIndex == -1 && Control.GetPathAtPos((int)location.X, (int)location.Y, out var path, out var dataColumn)) + { + columnIndex = GetColumnIndex(dataColumn); + rowIndex = GetRowIndexOfPath(path); + item = model.GetItemAtPath(path); + if (columnIndex == -1) + cellType = GridCellType.None; + else + cellType = GridCellType.Data; + } + else if (headerIndex != -1) + { + cellType = GridCellType.ColumnHeader; + columnIndex = headerIndex; + rowIndex = -1; + item = null; + } + else + { + columnIndex = -1; + rowIndex = -1; + item = null; + cellType = GridCellType.None; + } + + var column = columnIndex != -1 ? Widget.Columns[columnIndex] : null; + return new GridCell(column, columnIndex, rowIndex, cellType, item); + } protected class GridViewConnector : GridConnector @@ -206,7 +251,7 @@ protected class GridViewConnector : GridConnector protected override DragEventArgs GetDragEventArgs(Gdk.DragContext context, PointF? location, uint time = 0, object controlObject = null) { - var t = Handler?.Tree; + var t = Handler?.Control; GridViewDragInfo dragInfo = _dragInfo; if (dragInfo == null && location != null) { @@ -237,7 +282,7 @@ public override void HandleDragMotion(object o, Gtk.DragMotionArgs args) { var path = new Gtk.TreePath(new[] { info.Index }); var pos = info.Position.ToGtk(); - h.Tree.SetDragDestRow(path, pos); + h.Control.SetDragDestRow(path, pos); } } diff --git a/src/Eto.Gtk/Forms/Controls/TreeGridViewHandler.cs b/src/Eto.Gtk/Forms/Controls/TreeGridViewHandler.cs index 99ccdbd941..f5c19ccf8e 100755 --- a/src/Eto.Gtk/Forms/Controls/TreeGridViewHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/TreeGridViewHandler.cs @@ -49,7 +49,7 @@ public void ExpandItems(ITreeGridStore store, Gtk.TreePath path) { var newpath = path.Copy(); newpath.AppendIndex(i); - Handler.Tree.ExpandToPath(newpath); + Handler.Control.ExpandToPath(newpath); ExpandItems((ITreeGridStore)item, newpath); } } @@ -73,7 +73,7 @@ public override void AddItem(ITreeGridItem item) var path = new Gtk.TreePath(); path.AppendIndex(Collection.Count); var iter = Handler.model.GetIterFromItem(item, path); - Handler.Tree.Model.EmitRowInserted(path, iter); + Handler.Control.Model.EmitRowInserted(path, iter); } public override void InsertItem(int index, ITreeGridItem item) @@ -81,14 +81,14 @@ public override void InsertItem(int index, ITreeGridItem item) var path = new Gtk.TreePath(); path.AppendIndex(index); var iter = Handler.model.GetIterFromItem(item, path); - Handler.Tree.Model.EmitRowInserted(path, iter); + Handler.Control.Model.EmitRowInserted(path, iter); } public override void RemoveItem(int index) { var path = new Gtk.TreePath(); path.AppendIndex(index); - Handler.Tree.Model.EmitRowDeleted(path); + Handler.Control.Model.EmitRowDeleted(path); } public override void RemoveAllItems() @@ -120,7 +120,7 @@ public ITreeGridItem SelectedItem if (AllowMultipleSelection) return SelectedItems.FirstOrDefault() as ITreeGridItem; Gtk.TreeIter iter; - return Tree.Selection.GetSelected(out iter) ? model.GetItemAtIter(iter) : null; + return Control.Selection.GetSelected(out iter) ? model.GetItemAtIter(iter) : null; } set { @@ -129,13 +129,13 @@ public ITreeGridItem SelectedItem var path = model.GetPathFromItem(value); if (path != null) { - Tree.ExpandToPath(path); - Tree.Selection.SelectPath(path); - Tree.ScrollToCell(path, null, false, 0, 0); + Control.ExpandToPath(path); + Control.Selection.SelectPath(path); + Control.ScrollToCell(path, null, false, 0, 0); } } else - Tree.Selection.UnselectAll(); + Control.Selection.UnselectAll(); } } @@ -157,22 +157,22 @@ public override void AttachEvent(string id) switch (id) { case TreeGridView.ActivatedEvent: - Tree.RowActivated += Connector.HandleRowActivated; + Control.RowActivated += Connector.HandleRowActivated; break; case TreeGridView.ExpandingEvent: - Tree.TestExpandRow += Connector.HandleTestExpandRow; + Control.TestExpandRow += Connector.HandleTestExpandRow; break; case TreeGridView.ExpandedEvent: - Tree.RowExpanded += Connector.HandleRowExpanded; + Control.RowExpanded += Connector.HandleRowExpanded; break; case TreeGridView.CollapsingEvent: - Tree.TestCollapseRow += Connector.HandleTestCollapseRow; + Control.TestCollapseRow += Connector.HandleTestCollapseRow; break; case TreeGridView.CollapsedEvent: - Tree.RowCollapsed += Connector.HandleRowCollapsed; + Control.RowCollapsed += Connector.HandleRowCollapsed; break; case TreeGridView.SelectedItemChangedEvent: - Tree.Selection.Changed += Connector.HandleSelectionChanged; + Control.Selection.Changed += Connector.HandleSelectionChanged; break; default: base.AttachEvent(id); @@ -247,8 +247,8 @@ public void HandleRowCollapsed(object o, Gtk.RowCollapsedArgs args) handler.SkipSelectedChange = false; if (handler.selectCollapsingItem == true) { - handler.Tree.Selection.UnselectAll(); - handler.Tree.Selection.SelectPath(args.Path); + handler.Control.Selection.UnselectAll(); + handler.Control.Selection.SelectPath(args.Path); handler.selectCollapsingItem = null; } } @@ -274,7 +274,7 @@ public void HandleRowActivated(object o, Gtk.RowActivatedArgs args) protected override DragEventArgs GetDragEventArgs(Gdk.DragContext context, PointF? location, uint time = 0, object controlObject = null) { var h = Handler; - var t = h?.Tree; + var t = h?.Control; TreeGridViewDragInfo dragInfo = _dragInfo; if (dragInfo == null && location != null) { @@ -336,7 +336,7 @@ public override void HandleDragMotion(object o, Gtk.DragMotionArgs args) } if (path.Depth > 0) - h.Tree.SetDragDestRow(path, pos); + h.Control.SetDragDestRow(path, pos); } else if (insertIndex != -1 && !ReferenceEquals(info.Parent, null) && info.Position == GridDragPosition.After) { @@ -345,7 +345,7 @@ public override void HandleDragMotion(object o, Gtk.DragMotionArgs args) return; path.AppendIndex(0); - h.Tree.SetDragDestRow(path, Gtk.TreeViewDropPosition.Before); + h.Control.SetDragDestRow(path, Gtk.TreeViewDropPosition.Before); } } @@ -397,21 +397,21 @@ public override Gtk.TreePath GetPathAtRow(int row) Gtk.TreeIter iter; Gtk.TreeIter temp; - bool valid = Tree.Model.GetIterFirst(out iter); + bool valid = Control.Model.GetIterFirst(out iter); while (valid) { // Check - path = Tree.Model.GetPath(iter); + path = Control.Model.GetPath(iter); if (model.GetRowIndexOfIter(iter) == row) return path; // Go Down - if (Tree.GetRowExpanded(path) && Tree.Model.IterChildren(out iter, iter)) + if (Control.GetRowExpanded(path) && Control.Model.IterChildren(out iter, iter)) continue; // Go Next temp = iter; - if (Tree.Model.IterNext(ref iter)) + if (Control.Model.IterNext(ref iter)) continue; else iter = temp; @@ -419,11 +419,11 @@ public override Gtk.TreePath GetPathAtRow(int row) while (valid) { // Go Up - if (Tree.Model.IterParent(out iter, iter)) + if (Control.Model.IterParent(out iter, iter)) { // Go Next temp = iter; - if (Tree.Model.IterNext(ref iter)) + if (Control.Model.IterNext(ref iter)) break; else iter = temp; @@ -434,14 +434,14 @@ public override Gtk.TreePath GetPathAtRow(int row) } // Get and return first if given row does not exist - Tree.Model.GetIterFirst(out iter); - return Tree.Model.GetPath(iter); + Control.Model.GetIterFirst(out iter); + return Control.Model.GetPath(iter); } protected override void SetSelectedRows(IEnumerable value) { - Tree.Selection.UnselectAll(); + Control.Selection.UnselectAll(); if (value != null && collection != null) { int start = -1; @@ -457,18 +457,18 @@ protected override void SetSelectedRows(IEnumerable value) else { if (start == end) - Tree.Selection.SelectIter(GetIterAtRow(start)); + Control.Selection.SelectIter(GetIterAtRow(start)); else - Tree.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); + Control.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); start = end = row; } } if (start != -1) { if (start == end) - Tree.Selection.SelectIter(GetIterAtRow(start)); + Control.Selection.SelectIter(GetIterAtRow(start)); else - Tree.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); + Control.Selection.SelectRange(GetPathAtRow(start), GetPathAtRow(end)); } } } @@ -514,12 +514,12 @@ public void ReloadData() // restore selection SkipSelectedChange = true; bool selectionChanged = false; - Tree.Selection.UnselectAll(); + Control.Selection.UnselectAll(); foreach (var item in items.OfType()) { var iter = model.GetIterFromItem(item, true); if (iter != null) - Tree.Selection.SelectIter(iter.Value); + Control.Selection.SelectIter(iter.Value); else selectionChanged = true; } @@ -533,7 +533,7 @@ public void ReloadData() public void ReloadItem(ITreeGridItem item, bool reloadChildren) { - var tree = Tree; + var tree = Control; var path = model.GetPathFromItem(item); if (path != null && path.Depth > 0 && !ReferenceEquals(item, collection.Collection)) { @@ -575,26 +575,43 @@ public void ReloadItem(ITreeGridItem item, bool reloadChildren) ReloadData(); } - public ITreeGridItem GetCellAt(PointF location, out int column) + public TreeGridCell GetCellAt(PointF location) { - Gtk.TreePath path; - Gtk.TreeViewColumn col; - if (Tree.GetPathAtPos((int)location.X, (int)location.Y, out path, out col)) + int columnIndex; + int rowIndex; + object item; + GridCellType cellType; + + var isData = Control.GetPathAtPos((int)location.X, (int)location.Y, out var path, out var col); + + if (isData) + { + columnIndex = GetColumnIndex(col); + rowIndex = GetRowIndexOfPath(path); + item = model.GetItemAtPath(path); + if (columnIndex == -1) + cellType = GridCellType.None; + else + cellType = GridCellType.Data; + } + else { - column = GetColumnOfItem(col); - return model.GetItemAtPath(path); + columnIndex = -1; + rowIndex = -1; + item = null; + cellType = GridCellType.None; } - column = -1; - return null; + var column = columnIndex != -1 ? Widget.Columns[columnIndex] : null; + return new TreeGridCell(column, columnIndex, cellType, item); } public TreeGridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as TreeGridViewDragInfo; ITreeGridItem IGtkListModelHandler.GetItem(int row) => DataStore?[row]; - public override IEnumerable SelectedRows => Tree.Selection.GetSelectedRows().Select(GetRowIndexOfPath); + public override IEnumerable SelectedRows => Control.Selection.GetSelectedRows().Select(GetRowIndexOfPath); - public IEnumerable SelectedItems => Tree.Selection.GetSelectedRows().Select(GetItem); + public IEnumerable SelectedItems => Control.Selection.GetSelectedRows().Select(GetItem); protected override bool HasRows => model.IterHasChild(Gtk.TreeIter.Zero); diff --git a/src/Eto.Gtk/Forms/GtkControl.cs b/src/Eto.Gtk/Forms/GtkControl.cs index 0914940017..89a2a6b736 100644 --- a/src/Eto.Gtk/Forms/GtkControl.cs +++ b/src/Eto.Gtk/Forms/GtkControl.cs @@ -561,7 +561,9 @@ public void HandleMotionNotifyEvent(System.Object o, Gtk.MotionNotifyEventArgs a return; var p = new PointF((float)args.Event.X, (float)args.Event.Y); + p = handler.TranslatePoint(args.Event.Window, p); Keys modifiers = args.Event.State.ToEtoKey(); + MouseButtons buttons = args.Event.State.ToEtoMouseButtons(); handler.Callback.OnMouseMove(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); @@ -574,8 +576,8 @@ public void HandleButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args) if (handler == null) return; - args.Event.ToEtoLocation(); var p = new PointF((float)args.Event.X, (float)args.Event.Y); + p = handler.TranslatePoint(args.Event.Window, p); Keys modifiers = args.Event.State.ToEtoKey(); MouseButtons buttons = args.Event.ToEtoMouseButtons(); @@ -592,6 +594,7 @@ public void HandleButtonPressEvent(object sender, Gtk.ButtonPressEventArgs args) return; var p = new PointF((float)args.Event.X, (float)args.Event.Y); + p = handler.TranslatePoint(args.Event.Window, p); Keys modifiers = args.Event.State.ToEtoKey(); MouseButtons buttons = args.Event.ToEtoMouseButtons(); var mouseArgs = new MouseEventArgs(buttons, modifiers, p); @@ -822,6 +825,25 @@ public virtual void HandleStateFlagsChangedForEnabled(object o, Gtk.StateFlagsCh #endif } + public virtual bool ShouldTranslatePoints => false; + + public virtual PointF TranslatePoint(Gdk.Window window, PointF p) + { + if (!ShouldTranslatePoints || window == null) + return p; + var eventWindow = EventControl.GetWindow(); + + if (!ReferenceEquals(window, eventWindow)) + { + // adjust point! + eventWindow.GetOrigin(out var x, out var y); + window.GetOrigin(out var ex, out var ey); + p.X += ex - x; + p.Y += ey - y; + } + return p; + } + protected virtual Gtk.Widget FontControl { get { return Control; } diff --git a/src/Eto.Gtk/GtkConversions.cs b/src/Eto.Gtk/GtkConversions.cs index e71282bb4c..87d91bf36e 100644 --- a/src/Eto.Gtk/GtkConversions.cs +++ b/src/Eto.Gtk/GtkConversions.cs @@ -565,11 +565,6 @@ public static MouseButtons ToEtoMouseButtons(this Gdk.EventButton ev) } } - public static PointF ToEtoLocation(this Gdk.EventButton e) - { - return new PointF((float)e.X, (float)e.Y); - } - public static CellStates ToEto(this Gtk.CellRendererState value) { if (value.HasFlag(Gtk.CellRendererState.Selected)) @@ -882,5 +877,15 @@ public static Pango.WrapMode ToPango(this FormattedTextWrapMode wrap) } public static Gdk.Cursor ToGdk(this Cursor cursor) => CursorHandler.GetControl(cursor); + + public static Rectangle GetBounds(this Gdk.Window window) + { + window.GetPosition(out var x, out var y); +#if GTK2 + return new Rectangle(x, y, 0, 0); // who cares, need to drop support for GTK2 anyway +#else + return new Rectangle(x, y, window.Width, window.Height); +#endif + } } } diff --git a/src/Eto.Mac/Forms/Controls/GridHandler.cs b/src/Eto.Mac/Forms/Controls/GridHandler.cs index 59678972e4..4d4b1ad0e9 100644 --- a/src/Eto.Mac/Forms/Controls/GridHandler.cs +++ b/src/Eto.Mac/Forms/Controls/GridHandler.cs @@ -34,11 +34,11 @@ static class GridHandler public static readonly object IsCancelEdit_Key = new object(); } - class EtoTableHeaderView : NSTableHeaderView + class EtoTableHeaderView : NSTableHeaderView, IMacControl { - WeakReference handler; + public IGridHandler Handler { get { return (IGridHandler)WeakHandler.Target; } set { WeakHandler = new WeakReference(value); } } - public IGridHandler Handler { get { return (IGridHandler)handler.Target; } set { handler = new WeakReference(value); } } + public WeakReference WeakHandler { get; set; } public EtoTableHeaderView() { @@ -50,20 +50,29 @@ public EtoTableHeaderView(IntPtr handle) : base(handle) public override void MouseDown(NSEvent theEvent) { - if (!Handler.Table.AllowsColumnReordering) + var h = Handler; + if (h == null) + { + base.MouseDown(theEvent); + return; + } + + var sel = MacView.selMouseDown; + if (!h.Table.AllowsColumnReordering) { var point = ConvertPointFromView(theEvent.LocationInWindow, null); var col = GetColumn(point); if (col >= 0) { - var column = Handler.Widget.Columns[(int)col]; - var rect = Handler.Table.RectForColumn(col); + var column = h.Widget.Columns[(int)col]; + var rect = h.Table.RectForColumn(col); + // don't show any feedback to user when they click if (!column.Sortable && point.X < rect.Right - 4 && point.X > rect.Left + 2) - return; + sel = IntPtr.Zero; } } - base.MouseDown(theEvent); + h.TriggerMouseDown(this, sel, theEvent); } } @@ -283,6 +292,19 @@ public override void AttachEvent(string id) { switch (id) { + case Grid.ColumnWidthChangedEvent: + // handled in delegates + break; + case Eto.Forms.Control.MouseDownEvent: + AddMethod(MacView.selMouseDown, MacView.TriggerMouseDown_Delegate, "v@:@", Control.HeaderView); + AddMethod(MacView.selRightMouseDown, MacView.TriggerMouseDown_Delegate, "v@:@", Control.HeaderView); + AddMethod(MacView.selOtherMouseDown, MacView.TriggerMouseDown_Delegate, "v@:@", Control.HeaderView); + break; + case Eto.Forms.Control.MouseUpEvent: + AddMethod(MacView.selMouseUp, MacView.TriggerMouseUp_Delegate, "v@:@", Control.HeaderView); + AddMethod(MacView.selRightMouseUp, MacView.TriggerMouseUp_Delegate, "v@:@", Control.HeaderView); + AddMethod(MacView.selOtherMouseUp, MacView.TriggerMouseUp_Delegate, "v@:@", Control.HeaderView); + break; default: base.AttachEvent(id); break; @@ -415,6 +437,7 @@ public virtual ContextMenu ContextMenu { Widget.Properties.Set(GridHandler.ContextMenu_Key, value); Control.Menu = value.ToNS(); + Control.HeaderView.Menu = value.ToNS(); } } @@ -707,19 +730,30 @@ public override void DoDragDrop(DataObject data, DragEffects allowedAction, Imag } } + static readonly object DidSetAutoSizeColumn_Key = new object(); + + internal bool DidSetAutoSizeColumn + { + get => Widget.Properties.Get(DidSetAutoSizeColumn_Key); + set => Widget.Properties.Set(DidSetAutoSizeColumn_Key, value); + } + protected void ColumnDidResize(NSNotification notification) { + var column = notification.UserInfo["NSTableColumn"] as NSTableColumn; + var colHandler = GetColumn(column); if (!IsAutoSizingColumns && Widget.Loaded && hasAutoSizedColumns == true) { // when the user resizes the column, don't autosize anymore when data/scroll changes - var column = notification.UserInfo["NSTableColumn"] as NSTableColumn; if (column != null) { - var colHandler = GetColumn(column); - colHandler.AutoSize = false; + if (!DidSetAutoSizeColumn) + colHandler.AutoSize = false; InvalidateMeasure(); } } + if (colHandler != null) + Callback.OnColumnWidthChanged(Widget, new GridColumnEventArgs(colHandler.Widget)); } protected virtual bool HandleMouseEvent(NSEvent theEvent) @@ -795,6 +829,15 @@ public void SetColumnDisplayIndex(GridColumn column, int index) Control.MoveColumn(fromIndex, index); } } + + internal int DisplayIndexToColumnIndex(int displayIndex) + { + var col = Widget.Columns.FirstOrDefault(r => r.DisplayIndex == displayIndex); + if (col == null) + return -1; + return Widget.Columns.IndexOf(col); + } + } } diff --git a/src/Eto.Mac/Forms/Controls/GridViewHandler.cs b/src/Eto.Mac/Forms/Controls/GridViewHandler.cs index 68df78bd0e..cd42092305 100644 --- a/src/Eto.Mac/Forms/Controls/GridViewHandler.cs +++ b/src/Eto.Mac/Forms/Controls/GridViewHandler.cs @@ -160,7 +160,7 @@ public NSImage DragImageForRows(NSIndexSet dragRows, NSTableColumn[] tableColumn public class EtoTableViewDataSource : NSTableViewDataSource { WeakReference handler; - + public GridViewHandler Handler { get { return (GridViewHandler)(handler != null ? handler.Target : null); } set { handler = new WeakReference(value); } } public override nint GetRowCount(NSTableView tableView) @@ -325,7 +325,7 @@ public override bool WriteRows(NSTableView tableView, NSIndexSet rowIndexes, NSP public class EtoTableDelegate : NSTableViewDelegate { WeakReference handler; - + public GridViewHandler Handler { get { return (GridViewHandler)(handler != null ? handler.Target : null); } set { handler = new WeakReference(value); } } public override bool ShouldEditTableColumn(NSTableView tableView, NSTableColumn tableColumn, nint row) @@ -337,7 +337,7 @@ public override bool ShouldEditTableColumn(NSTableView tableView, NSTableColumn Handler.SetIsEditing(true); return true; } - + NSIndexSet previouslySelected; public override void SelectionDidChange(NSNotification notification) { @@ -367,7 +367,9 @@ public override nfloat GetSizeToFitColumnWidth(NSTableView tableView, nint colum if (colHandler != null) { // turn on autosizing for this column again - Application.Instance.AsyncInvoke(() => colHandler.AutoSize = true); + colHandler.AutoSize = true; + Handler.DidSetAutoSizeColumn = true; + Application.Instance.AsyncInvoke(() => Handler.DidSetAutoSizeColumn = false); return colHandler.GetPreferredWidth(); } return 20; @@ -594,7 +596,7 @@ public IEnumerable DataStore public override object GetItem(int row) { - if (collection == null || row >= collection.Count) + if (row == -1 || collection == null || row >= collection.Count) return null; return collection.ElementAt(row); } @@ -608,13 +610,52 @@ public void ReloadData(IEnumerable rows) } } - public object GetCellAt(PointF location, out int column, out int row) + public GridCell GetCellAt(PointF location) { - location += ScrollView.ContentView.Bounds.Location.ToEto(); - var nslocation = location.ToNS(); - column = (int)Control.GetColumn(nslocation); - row = (int)Control.GetRow(nslocation); - return row >= 0 ? GetItem(row) : null; + int columnIndex; + int rowIndex; + object item; + bool isHeader; + + if (ShowHeader) + { + // check if we're over header first, as data can be under the header + var headerBounds = Control.HeaderView.Bounds.ToEto(); + var nslocation = (location + headerBounds.Location).ToNS(); + columnIndex = (int)Control.HeaderView.GetColumn(nslocation); + isHeader = columnIndex != -1 || headerBounds.Contains(nslocation.ToEto()); + } + else + { + columnIndex = -1; + isHeader = false; + } + + // not over header, check where we are in the data cells + if (!isHeader) + { + var nslocation = (location + ScrollView.ContentView.Bounds.Location.ToEto()).ToNS(); + columnIndex = (int)Control.GetColumn(nslocation); + rowIndex = (int)Control.GetRow(nslocation); + item = GetItem(rowIndex); + } + else + { + rowIndex = -1; + item = null; + } + + GridCellType cellType; + if (isHeader) + cellType = GridCellType.ColumnHeader; + else if (columnIndex != -1 && rowIndex != -1) + cellType = GridCellType.Data; + else + cellType = GridCellType.None; + + columnIndex = DisplayIndexToColumnIndex(columnIndex); + var column = columnIndex != -1 ? Widget.Columns[columnIndex] : null; + return new GridCell(column, columnIndex, rowIndex, cellType, item); } public GridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as GridViewDragInfo; @@ -656,7 +697,7 @@ void ContextMenu_Closed(object sender, EventArgs e) void ContextMenu_Opening(object sender, EventArgs e) { var row = (int)Control.ClickedRow; - if (!SelectedRows.Contains(row)) + if (row != -1 && !SelectedRows.Contains(row)) { var menu = (ContextMenu)sender; menu.Closed += ContextMenu_Closed; diff --git a/src/Eto.Mac/Forms/Controls/TreeGridViewHandler.cs b/src/Eto.Mac/Forms/Controls/TreeGridViewHandler.cs index ea6869f8d1..9035054be1 100644 --- a/src/Eto.Mac/Forms/Controls/TreeGridViewHandler.cs +++ b/src/Eto.Mac/Forms/Controls/TreeGridViewHandler.cs @@ -215,7 +215,9 @@ public override nfloat GetSizeToFitColumnWidth(NSOutlineView outlineView, nint c if (colHandler != null) { // turn on autosizing for this column again - Application.Instance.AsyncInvoke(() => colHandler.AutoSize = true); + colHandler.AutoSize = true; + Handler.DidSetAutoSizeColumn = true; + Application.Instance.AsyncInvoke(() => Handler.DidSetAutoSizeColumn = false); return colHandler.GetPreferredWidth(); } return 20; @@ -668,8 +670,8 @@ public override void AttachEvent(string id) }; Widget.MouseDoubleClick += (sender, e) => { - var cell = GetCellAt(e.Location, out var column); - if (cell != null) + var cell = GetCellAt(e.Location); + if (cell.Item != null) { Callback.OnActivated(Widget, new TreeGridViewItemEventArgs(SelectedItem)); e.Handled = true; @@ -1019,22 +1021,53 @@ void SetItemExpansion(NSObject parent) } suppressExpandCollapseEvents--; } - - public ITreeGridItem GetCellAt(PointF location, out int column) + + public TreeGridCell GetCellAt(PointF location) { - location += ScrollView.ContentView.Bounds.Location.ToEto(); - // this is the diplay index, we need the actual index - var displayColumnIndex = (int)Control.GetColumn(location.ToNS()); - var col = Widget.Columns.FirstOrDefault(r => r.DisplayIndex == displayColumnIndex); - column = col != null ? Widget.Columns.IndexOf(col) : -1; - var row = Control.GetRow(location.ToNS()); - if (row >= 0) - { - var item = Control.ItemAtRow(row) as EtoTreeItem; - if (item != null) - return item.Item; + int columnIndex; + int rowIndex; + object item; + bool isHeader; + + if (ShowHeader) + { + // check if we're over header first, as data can be under the header + var headerBounds = Control.HeaderView.Bounds.ToEto(); + var nslocation = (location + headerBounds.Location).ToNS(); + columnIndex = (int)Control.HeaderView.GetColumn(nslocation); + isHeader = columnIndex != -1 || headerBounds.Contains(nslocation.ToEto()); } - return null; + else + { + columnIndex = -1; + isHeader = false; + } + + // not over header, check where we are in the data cells + if (!isHeader) + { + var nslocation = (location + ScrollView.ContentView.Bounds.Location.ToEto()).ToNS(); + columnIndex = (int)Control.GetColumn(nslocation); + rowIndex = (int)Control.GetRow(nslocation); + item = GetItem(rowIndex); + } + else + { + rowIndex = -1; + item = null; + } + + GridCellType cellType; + if (isHeader) + cellType = GridCellType.ColumnHeader; + else if (columnIndex != -1 && rowIndex != -1) + cellType = GridCellType.Data; + else + cellType = GridCellType.None; + + columnIndex = DisplayIndexToColumnIndex(columnIndex); + var column = columnIndex != -1 ? Widget.Columns[columnIndex] : null; + return new TreeGridCell(column, columnIndex, cellType, item); } public TreeGridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as TreeGridViewDragInfo; diff --git a/src/Eto.Mac/Forms/MacView.cs b/src/Eto.Mac/Forms/MacView.cs index fe3eea4049..0de8cc27a1 100644 --- a/src/Eto.Mac/Forms/MacView.cs +++ b/src/Eto.Mac/Forms/MacView.cs @@ -1451,7 +1451,7 @@ public virtual MouseEventArgs TriggerMouseDown(NSObject obj, IntPtr sel, NSEvent { Callback.OnMouseDown(Widget, args); } - if (!args.Handled) + if (!args.Handled && sel != IntPtr.Zero) { SuppressMouseTriggerCallback = false; SuppressMouseEvents++; diff --git a/src/Eto.WinForms/Forms/Controls/GridColumnHandler.cs b/src/Eto.WinForms/Forms/Controls/GridColumnHandler.cs index 6e632fd4ab..cf03622241 100644 --- a/src/Eto.WinForms/Forms/Controls/GridColumnHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/GridColumnHandler.cs @@ -28,7 +28,11 @@ public override int GetPreferredWidth(DataGridViewAutoSizeColumnMode autoSizeCol public GridColumnHandler() { - Control = new EtoDataGridViewColumn { Handler = this }; + Control = new EtoDataGridViewColumn + { + Handler = this, + AutoSizeMode = swf.DataGridViewAutoSizeColumnMode.DisplayedCells + }; DataCell = new TextBoxCell(); Editable = false; Resizable = true; @@ -206,6 +210,11 @@ public bool MouseClick(swf.MouseEventArgs e, int rowIndex) { return GridHandler != null && GridHandler.CellMouseClick(this, e, rowIndex); } + + internal void UpdateAutoSize(bool value) + { + Widget.Properties.Set(AutoSize_Key, value, true); + } } } diff --git a/src/Eto.WinForms/Forms/Controls/GridHandler.cs b/src/Eto.WinForms/Forms/Controls/GridHandler.cs index e1edb072cd..74feec75a5 100644 --- a/src/Eto.WinForms/Forms/Controls/GridHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/GridHandler.cs @@ -218,6 +218,14 @@ public override void OnUnLoad(EventArgs e) LeakHelper.UnhookObject(Control); } + static readonly object DisableAutoSizeToggle_Key = new object(); + + int DisableAutoSizeToggle + { + get => Widget.Properties.Get(DisableAutoSizeToggle_Key); + set => Widget.Properties.Set(DisableAutoSizeToggle_Key, value); + } + bool handledAutoSize; void HandleRowPostPaint(object sender, swf.DataGridViewRowPostPaintEventArgs e) @@ -226,6 +234,7 @@ void HandleRowPostPaint(object sender, swf.DataGridViewRowPostPaintEventArgs e) return; handledAutoSize = true; + DisableAutoSizeToggle++; int colNum = 0; foreach (var col in Widget.Columns) { @@ -234,11 +243,12 @@ void HandleRowPostPaint(object sender, swf.DataGridViewRowPostPaintEventArgs e) { Control.AutoResizeColumn(colNum, colHandler.Control.InheritedAutoSizeMode); var width = colHandler.Control.Width; - colHandler.Control.AutoSizeMode = swf.DataGridViewAutoSizeColumnMode.None; colHandler.Control.Width = width; + colHandler.Control.AutoSizeMode = swf.DataGridViewAutoSizeColumnMode.None; } colNum++; } + DisableAutoSizeToggle--; } class FormattingArgs : GridCellFormatEventArgs @@ -342,12 +352,26 @@ public override void AttachEvent(string id) Control.ColumnDisplayIndexChanged += HandleColumnDisplayIndexChanged; Control.MouseUp += HandleColumnOrderChangedOnMouseUp; break; + case Grid.ColumnWidthChangedEvent: + Control.ColumnWidthChanged += HandleColumnWidthChanged; + break; default: base.AttachEvent(id); break; } } + private void HandleColumnWidthChanged(object sender, swf.DataGridViewColumnEventArgs e) + { + var column = Widget.Columns[e.Column.Index]; + if (handledAutoSize && DisableAutoSizeToggle == 0 && column.Handler is GridColumnHandler handler) + { + // turn off autosize, user (most likely?) resized this column + handler.UpdateAutoSize(false); + } + Callback.OnColumnWidthChanged(Widget, new GridColumnEventArgs(column)); + } + static readonly object ColumnOrderChanged_Key = new object(); int? ColumnOrderChangedIndex { @@ -379,6 +403,22 @@ protected override void Initialize() columns = new ColumnCollection { Handler = this }; columns.Register(Widget.Columns); Widget.MouseDown += Widget_MouseDown; + Control.ColumnDividerDoubleClick += Control_ColumnDividerDoubleClick; + } + + private void Control_ColumnDividerDoubleClick(object sender, swf.DataGridViewColumnDividerDoubleClickEventArgs e) + { + if (e.Button == swf.MouseButtons.Left) + { + DisableAutoSizeToggle++; + if (Widget.Columns[e.ColumnIndex].Handler is GridColumnHandler handler) + { + handler.UpdateAutoSize(true); + } + Control.AutoResizeColumn(e.ColumnIndex, swf.DataGridViewAutoSizeColumnMode.DisplayedCells); + DisableAutoSizeToggle--; + e.Handled = true; + } } class ColumnCollection : EnumerableChangedHandler diff --git a/src/Eto.WinForms/Forms/Controls/GridViewHandler.cs b/src/Eto.WinForms/Forms/Controls/GridViewHandler.cs index f919811c43..819af13837 100644 --- a/src/Eto.WinForms/Forms/Controls/GridViewHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/GridViewHandler.cs @@ -153,14 +153,12 @@ void IncrementRowCountBy(int increment) Control.Refresh(); // Need to refresh rather than invalidate owing to WinForms DataGridView bugs. } - public object GetCellAt(PointF location, out int column, out int row) + public GridCell GetCellAt(PointF location) { var result = Control.HitTest((int)location.X, (int)location.Y); - column = result.ColumnIndex; - row = result.RowIndex; - if (row == -1) - return null; - return GetItemAtRow(row); + var column = result.ColumnIndex != -1 ? Widget.Columns[result.ColumnIndex] : null; + var item = GetItemAtRow(result.RowIndex); + return new GridCell(column, result.ColumnIndex, result.RowIndex, result.Type.ToEto(), item); } public GridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as GridViewDragInfo; diff --git a/src/Eto.WinForms/Forms/Controls/TreeGridViewHandler.cs b/src/Eto.WinForms/Forms/Controls/TreeGridViewHandler.cs index 3fb1d93b59..eab4cf41a2 100644 --- a/src/Eto.WinForms/Forms/Controls/TreeGridViewHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/TreeGridViewHandler.cs @@ -397,13 +397,12 @@ public void ReloadItem(ITreeGridItem item, bool reloadChildren) controller.ReloadItem(item); } - public ITreeGridItem GetCellAt(PointF location, out int column) + public TreeGridCell GetCellAt(PointF location) { var result = Control.HitTest((int)location.X, (int)location.Y); - column = result.ColumnIndex; - if (result.RowIndex == -1) - return null; - return GetItemAtRow(result.RowIndex) as ITreeGridItem; + var column = result.ColumnIndex != -1 ? Widget.Columns[result.ColumnIndex] : null; + var item = GetItemAtRow(result.RowIndex); + return new TreeGridCell(column, result.ColumnIndex, result.Type.ToEto(), item); } public TreeGridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as TreeGridViewDragInfo; diff --git a/src/Eto.WinForms/WinConversions.cs b/src/Eto.WinForms/WinConversions.cs index 4d1ed5dd6b..4dadbb4ffd 100644 --- a/src/Eto.WinForms/WinConversions.cs +++ b/src/Eto.WinForms/WinConversions.cs @@ -892,6 +892,19 @@ public static Image ToEto(this sd.Image image) return new Bitmap(new BitmapHandler(bitmap)); throw new NotSupportedException(); } + + public static GridCellType ToEto(this swf.DataGridViewHitTestType type) + { + switch (type) + { + case swf.DataGridViewHitTestType.ColumnHeader: + return GridCellType.ColumnHeader; + case swf.DataGridViewHitTestType.Cell: + return GridCellType.Data; + default: + return GridCellType.None; + } + } } } diff --git a/src/Eto.Wpf/Forms/Cells/CellHandler.cs b/src/Eto.Wpf/Forms/Cells/CellHandler.cs index b6fd49f177..07e2da14f0 100755 --- a/src/Eto.Wpf/Forms/Cells/CellHandler.cs +++ b/src/Eto.Wpf/Forms/Cells/CellHandler.cs @@ -32,6 +32,8 @@ public abstract class CellHandler : WidgetHandler< { public ICellContainerHandler ContainerHandler { get; set; } + public Controls.IGridHandler GridHandler => ContainerHandler?.Grid?.Handler as Controls.IGridHandler; + public static bool IsControlInitialized(sw.DependencyObject obj) => (bool)obj.GetValue(CellProperties.ControlInitializedProperty); public static void SetControlInitialized(sw.DependencyObject obj, bool value) => obj.SetValue(CellProperties.ControlInitializedProperty, value); public static bool IsControlEditInitialized(sw.DependencyObject obj) => (bool)obj.GetValue(CellProperties.ControlEditInitializedProperty); @@ -72,5 +74,16 @@ public virtual void OnMouseDown(GridCellMouseEventArgs args, sw.DependencyObject public virtual void OnMouseUp(GridCellMouseEventArgs args, sw.DependencyObject hitTestResult, swc.DataGridCell cell) { } + + protected override void Initialize() + { + base.Initialize(); + Widget.Properties.Set(swc.DataGridColumn.ActualWidthProperty, PropertyChangeNotifier.Register(swc.DataGridColumn.ActualWidthProperty, HandleWidthChanged, Control)); + } + + private void HandleWidthChanged(object sender, sw.DependencyPropertyChangedEventArgs e) + { + GridHandler?.OnColumnWidthChanged(ContainerHandler as Controls.GridColumnHandler); + } } } \ No newline at end of file diff --git a/src/Eto.Wpf/Forms/Controls/GridColumnHandler.cs b/src/Eto.Wpf/Forms/Controls/GridColumnHandler.cs index 9d9d1a7919..40dac82244 100755 --- a/src/Eto.Wpf/Forms/Controls/GridColumnHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/GridColumnHandler.cs @@ -17,6 +17,7 @@ public interface IGridHandler sw.FrameworkElement SetupCell(IGridColumnHandler column, sw.FrameworkElement defaultContent, swc.DataGridCell cell); void FormatCell(IGridColumnHandler column, ICellHandler cell, sw.FrameworkElement element, swc.DataGridCell gridcell, object dataItem); void CellEdited(int row, swc.DataGridColumn dataGridColumn, object dataItem); + void OnColumnWidthChanged(GridColumnHandler gridColumnHandler); } public interface IGridColumnHandler : GridColumn.IHandler @@ -43,6 +44,11 @@ protected override void Initialize() Sortable = false; } + private void HandleWidthChanged(object sender, sw.DependencyPropertyChangedEventArgs e) + { + GridHandler.OnColumnWidthChanged(this); + } + public string HeaderText { get { return Control.Header as string; } diff --git a/src/Eto.Wpf/Forms/Controls/GridHandler.cs b/src/Eto.Wpf/Forms/Controls/GridHandler.cs index f148e56c09..728bf6f586 100755 --- a/src/Eto.Wpf/Forms/Controls/GridHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/GridHandler.cs @@ -216,6 +216,8 @@ public override void AttachEvent(string id) case Grid.ColumnOrderChangedEvent: Control.ColumnReordered += HandleColumnReordered; break; + case Grid.ColumnWidthChangedEvent: + break; default: base.AttachEvent(id); break; @@ -232,11 +234,10 @@ private void HandleColumnReordered(object sender, swc.DataGridColumnEventArgs e) GridCellMouseEventArgs CreateCellMouseArgs(object originalSource, swi.MouseButtonEventArgs ea, out bool isValid) { - swc.DataGridCell cell; - var row = GetRowOfElement(originalSource, out cell, out isValid); + var row = GetRowOfElement(originalSource, out var dataGridColumn, out isValid); int rowIndex = row?.GetIndex() ?? -1; - var column = GetColumn(cell?.Column); + var column = GetColumn(dataGridColumn); var columnIndex = column != null ? Widget.Columns.IndexOf(column) : -1; var item = row?.Item; @@ -247,27 +248,36 @@ GridCellMouseEventArgs CreateCellMouseArgs(object originalSource, swi.MouseButto return new GridCellMouseEventArgs(column, rowIndex, columnIndex, item, buttons, modifiers, location); } - swc.DataGridRow GetRowOfElement(object source, out swc.DataGridCell cell, out bool isValid) + swc.DataGridRow GetRowOfElement(object source, out swc.DataGridColumn column, out bool isValid) { // when clicking on labels, etc this will be a content element while (source is sw.FrameworkContentElement) source = ((sw.FrameworkContentElement)source).Parent; // VisualTreeHelper will throw if not a Visual, we can return null here + column = null; var dep = source as swm.Visual; - while (dep != null && !(dep is swc.DataGridCell)) + while (dep != null) { + if (dep is swc.DataGridCell cell) + { + column = cell.Column; + break; + } + if (dep is swc.Primitives.DataGridColumnHeader header) + { + column = header.Column; + break; + } if (ReferenceEquals(dep, Control)) { // found the grid, it's a valid click.. but no row or cell. isValid = true; - cell = null; return null; } dep = swm.VisualTreeHelper.GetParent(dep) as swm.Visual; } - cell = dep as swc.DataGridCell; while (dep != null && !(dep is swc.DataGridRow)) dep = swm.VisualTreeHelper.GetParent(dep) as swm.Visual; @@ -954,6 +964,47 @@ protected void EnsureSelection() } } + public void OnColumnWidthChanged(GridColumnHandler gridColumnHandler) + { + Callback.OnColumnWidthChanged(Widget, new GridColumnEventArgs(gridColumnHandler.Widget)); + } + + internal (swc.DataGridColumn Column, swc.DataGridRow Row, bool IsHeader) GetCellInfoForElement(sw.DependencyObject element) + { + swc.DataGridColumn column = null; + bool isHeader = false; + var cell = element.GetVisualParent(); + if (cell != null) + column = cell.Column; + else + { + var header = element.GetVisualParent(); + if (header != null) + { + column = header.Column; + isHeader = true; + } + } + var dataGridRow = element.GetVisualParent(); + return (column, dataGridRow, isHeader); + } + + internal (GridColumn Column, int ColumnIndex, int RowIndex, GridCellType CellType, object Item) GetCellInfo(PointF location) + { + var hitTestResult = swm.VisualTreeHelper.HitTest(Control, location.ToWpf())?.VisualHit; + var cellInfo = GetCellInfoForElement(hitTestResult); + var columnIndex = cellInfo.Column != null ? Control.Columns.IndexOf(cellInfo.Column) : -1; + var column = columnIndex != -1 ? Widget.Columns[columnIndex] : null; + int rowIndex = -1; + object item = null; + if (cellInfo.Row != null) + { + rowIndex = cellInfo.Row.GetIndex(); + item = GetItemAtRow(rowIndex); + } + var cellType = cellInfo.IsHeader ? GridCellType.ColumnHeader : rowIndex != -1 && columnIndex != -1 ? GridCellType.Data : GridCellType.None; + return (column, columnIndex, rowIndex, cellType, item); + } } } diff --git a/src/Eto.Wpf/Forms/Controls/GridViewHandler.cs b/src/Eto.Wpf/Forms/Controls/GridViewHandler.cs index 44d1995bea..bc5d0be0a5 100644 --- a/src/Eto.Wpf/Forms/Controls/GridViewHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/GridViewHandler.cs @@ -35,27 +35,11 @@ public override void AttachEvent(string id) break; } } - - public object GetCellAt(PointF location, out int column, out int row) + + public GridCell GetCellAt(PointF location) { - var hitTestResult = swm.VisualTreeHelper.HitTest(Control, location.ToWpf())?.VisualHit; - if (hitTestResult == null) - { - column = -1; - row = -1; - return null; - } - var dataGridCell = hitTestResult.GetVisualParent(); - column = dataGridCell?.Column != null ? Control.Columns.IndexOf(dataGridCell.Column) : -1; - - var dataGridRow = hitTestResult.GetVisualParent(); - if (dataGridRow != null) - { - row = dataGridRow.GetIndex(); - return GetItemAtRow(row); - } - row = -1; - return null; + var info = GetCellInfo(location); + return new GridCell(info.Column, info.ColumnIndex, info.RowIndex, info.CellType, info.Item); } diff --git a/src/Eto.Wpf/Forms/Controls/TreeGridViewHandler.cs b/src/Eto.Wpf/Forms/Controls/TreeGridViewHandler.cs index 87ffcdd89c..9f79566745 100755 --- a/src/Eto.Wpf/Forms/Controls/TreeGridViewHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/TreeGridViewHandler.cs @@ -227,24 +227,10 @@ public void ReloadItem(ITreeGridItem item, bool reloadChildren) controller.ReloadData(); } - public ITreeGridItem GetCellAt(PointF location, out int column) + public TreeGridCell GetCellAt(PointF location) { - var hitTestResult = swm.VisualTreeHelper.HitTest(Control, location.ToWpf())?.VisualHit; - if (hitTestResult == null) - { - column = -1; - return null; - } - var dataGridCell = hitTestResult.GetVisualParent(); - column = dataGridCell?.Column != null ? Control.Columns.IndexOf(dataGridCell.Column) : -1; - - var dataGridRow = hitTestResult.GetVisualParent(); - if (dataGridRow != null) - { - int row = dataGridRow.GetIndex(); - return GetItemAtRow(row) as ITreeGridItem; - } - return null; + var info = GetCellInfo(location); + return new TreeGridCell(info.Column, info.ColumnIndex, info.CellType, info.Item); } public TreeGridViewDragInfo GetDragInfo(DragEventArgs args) => args.ControlObject as TreeGridViewDragInfo; diff --git a/src/Eto/Forms/Controls/Grid.cs b/src/Eto/Forms/Controls/Grid.cs index d38db423fc..42ea8d7128 100644 --- a/src/Eto/Forms/Controls/Grid.cs +++ b/src/Eto/Forms/Controls/Grid.cs @@ -384,6 +384,29 @@ protected virtual void OnColumnOrderChanged(GridColumnEventArgs e) Properties.TriggerEvent(ColumnOrderChangedEvent, this, e); } + /// + /// Event identifier for handlers when attaching the event + /// + public const string ColumnWidthChangedEvent = "Grid.ColumnWidthChanged"; + + /// + /// Event to handle when a column width has been changed. + /// + public event EventHandler ColumnWidthChanged + { + add { Properties.AddHandlerEvent(ColumnWidthChangedEvent, value); } + remove { Properties.RemoveEvent(ColumnWidthChangedEvent, value); } + } + + /// + /// Raises the event + /// + /// Event arguments + protected virtual void OnColumnWidthChanged(GridColumnEventArgs e) + { + Properties.TriggerEvent(ColumnWidthChangedEvent, this, e); + } + #endregion static Grid() @@ -396,6 +419,7 @@ static Grid() EventLookup.Register(c => c.OnSelectionChanged(null), Grid.SelectionChangedEvent); EventLookup.Register(c => c.OnColumnHeaderClick(null), Grid.ColumnHeaderClickEvent); EventLookup.Register(c => c.OnColumnOrderChanged(null), Grid.ColumnOrderChangedEvent); + EventLookup.Register(c => c.OnColumnWidthChanged(null), Grid.ColumnWidthChangedEvent); } /// @@ -695,6 +719,11 @@ protected override object GetCallback() /// Raises the column display index changed event. /// void OnColumnOrderChanged(Grid widget, GridColumnEventArgs e); + + /// + /// Raises the column width changed event. + /// + void OnColumnWidthChanged(Grid widget, GridColumnEventArgs e); } /// @@ -776,6 +805,15 @@ public void OnColumnOrderChanged(Grid widget, GridColumnEventArgs e) using (widget.Platform.Context) widget.OnColumnOrderChanged(e); } + + /// + /// Raises the column width changed event. + /// + public void OnColumnWidthChanged(Grid widget, GridColumnEventArgs e) + { + using (widget.Platform.Context) + widget.OnColumnWidthChanged(e); + } } #region Handler diff --git a/src/Eto/Forms/Controls/GridView.cs b/src/Eto/Forms/Controls/GridView.cs index 3f073c4619..72c934dbe7 100644 --- a/src/Eto/Forms/Controls/GridView.cs +++ b/src/Eto/Forms/Controls/GridView.cs @@ -57,35 +57,50 @@ public GridViewCellEventArgs(GridColumn gridColumn, int row, int column, object public class GridCell { /// - /// Gets the item associated with the row of the cell. + /// Gets the item associated with the row of the cell, or null if there is no row. /// /// The row item. public object Item { get; } /// - /// Gets the index of the row. + /// Gets the index of the row, or -1 if there is no row at this location. /// /// The index of the row. public int RowIndex { get; } /// - /// Gets the column of the cell, or null + /// Gets the column of the cell, or null if there is no column at the specified location. /// /// The column. public GridColumn Column { get; } /// - /// Gets the index of the column. + /// Gets the index of the column, or -1 if there is no column at the specified location. /// /// The index of the column. public int ColumnIndex { get; } + + /// + /// Gets the type of the cell + /// + /// Type of the cell + public GridCellType Type { get; } - internal GridCell(object item, GridColumn column, int columnIndex, int rowIndex) + /// + /// Initializes a new instance of the GridCell class + /// + /// Column instance, or null if no column (e.g. empty area to right of columns) + /// Index of the column for this cell, or -1 if no column (e.g. empty area to right of columns) + /// Index of the row at the cell, or -1 if no row (e.g. header or empty area) + /// Type of the cell, e.g. header, data, none + /// Item instance for this row + public GridCell(GridColumn column, int columnIndex, int rowIndex, GridCellType type, object item) { Item = item; Column = column; ColumnIndex = columnIndex; RowIndex = rowIndex; + Type = type; } } @@ -399,20 +414,14 @@ public void Dispose() } /// - /// Gets the node at a specified location from the origin of the control + /// Gets the cell information at a specified location from the origin of the control /// /// - /// Useful for determining which node is under the mouse cursor. + /// Useful for determining which cell is under the mouse cursor. /// - /// The item from the data store that is displayed at the specified location + /// The cell information at the specified location /// Point to find the node - public GridCell GetCellAt(PointF location) - { - int column; - int row; - var item = Handler.GetCellAt(location, out column, out row); - return new GridCell(item, column >= 0 ? Columns[column] : null, column, row); - } + public GridCell GetCellAt(PointF location) => Handler.GetCellAt(location); /// /// Gets a new selection preserver instance for the grid. @@ -521,16 +530,14 @@ protected override object GetCallback() void ReloadData(IEnumerable rows); /// - /// Gets the node at a specified point from the origin of the control + /// Gets the cell information at a specified location from the origin of the control /// /// - /// Useful for determining which node is under the mouse cursor. + /// Useful for determining which cell is under the mouse cursor. /// - /// The item from the data store that is displayed at the specified location + /// The cell information at the specified location /// Point to find the node - /// Row under the specified location - /// Column under the specified location - object GetCellAt(PointF location, out int column, out int row); + GridCell GetCellAt(PointF location); /// /// Gets the grid drag info for the specified DragEventArgs. diff --git a/src/Eto/Forms/Controls/TreeGridView.cs b/src/Eto/Forms/Controls/TreeGridView.cs index abfe82fb16..05f1b2ed8c 100644 --- a/src/Eto/Forms/Controls/TreeGridView.cs +++ b/src/Eto/Forms/Controls/TreeGridView.cs @@ -55,6 +55,25 @@ public TreeGridViewItemCancelEventArgs(ITreeGridItem item) this.Item = item; } } + + /// + /// Enumeration of the types of grid cells + /// + public enum GridCellType + { + /// + /// Empty part of the grid + /// + None, + /// + /// Cell containing the data, that is when the row and column are a valid value. + /// + Data, + /// + /// Column header + /// + ColumnHeader + } /// /// Information of a cell in the @@ -62,28 +81,42 @@ public TreeGridViewItemCancelEventArgs(ITreeGridItem item) public class TreeGridCell { /// - /// Gets the item associated with the row of the cell. + /// Gets the item associated with the row of the cell, or null if there is no item. /// /// The row item. public object Item { get; } /// - /// Gets the column of the cell, or null + /// Gets the column of the cell, or null if there is no column at the specified location. /// /// The column. public GridColumn Column { get; } /// - /// Gets the index of the column. + /// Gets the index of the column, or -1 if there is no column at the specified location. /// /// The index of the column. public int ColumnIndex { get; } + + /// + /// Gets the type of the cell + /// + /// Type of the cell + public GridCellType Type { get; } - internal TreeGridCell(object item, GridColumn column, int columnIndex) + /// + /// Initializes a new instance of the TreeGridCell class + /// + /// Column instance, or null if no column (e.g. empty area to right of columns) + /// Index of the column for this cell, or -1 if no column (e.g. empty area to right of columns) + /// Type of the cell, e.g. header, data, none + /// Item instance for this row + public TreeGridCell(GridColumn column, int columnIndex, GridCellType type, object item) { Item = item; Column = column; ColumnIndex = columnIndex; + Type = type; } } @@ -567,19 +600,14 @@ public void ReloadData() public void ReloadItem(ITreeGridItem item, bool reloadChildren) => Handler.ReloadItem(item, reloadChildren); /// - /// Gets the node at a specified location from the origin of the control + /// Gets the cell information at a specified location from the origin of the control /// /// /// Useful for determining which node is under the mouse cursor. /// - /// The item from the data store that is displayed at the specified location + /// The cell information at the specified location /// Point to find the node - public TreeGridCell GetCellAt(PointF location) - { - int column; - var item = Handler.GetCellAt(location, out column); - return new TreeGridCell(item, column >= 0 ? Columns[column] : null, column); - } + public TreeGridCell GetCellAt(PointF location) => Handler.GetCellAt(location); /// /// Gets the tree grid drag info for the specified DragEventArgs. @@ -737,12 +765,11 @@ public void OnSelectedItemChanged(TreeGridView widget, EventArgs e) void ReloadItem(ITreeGridItem item, bool reloadChildren); /// - /// Gets the item and column of a location in the control. + /// Gets the cell information at a specified location from the origin of the control /// - /// The item from the data store that is displayed at the specified location + /// The cell information at the specified location /// Point to find the node - /// Column at the location, or -1 if no column (e.g. at the end of the row) - ITreeGridItem GetCellAt(PointF location, out int column); + TreeGridCell GetCellAt(PointF location); /// /// Gets the tree grid drag info for the specified DragEventArgs. diff --git a/test/Eto.Test/Sections/Controls/GridViewSection.cs b/test/Eto.Test/Sections/Controls/GridViewSection.cs index 726e8942f2..4d99670acd 100644 --- a/test/Eto.Test/Sections/Controls/GridViewSection.cs +++ b/test/Eto.Test/Sections/Controls/GridViewSection.cs @@ -18,7 +18,7 @@ public class GridViewSection : GridViewSection protected override string GetCellInfo(GridView grid, PointF location) { var cell = grid.GetCellAt(location); - return $"Row: {cell?.RowIndex}, Column: {cell?.ColumnIndex} ({cell?.Column?.HeaderText}), Item: {cell?.Item}"; + return $"Row: {cell?.RowIndex}, Column: {cell?.ColumnIndex} ({cell?.Column?.HeaderText}), Type: {cell?.Type}, Item: {cell?.Item}"; } protected override int GetRowCount(GridView grid) => ((ICollection)grid.DataStore).Count; @@ -135,7 +135,7 @@ public GridViewSection() { Styles.Add