diff --git a/source/Components/AvalonDock/Controls/LayoutAnchorableFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutAnchorableFloatingWindowControl.cs index b9d6e0a4..dde5c521 100644 --- a/source/Components/AvalonDock/Controls/LayoutAnchorableFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutAnchorableFloatingWindowControl.cs @@ -208,20 +208,14 @@ protected override void OnInitialized(EventArgs e) /// protected override void OnClosed(EventArgs e) { - var root = Model.Root; - if (root != null) - { - if (root is LayoutRoot layoutRoot) layoutRoot.Updated -= OnRootUpdated; - root.Manager.RemoveFloatingWindow(this); - root.CollectGarbage(); - } + if (Model.Root is LayoutRoot layoutRoot) layoutRoot.Updated -= OnRootUpdated; + if (_overlayWindow != null) { _overlayWindow.Close(); _overlayWindow = null; } base.OnClosed(e); - if (!CloseInitiatedByUser) root?.FloatingWindows.Remove(_model); // We have to clear binding instead of creating a new empty binding. BindingOperations.ClearBinding(_model, VisibilityProperty); @@ -235,11 +229,143 @@ protected override void OnClosed(EventArgs e) } /// - protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + protected override void OnClosing(CancelEventArgs e) + { + // Allow base Window class and attached Closing event handlers to potentially cancel first. + base.OnClosing(e); + + if (e.Cancel) // If already cancelled by base or others, do nothing. + return; + + // If closed programmatically by AvalonDock (e.g., dragging last anchorable out), skip user checks. + if (!CloseInitiatedByUser) + return; + + // Handle user-initiated close (Taskbar, Alt+F4, Window's 'X' button). + var manager = Model?.Root?.Manager; + if (manager == null) + return; + + var anchorablesToProcess = Model.Descendents().OfType().ToArray(); + + // Phase 1: Validate if ALL anchorables can be processed (closed or hidden) + // This checks properties, internal events, manager events, and command CanExecute + // before deciding if the window closing can proceed. + // Priority: Try Close Path + // - Check internal Closing + // - Check manager AnchorableClosing event + // - Check command CanExecute + // Fallback: Try Hide Path + // - Check internal Hiding + // - Check manager AnchorableHiding event + // - Check command CanExecute + // Fallback 2: Cannot Close AND Cannot Hide + var cancelAll = anchorablesToProcess.Any(anch => + { + var closeCommand = manager.GetLayoutItemFromModel(anch)?.CloseCommand; + var hideCommand = (manager.GetLayoutItemFromModel(anch) as LayoutAnchorableItem)?.HideCommand; + return anch.CanClose && (!anch.TestCanClose() || !ManagerTestCanClose(anch) || closeCommand?.CanExecute(null) is false) || + anch.CanHide && (!anch.TestCanHide() || !ManagerTestCanHide(anch) || hideCommand?.CanExecute(null) is false) || + !anch.CanClose && !anch.CanHide; + }); + + if (cancelAll) + { + e.Cancel = true; + return; + } + + // Phase 2: Execute actions based on the validated priority (Close > Hide) + // We use the compromise: execute user command if provided, otherwise execute default logic directly. + var wasAnyContentHidden = false; + foreach (var anch in anchorablesToProcess.ToList()) // Use ToList() as actions might modify the underlying collection. + { + var layoutItem = manager.GetLayoutItemFromModel(anch) as LayoutAnchorableItem; + bool useDefaultLogic = layoutItem == null; // Should not happen, but safe default + + if (anch.CanClose) // Priority Action: Close + { + if (!useDefaultLogic) useDefaultLogic = layoutItem.IsDefaultCloseCommand; + + if (!useDefaultLogic) + { + // User Custom Command Path + layoutItem.CloseCommand?.Execute(null); + } + else + { + // Default AvalonDock Logic Path + anch.CloseInternal(); // Does NOT raise manager's Closing/Closed events again + if (layoutItem?.IsViewExists() == true) + manager.InternalRemoveLogicalChild(layoutItem.View); + manager.RaiseAnchorableClosed(anch); // Raise final event + } + } + else if (anch.CanHide) // Fallback Action: Hide + { + if (!useDefaultLogic) useDefaultLogic = layoutItem.IsDefaultHideCommand; + + if (!useDefaultLogic) + { + // User Custom Command Path + layoutItem.HideCommand?.Execute(null); + } + else + { + // Default AvalonDock Logic Path + // Use 'false' to bypass internal cancel checks already done in Phase 1. + if (anch.HideAnchorable(false)) // Does NOT raise manager's Hiding/Hidden events again + { + // View removal for Hide is typically handled by DockingManager logic or CollectGarbage. + manager.RaiseAnchorableHidden(anch); // Raise final event + } + } + + wasAnyContentHidden = true; + } + // If neither CanClose nor CanHide, do nothing (already validated in Phase 1). + } + + if (wasAnyContentHidden) + { + // Close the window only if all anchorables were closed + e.Cancel = true; + } + } + + /// + /// Helper method to check DockingManager's AnchorableClosing event for cancellation. + /// + private bool ManagerTestCanClose(LayoutAnchorable anch) { - var canHide = HideWindowCommand.CanExecute(null); - if (CloseInitiatedByUser && !KeepContentVisibleOnClose && !canHide) e.Cancel = true; - base.OnClosing(e); + var ancClosingArgs = new AnchorableClosingEventArgs(anch); + Model?.Root?.Manager.RaiseAnchorableClosing(ancClosingArgs); + return !ancClosingArgs.Cancel; + } + + /// + /// Helper method to check DockingManager's AnchorableHiding event for cancellation, + /// including the CloseInsteadOfHide request when CanClose is false. + /// + private bool ManagerTestCanHide(LayoutAnchorable anch) + { + var hidingArgs = new AnchorableHidingEventArgs(anch); + Model?.Root?.Manager.RaiseAnchorableHiding(hidingArgs); + + // If the Hiding event itself was cancelled, prevent the action. + if (hidingArgs.Cancel) + return false; + + // If Hiding requests Close instead, but CanClose is false (which it must be + // to reach this point), then the requested action cannot be performed, so cancel. + if (hidingArgs.CloseInsteadOfHide) + { + // Log warning maybe? "CloseInsteadOfHide requested for an anchorable where CanClose=false." + return false; + } + + // Hiding was not cancelled and not replaced by an impossible Close action. + return true; } /// diff --git a/source/Components/AvalonDock/Controls/LayoutAnchorableItem.cs b/source/Components/AvalonDock/Controls/LayoutAnchorableItem.cs index 67e5f0dd..afb6fa8d 100644 --- a/source/Components/AvalonDock/Controls/LayoutAnchorableItem.cs +++ b/source/Components/AvalonDock/Controls/LayoutAnchorableItem.cs @@ -69,6 +69,9 @@ public ICommand HideCommand set => SetValue(HideCommandProperty, value); } + /// Gets a value indicating whether the is the default value. + internal bool IsDefaultHideCommand => HideCommand == _defaultHideCommand; + /// Handles changes to the property. private static void OnHideCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LayoutAnchorableItem)d).OnHideCommandChanged(e); diff --git a/source/Components/AvalonDock/Controls/LayoutDocumentFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutDocumentFloatingWindowControl.cs index e75d1aa6..7274b0fd 100644 --- a/source/Components/AvalonDock/Controls/LayoutDocumentFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutDocumentFloatingWindowControl.cs @@ -69,6 +69,28 @@ internal LayoutDocumentFloatingWindowControl(LayoutDocumentFloatingWindow model) #endregion Constructors + #region Public Methods + + /// + public override void EnableBindings() + { + _model.PropertyChanged += Model_PropertyChanged; + _model.IsVisibleChanged += _model_IsVisibleChanged; + + base.EnableBindings(); + } + + /// + public override void DisableBindings() + { + _model.PropertyChanged -= Model_PropertyChanged; + _model.IsVisibleChanged -= _model_IsVisibleChanged; + + base.DisableBindings(); + } + + #endregion + #region Overrides /// @@ -105,16 +127,10 @@ protected override void OnInitialized(EventArgs e) base.OnInitialized(e); var manager = _model.Root.Manager; Content = manager.CreateUIElementForModel(_model.RootPanel); - // TODO IsVisibleChanged + EnableBindings(); //SetBinding(SingleContentLayoutItemProperty, new Binding("Model.SinglePane.SelectedContent") { Source = this, Converter = new LayoutItemFromLayoutModelConverter() }); _model.RootPanel.ChildrenCollectionChanged += RootPanelOnChildrenCollectionChanged; } - - private void Model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(LayoutDocumentFloatingWindow.RootPanel) && _model.RootPanel == null) InternalClose(); - } - /// protected override IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { @@ -150,17 +166,6 @@ protected override IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, Int } } break; - - case Win32Helper.WM_CLOSE: - if (CloseInitiatedByUser) - { - // We want to force the window to go through our standard logic for closing. - // So, if the window close is initiated outside of our code (such as from the taskbar), - // we cancel that close and trigger our close logic instead. - this.CloseWindowCommand.Execute(null); - handled = true; - } - break; } return base.FilterMessage(hwnd, msg, wParam, lParam, ref handled); } @@ -168,20 +173,12 @@ protected override IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, Int /// protected override void OnClosed(EventArgs e) { - var root = Model.Root; - // MK sometimes root is null, prevent crash, or should it always be set?? - if (root != null) - { - root.Manager.RemoveFloatingWindow(this); - root.CollectGarbage(); - } if (_overlayWindow != null) { _overlayWindow.Close(); _overlayWindow = null; } base.OnClosed(e); - if (!CloseInitiatedByUser) root?.FloatingWindows.Remove(_model); _model.PropertyChanged -= Model_PropertyChanged; } @@ -189,6 +186,28 @@ protected override void OnClosed(EventArgs e) #region Private Methods + private void _model_IsVisibleChanged(object sender, EventArgs e) + { + if (!IsVisible && _model.IsVisible) Show(); + } + + private void Model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(LayoutDocumentFloatingWindow.RootPanel): + if (_model.RootPanel == null) InternalClose(); + break; + + case nameof(LayoutDocumentFloatingWindow.IsVisible): + if (_model.IsVisible != IsVisible) + { + Visibility = _model.IsVisible ? Visibility.Visible : Visibility.Hidden; + } + break; + } + } + private void RootPanelOnChildrenCollectionChanged(object sender, EventArgs e) { if (_model.RootPanel == null || _model.RootPanel.Children.Count == 0) InternalClose(); @@ -208,15 +227,192 @@ private bool OpenContextMenu() /// protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { - // TODO - if (CloseInitiatedByUser && !KeepContentVisibleOnClose) + // Allow base Window class and attached Closing event handlers to potentially cancel first. + base.OnClosing(e); + + if (e.Cancel) // If already cancelled by base or others, do nothing. + return; + + // If closed programmatically by AvalonDock (e.g., dragging last doc out), skip user checks. + if (!CloseInitiatedByUser) + return; + + // Handle user-initiated close (Taskbar, Alt+F4, Window's 'X' button). + var manager = Model?.Root?.Manager; + if (manager == null) + return; + + var documentsToClose = this.Model.Descendents().OfType().ToArray(); + var anchorablesToProcess = Model.Descendents().OfType().ToArray(); + + // Phase 1.1: Validate if ALL documents can be closed + // This checks properties and fires Closing events without actually closing yet. + // - Check explicit property first. + // - Check internal LayoutContent.Closing event subscribers. + // - Check external DockingManager.DocumentClosing event subscribers. + // - Check command CanExecute. + var cancelAllFromDocuments = documentsToClose.Any( + doc => !doc.CanClose + || !doc.TestCanClose() + || !ManagerTestCanClose(doc) + || !(manager.GetLayoutItemFromModel(doc)?.CloseCommand?.CanExecute(null) ?? false)); + + // Phase 1.2: Validate if ALL anchorables can be processed (closed or hidden) + // Priority: Try Close Path + // Fallback: Try Hide Path + // Fallback 2: Cannot Close AND Cannot Hide + var cancelAllFromAnchorables = anchorablesToProcess.Any(anch => + { + var closeCommand = manager.GetLayoutItemFromModel(anch)?.CloseCommand; + var hideCommand = (manager.GetLayoutItemFromModel(anch) as LayoutAnchorableItem)?.HideCommand; + return anch.CanClose && (!anch.TestCanClose() || !ManagerTestCanClose(anch) || closeCommand?.CanExecute(null) is false) || + anch.CanHide && (!anch.TestCanHide() || !ManagerTestCanHide(anch) || hideCommand?.CanExecute(null) is false) || + !anch.CanClose && !anch.CanHide; + }); + var cancelAll = cancelAllFromDocuments || cancelAllFromAnchorables; + + // If any document prevents closing, cancel the window closing. + if (cancelAll) { e.Cancel = true; - //_model.Descendents().OfType().ToArray().ForEach((a) => a.Hide()); + return; } - base.OnClosing(e); + + // Phase 2: Execute actions + foreach (var doc in documentsToClose.ToList()) + { + var layoutItem = manager.GetLayoutItemFromModel(doc) as LayoutDocumentItem; + + if (layoutItem != null && !layoutItem.IsDefaultCloseCommand) + { + // User Custom Command Path + // Execute the user's command. Assume user handles risks. + layoutItem.CloseCommand?.Execute(null); // CanExecute already checked in Phase 1 + } + else + { + // Default AvalonDock Logic Path + // 1. Perform internal close logic (removes from parent, etc.) + doc.CloseInternal(); // Does NOT raise manager's DocumentClosed event + + // 2. Clean up view/logical tree elements + if (layoutItem?.IsViewExists() == true) + manager.InternalRemoveLogicalChild(layoutItem.View); + + if (doc.Content is UIElement uiElement) + manager.InternalRemoveLogicalChild(uiElement); + + doc.Content = null; // Final content cleanup + + // 3. Raise the manager's final event + manager.RaiseDocumentClosed(doc); + } + } + + // We use the compromise: execute user command if provided, otherwise execute default logic directly. + var wasAnyContentHidden = false; + foreach (var anch in anchorablesToProcess.ToList()) // Use ToList() as actions might modify the underlying collection. + { + var layoutItem = manager.GetLayoutItemFromModel(anch) as LayoutAnchorableItem; + bool useDefaultLogic = layoutItem == null; // Should not happen, but safe default + + if (anch.CanClose) // Priority Action: Close + { + if (!useDefaultLogic) useDefaultLogic = layoutItem.IsDefaultCloseCommand; + + if (!useDefaultLogic) + { + // User Custom Command Path + layoutItem.CloseCommand?.Execute(null); + } + else + { + // Default AvalonDock Logic Path + anch.CloseInternal(); // Does NOT raise manager's Closing/Closed events again + if (layoutItem?.IsViewExists() == true) + manager.InternalRemoveLogicalChild(layoutItem.View); + manager.RaiseAnchorableClosed(anch); // Raise final event + } + } + else if (anch.CanHide) // Fallback Action: Hide + { + if (!useDefaultLogic) useDefaultLogic = layoutItem.IsDefaultHideCommand; + + if (!useDefaultLogic) + { + // User Custom Command Path + layoutItem.HideCommand?.Execute(null); + } + else + { + // Default AvalonDock Logic Path + // Use 'false' to bypass internal cancel checks already done in Phase 1. + if (anch.HideAnchorable(false)) // Does NOT raise manager's Hiding/Hidden events again + { + // View removal for Hide is typically handled by DockingManager logic or CollectGarbage. + manager.RaiseAnchorableHidden(anch); // Raise final event + } + } + + wasAnyContentHidden = true; + } + // If neither CanClose nor CanHide, do nothing (already validated in Phase 1). + } + + if (wasAnyContentHidden) + { + // Close the window only if all anchorables were closed + e.Cancel = true; + } + + // Window will close naturally as e.Cancel was not set to true. } + /// + /// Helper method to check DockingManager's DocumentClosing event for cancellation. + /// + private bool ManagerTestCanClose(LayoutDocument doc) + { + var docClosingArgs = new DocumentClosingEventArgs(doc); + Model?.Root?.Manager.RaiseDocumentClosing(docClosingArgs); + return !docClosingArgs.Cancel; + } + + /// + /// Helper method to check DockingManager's AnchorableClosing event for cancellation. + /// + private bool ManagerTestCanClose(LayoutAnchorable anch) + { + var ancClosingArgs = new AnchorableClosingEventArgs(anch); + Model?.Root?.Manager.RaiseAnchorableClosing(ancClosingArgs); + return !ancClosingArgs.Cancel; + } + + /// + /// Helper method to check DockingManager's AnchorableHiding event for cancellation, + /// including the CloseInsteadOfHide request when CanClose is false. + /// + private bool ManagerTestCanHide(LayoutAnchorable anch) + { + var hidingArgs = new AnchorableHidingEventArgs(anch); + Model?.Root?.Manager.RaiseAnchorableHiding(hidingArgs); + + // If the Hiding event itself was cancelled, prevent the action. + if (hidingArgs.Cancel) + return false; + + // If Hiding requests Close instead, but CanClose is false (which it must be + // to reach this point), then the requested action cannot be performed, so cancel. + if (hidingArgs.CloseInsteadOfHide) + { + // Log warning maybe? "CloseInsteadOfHide requested for an anchorable where CanClose=false." + return false; + } + + // Hiding was not cancelled and not replaced by an impossible Close action. + return true; + } + bool IOverlayWindowHost.HitTestScreen(Point dragPoint) { return HitTest(this.TransformToDeviceDPI(dragPoint)); diff --git a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs index f13ab5d6..29057e5f 100644 --- a/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs +++ b/source/Components/AvalonDock/Controls/LayoutFloatingWindowControl.cs @@ -513,7 +513,10 @@ internal void InternalClose(bool closeInitiatedByUser = false) /// protected override void OnClosed(EventArgs e) { + // Unsubscribe from window events to prevent memory leaks. SizeChanged -= OnSizeChanged; + + // Clean up native window resources and message hook. if (Content != null) { (Content as FloatingWindowContentHost)?.Dispose(); @@ -524,6 +527,29 @@ protected override void OnClosed(EventArgs e) _hwndSrc = null; } } + + var root = Model?.Root; + var modelToRemove = _model as LayoutFloatingWindow; + if (root != null) + { + // Notify the manager to remove this view instance from its tracking list (_fwList). + root.Manager?.RemoveFloatingWindow(this); + + // Remove the model from the LayoutRoot's collection only if closed externally + // AND if the model wasn't already detached (e.g., by CollectGarbage during the closing process). + if (!_internalCloseFlag && modelToRemove?.Parent == root) + { + root.FloatingWindows?.Remove(modelToRemove); + } + + // Clean up the layout tree (e.g., remove empty parent panes). + root.CollectGarbage(); + } + + // Ensure derived classes clean up model event handlers and bindings + // to prevent leaks between view and model. + DisableBindings(); + base.OnClosed(e); } diff --git a/source/Components/AvalonDock/Controls/LayoutItem.cs b/source/Components/AvalonDock/Controls/LayoutItem.cs index 891d17b5..c99ee4df 100644 --- a/source/Components/AvalonDock/Controls/LayoutItem.cs +++ b/source/Components/AvalonDock/Controls/LayoutItem.cs @@ -289,6 +289,9 @@ public ICommand CloseCommand get => (ICommand)GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); } + + /// Gets wether the property has its default value. + internal bool IsDefaultCloseCommand => CloseCommand == _defaultCloseCommand; /// Handles changes to the property. private static void OnCloseCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LayoutItem)d).OnCloseCommandChanged(e); diff --git a/source/Components/AvalonDock/DockingManager.cs b/source/Components/AvalonDock/DockingManager.cs index 8c7ca9d0..37fbf465 100644 --- a/source/Components/AvalonDock/DockingManager.cs +++ b/source/Components/AvalonDock/DockingManager.cs @@ -1990,6 +1990,18 @@ internal void ExecuteHideCommand(LayoutAnchorable anchorable) internal void ExecuteContentActivateCommand(LayoutContent content) => content.IsActive = true; + internal void RaiseDocumentClosing(DocumentClosingEventArgs e) => DocumentClosing?.Invoke(this, e); + + internal void RaiseAnchorableHiding(AnchorableHidingEventArgs e) => AnchorableHiding?.Invoke(this, e); + + internal void RaiseAnchorableClosing(AnchorableClosingEventArgs e) => AnchorableClosing?.Invoke(this, e); + + internal void RaiseDocumentClosed(LayoutDocument document) => DocumentClosed?.Invoke(this, new DocumentClosedEventArgs(document)); + + internal void RaiseAnchorableClosed(LayoutAnchorable anchorable) => AnchorableClosed?.Invoke(this, new AnchorableClosedEventArgs(anchorable)); + + internal void RaiseAnchorableHidden(LayoutAnchorable anchorable) => AnchorableHidden?.Invoke(this, new AnchorableHiddenEventArgs(anchorable)); + #endregion Internal Methods #region Overrides diff --git a/source/Components/AvalonDock/Layout/LayoutAnchorable.cs b/source/Components/AvalonDock/Layout/LayoutAnchorable.cs index 68efffde..124fd308 100644 --- a/source/Components/AvalonDock/Layout/LayoutAnchorable.cs +++ b/source/Components/AvalonDock/Layout/LayoutAnchorable.cs @@ -646,6 +646,13 @@ internal bool CloseAnchorable() // _canClose = _canCloseValueBeforeInternalSet; //} + internal bool TestCanHide() + { + var args = new CancelEventArgs(); + OnHiding(args); + return !args.Cancel; + } + #endregion Internal Methods #region Private Methods diff --git a/source/Components/AvalonDock/Layout/LayoutDocumentPaneGroup.cs b/source/Components/AvalonDock/Layout/LayoutDocumentPaneGroup.cs index ae317ec5..8dc6f2cc 100644 --- a/source/Components/AvalonDock/Layout/LayoutDocumentPaneGroup.cs +++ b/source/Components/AvalonDock/Layout/LayoutDocumentPaneGroup.cs @@ -8,6 +8,7 @@ This program is provided to you under the terms of the Microsoft Public ************************************************************************/ using System; +using System.Linq; using System.Windows.Controls; using System.Windows.Markup; @@ -60,9 +61,10 @@ public Orientation Orientation #endregion Properties #region Overrides - + /// - protected override bool GetVisibility() => true; + protected override bool GetVisibility() => + Children.Count > 0 && Children.Any(c => c.IsVisible); /// public override void WriteXml(System.Xml.XmlWriter writer) diff --git a/source/Components/AvalonDock/Win32Helper.cs b/source/Components/AvalonDock/Win32Helper.cs index 9c66b608..7c513190 100644 --- a/source/Components/AvalonDock/Win32Helper.cs +++ b/source/Components/AvalonDock/Win32Helper.cs @@ -153,7 +153,6 @@ internal class WINDOWPOS internal const int WM_INITMENUPOPUP = 0x0117; internal const int WM_KEYDOWN = 0x0100; internal const int WM_KEYUP = 0x0101; - internal const int WM_CLOSE = 0x10; internal const int WA_INACTIVE = 0x0000;