这是indexloc提供的服务,不要输入任何密码
Skip to content

Fix Floating Window Closing Behavior and Visibility Handling #506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,14 @@ protected override void OnInitialized(EventArgs e)
/// <inheritdoc />
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);
Expand All @@ -235,11 +229,143 @@ protected override void OnClosed(EventArgs e)
}

/// <inheritdoc />
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<LayoutAnchorable>().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;
}
}

/// <summary>
/// Helper method to check DockingManager's AnchorableClosing event for cancellation.
/// </summary>
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;
}

/// <summary>
/// Helper method to check DockingManager's AnchorableHiding event for cancellation,
/// including the CloseInsteadOfHide request when CanClose is false.
/// </summary>
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;
}

/// <inheritdoc />
Expand Down
3 changes: 3 additions & 0 deletions source/Components/AvalonDock/Controls/LayoutAnchorableItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public ICommand HideCommand
set => SetValue(HideCommandProperty, value);
}

/// <summary>Gets a value indicating whether the <see cref="HideCommand"/> is the default value.</summary>
internal bool IsDefaultHideCommand => HideCommand == _defaultHideCommand;

/// <summary>Handles changes to the <see cref="HideCommand"/> property.</summary>
private static void OnHideCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((LayoutAnchorableItem)d).OnHideCommandChanged(e);

Expand Down
Loading