diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index c2a9faf70c..7782165263 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -267,7 +267,8 @@ AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { - virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; + virtual HRESULT SetEnabled (bool enable) = 0; + virtual HRESULT SetParent (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; @@ -309,6 +310,8 @@ AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents virtual bool Closing () = 0; virtual void WindowStateChanged (AvnWindowState state) = 0; + + virtual void GotInputWhenDisabled () = 0; }; AVNCOM(IAvnMacOptions, 07) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 163db36800..ca60914526 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,8 +19,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(bool) isModal; --(void) setModal: (bool) isModal; +-(void) setEnabled: (bool) enable; -(void) showAppMenuOnly; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 06b0c50456..fed2176580 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -497,12 +497,7 @@ private: virtual HRESULT Show () override { @autoreleasepool - { - if([Window parentWindow] != nil) - [[Window parentWindow] removeChildWindow:Window]; - - [Window setModal:FALSE]; - + { WindowBaseImpl::Show(); HideOrShowTrafficLights(); @@ -511,7 +506,16 @@ private: } } - virtual HRESULT ShowDialog (IAvnWindow* parent) override + virtual HRESULT SetEnabled (bool enable) override + { + @autoreleasepool + { + [Window setEnabled:enable]; + return S_OK; + } + } + + virtual HRESULT SetParent (IAvnWindow* parent) override { @autoreleasepool { @@ -522,12 +526,9 @@ private: if(cparent == nullptr) return E_INVALIDARG; - [Window setModal:TRUE]; - [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; - WindowBaseImpl::Show(); - HideOrShowTrafficLights(); + UpdateStyle(); return S_OK; } @@ -883,15 +884,15 @@ protected: switch (_decorations) { case SystemDecorationsNone: - s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; break; case SystemDecorationsFull: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskBorderless; if(_canResize) { @@ -900,6 +901,10 @@ protected: break; } + if([Window parentWindow] == nullptr) + { + s |= NSWindowStyleMaskMiniaturizable; + } return s; } }; @@ -1081,15 +1086,28 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (bool) ignoreUserInput { auto parentWindow = objc_cast([self window]); + if(parentWindow == nil || ![parentWindow shouldTryToHandleEvents]) + { + auto window = dynamic_cast(_parent.getRaw()); + + if(window != nullptr) + { + window->WindowEvents->GotInputWhenDisabled(); + } + return TRUE; + } + return FALSE; } - (void)mouseEvent:(NSEvent *)event withType:(AvnRawMouseEventType) type { if([self ignoreUserInput]) + { return; + } [self becomeFirstResponder]; auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; @@ -1234,7 +1252,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type { if([self ignoreUserInput]) + { return; + } + auto key = s_KeyMap[[event keyCode]]; auto timestamp = [event timestamp] * 1000; @@ -1416,7 +1437,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - bool _isModal; + bool _isEnabled; AvnMenu* _menu; double _lastScaling; } @@ -1538,6 +1559,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent = parent; [self setDelegate:self]; _closed = false; + _isEnabled = true; _lastScaling = [self backingScaleFactor]; [self setOpaque:NO]; @@ -1604,28 +1626,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent -(bool)shouldTryToHandleEvents { - for(NSWindow* uch in [self childWindows]) - { - auto ch = objc_cast(uch); - if(ch == nil) - continue; - - if(![ch isModal]) - continue; - - return FALSE; - } - return TRUE; -} - --(bool) isModal -{ - return _isModal; + return _isEnabled; } --(void) setModal: (bool) isModal +-(void) setEnabled:(bool)enable { - _isModal = isModal; + _isEnabled = enable; } -(void)makeKeyWindow diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 5fa0ec57b5..cf31d30332 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -26,9 +26,21 @@ namespace Avalonia.Platform void SetTitle(string title); /// - /// Shows the window as a dialog. + /// Sets the parent of the window. /// - void ShowDialog(IWindowImpl parent); + /// The parent . + void SetParent(IWindowImpl parent); + + /// + /// Disables the window for example when a modal dialog is open. + /// + /// true if the window is enabled, or false if it is disabled. + void SetEnabled(bool enable); + + /// + /// Called when a disabled window received input. Can be used to activate child windows. + /// + Action GotInputWhenDisabled { get; set; } /// /// Enables or disables system window decorations (title bar, buttons, etc) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7dacf4b2af..194ebeb999 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; @@ -68,6 +69,8 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { + private List _children = new List(); + /// /// Defines the property. /// @@ -131,7 +134,7 @@ namespace Avalonia.Controls /// public static readonly RoutedEvent WindowClosedEvent = RoutedEvent.Register("WindowClosed", RoutingStrategies.Direct); - + /// /// Routed event that can be used for global tracking of opening windows /// @@ -183,6 +186,7 @@ namespace Avalonia.Controls : base(impl) { impl.Closing = HandleClosing; + impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); @@ -302,7 +306,7 @@ namespace Avalonia.Controls PlatformImpl?.Move(value); } } - + /// /// Starts moving a window with left button being held. Should be called from left mouse button press event handler /// @@ -323,7 +327,7 @@ namespace Avalonia.Controls /// /// Fired before a window is closed. /// - public event EventHandler Closing; + public event EventHandler Closing; /// /// Closes the window. @@ -365,19 +369,59 @@ namespace Avalonia.Controls { if (close) { - PlatformImpl?.Dispose(); + CloseInternal(); } } } + private void CloseInternal() + { + foreach (var child in _children.ToList()) + { + // if we HandleClosing() before then there will be no children. + child.CloseInternal(); + } + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; + + PlatformImpl?.Dispose(); + } + /// /// Handles a closing notification from . /// protected virtual bool HandleClosing() { - var args = new CancelEventArgs(); - OnClosing(args); - return args.Cancel; + bool canClose = true; + + foreach (var child in _children.ToList()) + { + if (!child.HandleClosing()) + { + child.CloseInternal(); + } + else + { + canClose = false; + } + } + + if (canClose) + { + var args = new CancelEventArgs(); + OnClosing(args); + + return args.Cancel; + } + else + { + return !canClose; + } } protected virtual void HandleWindowStateChanged(WindowState state) @@ -407,6 +451,14 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { Renderer?.Stop(); + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; + PlatformImpl?.Hide(); } @@ -519,7 +571,10 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - PlatformImpl?.ShowDialog(owner.PlatformImpl); + PlatformImpl.SetParent(owner.PlatformImpl); + Owner = owner; + owner.AddChild(this); + PlatformImpl?.Show(); Renderer?.Start(); @@ -541,6 +596,37 @@ namespace Avalonia.Controls return result.Task; } + private void UpdateEnabled() + { + PlatformImpl.SetEnabled(_children.Count == 0); + } + + private void AddChild(Window window) + { + _children.Add(window); + UpdateEnabled(); + } + + private void RemoveChild(Window window) + { + _children.Remove(window); + UpdateEnabled(); + } + + private void OnGotInputWhenDisabled() + { + var firstChild = _children.FirstOrDefault(); + + if (firstChild != null) + { + firstChild.OnGotInputWhenDisabled(); + } + else + { + Activate(); + } + } + private void SetWindowStartupLocation(IWindowBaseImpl owner = null) { var scaling = owner?.Scaling ?? PlatformImpl?.Scaling ?? 1; @@ -631,6 +717,13 @@ namespace Avalonia.Controls RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); base.HandleClosed(); + + if (Owner is Window owner) + { + owner.RemoveChild(this); + } + + Owner = null; } /// diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index bb63d1b353..afc01db506 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls public WindowBase Owner { get { return _owner; } - set { SetAndRaise(OwnerProperty, ref _owner, value); } + protected set { SetAndRaise(OwnerProperty, ref _owner, value); } } /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 7480b3519c..844489ef97 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -83,6 +83,7 @@ namespace Avalonia.DesignerSupport.Remote } public IScreenImpl Screen { get; } = new ScreenStub(); + public Action GotInputWhenDisabled { get; set; } public void Activate() { @@ -115,5 +116,13 @@ namespace Avalonia.DesignerSupport.Remote public void SetTopmost(bool value) { } + + public void SetParent(IWindowImpl parent) + { + } + + public void SetEnabled(bool enable) + { + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 82950ce53b..484cf3bc97 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -130,7 +130,17 @@ namespace Avalonia.DesignerSupport.Remote { } + public void SetParent(IWindowImpl parent) + { + } + + public void SetEnabled(bool enable) + { + } + public IPopupPositioner PopupPositioner { get; } + + public Action GotInputWhenDisabled { get; set; } } class ClipboardStub : IClipboard diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index b7eec51c85..e4ee293757 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -43,6 +43,11 @@ namespace Avalonia.Native _parent = parent; } + public void GotInputWhenDisabled() + { + // NOP on Popup + } + bool IAvnWindowEvents.Closing() { return true; diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 03a7b4fceb..e91445000a 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -40,7 +40,7 @@ namespace Avalonia.Native bool IAvnWindowEvents.Closing() { - if(_parent.Closing != null) + if (_parent.Closing != null) { return _parent.Closing(); } @@ -52,15 +52,15 @@ namespace Avalonia.Native { _parent.WindowStateChanged?.Invoke((WindowState)state); } + + void IAvnWindowEvents.GotInputWhenDisabled() + { + _parent.GotInputWhenDisabled?.Invoke(); + } } public IAvnWindow Native => _native; - public void ShowDialog(IWindowImpl window) - { - _native.ShowDialog(((WindowImpl)window).Native); - } - public void CanResize(bool value) { _native.CanResize = value; @@ -71,7 +71,7 @@ namespace Avalonia.Native _native.Decorations = (Interop.SystemDecorations)enabled; } - public void SetTitleBarColor (Avalonia.Media.Color color) + public void SetTitleBarColor(Avalonia.Media.Color color) { _native.SetTitleBarColor(new AvnColor { Alpha = color.A, Red = color.R, Green = color.G, Blue = color.B }); } @@ -116,5 +116,17 @@ namespace Avalonia.Native public override IPopupImpl CreatePopup() => _opts.OverlayPopups ? null : new PopupImpl(_factory, _opts, _glFeature, this); + + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) + { + _native.SetParent(((WindowImpl)parent).Native); + } + + public void SetEnabled(bool enable) + { + _native.SetEnabled(enable); + } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 1b3d1a7dda..643b037b3f 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -41,9 +41,9 @@ namespace Avalonia.X11 private IntPtr _renderHandle; private bool _mapped; private bool _wasMappedAtLeastOnce = false; - private HashSet _transientChildren = new HashSet(); - private X11Window _transientParent; private double? _scalingOverride; + private bool _disabled; + public object SyncRoot { get; } = new object(); class InputEventContainer @@ -746,7 +746,6 @@ namespace Avalonia.X11 void Cleanup() { - SetTransientParent(null, false); if (_xic != IntPtr.Zero) { XDestroyIC(_xic); @@ -773,38 +772,24 @@ namespace Avalonia.X11 bool ActivateTransientChildIfNeeded() { - if (_transientChildren.Count == 0) - return false; - var child = _transientChildren.First(); - if (!child.ActivateTransientChildIfNeeded()) - child.Activate(); - return true; - } - - void SetTransientParent(X11Window window, bool informServer = true) - { - _transientParent?._transientChildren.Remove(this); - _transientParent = window; - _transientParent?._transientChildren.Add(this); - if (informServer) - SetTransientForHint(_transientParent?._handle); + if (_disabled) + { + GotInputWhenDisabled?.Invoke(); + return true; + } + + return false; } - void SetTransientForHint(IntPtr? parent) + public void SetParent(IWindowImpl parent) { - if (parent == null || parent == IntPtr.Zero) + if (parent == null || parent.Handle == null || parent.Handle.Handle == IntPtr.Zero) XDeleteProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_TRANSIENT_FOR); else - XSetTransientForHint(_x11.Display, _handle, parent.Value); + XSetTransientForHint(_x11.Display, _handle, parent.Handle.Handle); } public void Show() - { - SetTransientParent(null); - ShowCore(); - } - - void ShowCore() { _wasMappedAtLeastOnce = true; XMapWindow(_x11.Display, _handle); @@ -813,7 +798,6 @@ namespace Avalonia.X11 public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( @@ -1034,13 +1018,14 @@ namespace Avalonia.X11 { ChangeWMAtoms(value, _x11.Atoms._NET_WM_STATE_ABOVE); } - - public void ShowDialog(IWindowImpl parent) + + public void SetEnabled(bool enable) { - SetTransientParent((X11Window)parent); - ShowCore(); + _disabled = !enable; } + public Action GotInputWhenDisabled { get; set; } + public void SetIcon(IWindowIconImpl icon) { if (icon != null) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 3ea8c1e48f..138553b962 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -77,12 +77,6 @@ namespace Avalonia.Win32 s_instances.Remove(this); Closed?.Invoke(); - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - } - _mouseDevice.Dispose(); _touchDevice?.Dispose(); //Free other resources diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 608563cb67..cc4c12ec3c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -45,7 +45,6 @@ namespace Avalonia.Win32 #endif private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); - private readonly List _disabledBy; private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; private readonly ManagedDeferredRendererLock _rendererLock; @@ -70,7 +69,6 @@ namespace Avalonia.Win32 public WindowImpl() { - _disabledBy = new List(); _touchDevice = new TouchDevice(); _mouseDevice = new WindowsMouseDevice(); @@ -342,31 +340,25 @@ namespace Avalonia.Win32 public void Hide() { - if (_parent != null) - { - _parent._disabledBy.Remove(this); - _parent.UpdateEnabled(); - _parent = null; - } - UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide); } public virtual void Show() { - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, IntPtr.Zero); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero); ShowWindow(_showWindowState); } - public void ShowDialog(IWindowImpl parent) + public Action GotInputWhenDisabled { get; set; } + + public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - _parent._disabledBy.Add(this); - _parent.UpdateEnabled(); - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, ((WindowImpl)parent)._hwnd); - ShowWindow(_showWindowState); + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd); } + public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); + public void BeginMoveDrag(PointerPressedEventArgs e) { _mouseDevice.Capture(null); @@ -666,7 +658,7 @@ namespace Avalonia.Win32 } } - private WindowStyles GetWindowStateStyles () + private WindowStyles GetWindowStateStyles() { return GetStyle() & WindowStateMask; } @@ -721,11 +713,6 @@ namespace Avalonia.Win32 } } - private void UpdateEnabled() - { - EnableWindow(_hwnd, _disabledBy.Count == 0); - } - private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { var oldProperties = _windowProperties; diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e99be9cfd2..80c8a34ffd 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -322,10 +322,9 @@ namespace Avalonia.Controls.UnitTests { var window = new Window(); window.WindowStartupLocation = WindowStartupLocation.CenterOwner; - window.Position = new PixelPoint(60, 40); - window.Owner = parentWindow; + window.Position = new PixelPoint(60, 40); - window.Show(); + window.ShowDialog(parentWindow); var expectedPosition = new PixelPoint( (int)(parentWindow.Position.X + parentWindow.ClientSize.Width / 2 - window.ClientSize.Width / 2),