Skip to content
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

feat: add intermediary window display states #383

Merged
merged 7 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions GlazeWM.Domain/Common/Enums/DisplayState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace GlazeWM.Domain.Common.Enums
{
public enum DisplayState
{
Shown,
Showing,
Hidden,
Hiding,
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Linq;
using GlazeWM.Domain.Common.Enums;
using GlazeWM.Domain.Containers.Commands;
using GlazeWM.Domain.UserConfigs;
using GlazeWM.Domain.Windows;
using GlazeWM.Domain.Workspaces;
using GlazeWM.Infrastructure.Bussing;
using static GlazeWM.Infrastructure.WindowsApi.WindowsApiService;

Expand Down Expand Up @@ -71,14 +73,28 @@ private void SetWindowPosition(Window window)
SetWindowPosFlags.FrameChanged |
SetWindowPosFlags.NoActivate |
SetWindowPosFlags.NoCopyBits |
SetWindowPosFlags.NoSendChanging;
SetWindowPosFlags.NoSendChanging |
SetWindowPosFlags.AsyncWindowPos;

var workspace = window.Ancestors.OfType<Workspace>().First();
var isWorkspaceDisplayed = workspace.IsDisplayed;

// Show or hide the window depending on whether the workspace is displayed.
if (window.IsDisplayed)
if (isWorkspaceDisplayed)
defaultFlags |= SetWindowPosFlags.ShowWindow;
else
defaultFlags |= SetWindowPosFlags.HideWindow;

// Transition display state depending on whether window will be shown/hidden.
window.DisplayState = window.DisplayState switch
{
DisplayState.Hidden or
DisplayState.Hiding when isWorkspaceDisplayed => DisplayState.Showing,
DisplayState.Shown or
DisplayState.Showing when !isWorkspaceDisplayed => DisplayState.Hiding,
_ => window.DisplayState
};

if (window is TilingWindow)
{
SetWindowPos(
Expand Down
2 changes: 1 addition & 1 deletion GlazeWM.Domain/Containers/Container.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public IEnumerable<Container> DescendantFocusOrder

public bool IsDetached()
{
return Parent is null;
return Parent is null || Index == -1;
}

public bool HasChildren()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ public CommandResponse Handle(UnmanageWindowCommand command)
if (focusTarget is null)
return CommandResponse.Ok;

// The OS automatically switches focus to a different window after closing. If
// there are focusable windows, then set focus *after* the OS sets focus. This will
// cause focus to briefly flicker to the OS focus target and then to the WM's focus
// target.
_bus.Invoke(new SetFocusedDescendantCommand(focusTarget));
_containerService.HasPendingFocusSync = true;
_windowService.UnmanagedOrMinimizedStopwatch.Restart();
Expand Down
27 changes: 25 additions & 2 deletions GlazeWM.Domain/Windows/EventHandlers/WindowFocusedHandler.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Linq;
using GlazeWM.Domain.Common.Enums;
using GlazeWM.Domain.Common.Utils;
using GlazeWM.Domain.Containers;
using GlazeWM.Domain.Containers.Commands;
using GlazeWM.Domain.Containers.Events;
using GlazeWM.Domain.Workspaces;
using GlazeWM.Domain.Workspaces.Commands;
using GlazeWM.Infrastructure.Bussing;
using GlazeWM.Infrastructure.Common.Events;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -36,7 +38,8 @@ public void Handle(WindowFocusedEvent @event)
var window = _windowService.GetWindows()
.FirstOrDefault(window => window.Handle == @event.WindowHandle);

if (window is null || window?.IsDisplayed == false)
// Ignore event if window is unmanaged or being hidden by the WM.
if (window is null || window.DisplayState is DisplayState.Hiding)
return;

_logger.LogWindowEvent("Native focus event", window);
Expand All @@ -49,13 +52,33 @@ public void Handle(WindowFocusedEvent @event)

var unmanagedStopwatch = _windowService.UnmanagedOrMinimizedStopwatch;

if (unmanagedStopwatch?.ElapsedMilliseconds < 100)
// Handle overriding focus on close/minimize. After a window is closed or minimized,
// the OS or the closed application might automatically switch focus to a different
// window. To force focus to go to the WM's target focus container, we reassign any
// focus events 100ms after close/minimize. This will cause focus to briefly flicker
// to the OS focus target and then to the WM's focus target.
if (unmanagedStopwatch.IsRunning && unmanagedStopwatch.ElapsedMilliseconds < 100)
{
_logger.LogDebug("Overriding native focus.");
_bus.Invoke(new SyncNativeFocusCommand());
return;
}

// Handle focus events from windows on hidden workspaces. For example, if Discord
// is forcefully shown by the OS when it's on a hidden workspace, switch focus to
// Discord's workspace.
if (window.DisplayState is DisplayState.Hidden)
{
_logger.LogWindowEvent("Focusing off-screen window", window);

var workspace = WorkspaceService.GetWorkspaceFromChildContainer(window);
_bus.Invoke(new FocusWorkspaceCommand(workspace.Name));
_bus.Invoke(new SetFocusedDescendantCommand(window));
_bus.Invoke(new RedrawContainersCommand());
_bus.Emit(new FocusChangedEvent(window));
return;
}

// Update the WM's focus state.
_bus.Invoke(new SetFocusedDescendantCommand(window));
_bus.Emit(new FocusChangedEvent(window));
Expand Down
27 changes: 19 additions & 8 deletions GlazeWM.Domain/Windows/EventHandlers/WindowHiddenHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;
using GlazeWM.Domain.Common.Enums;
using GlazeWM.Domain.Common.Utils;
using GlazeWM.Domain.Containers.Commands;
using GlazeWM.Domain.Monitors.Commands;
Expand Down Expand Up @@ -39,18 +40,28 @@ public void Handle(WindowHiddenEvent @event)
var window = _windowService.GetWindows()
.FirstOrDefault(window => window.Handle == windowHandle);

// Ignore events where the window isn't managed or is actually supposed to be hidden. Since
// window events are processed in a sequence, also handle case where the window is not
// actually hidden anymore when the event is processed.
if (window?.IsDisplayed != true || WindowService.IsHandleVisible(window.Handle))
// Ignore event if window is unmanaged.
if (window is null)
return;

_logger.LogWindowEvent("Window hidden", window);

// Detach the hidden window from its parent.
_bus.Invoke(new UnmanageWindowCommand(window));
_bus.Invoke(new RedrawContainersCommand());
_bus.Invoke(new SyncNativeFocusCommand());
// Update the display state.
if (window.DisplayState is DisplayState.Hiding)
{
window.DisplayState = DisplayState.Hidden;
return;
}

// Unmanage the window if it's not in a display state transition. Also, since window
// events are not 100% guaranteed to be in correct order, we need to ignore events
// where the window is not actually hidden.
if (window.DisplayState is DisplayState.Shown && !WindowService.IsHandleVisible(window.Handle))
{
_bus.Invoke(new UnmanageWindowCommand(window));
_bus.Invoke(new RedrawContainersCommand());
_bus.Invoke(new SyncNativeFocusCommand());
}
}
}
}
28 changes: 22 additions & 6 deletions GlazeWM.Domain/Windows/EventHandlers/WindowShownHandler.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
using System.Linq;
using GlazeWM.Domain.Common.Enums;
using GlazeWM.Domain.Common.Utils;
using GlazeWM.Domain.Containers.Commands;
using GlazeWM.Domain.Monitors.Commands;
using GlazeWM.Domain.Windows.Commands;
using GlazeWM.Infrastructure.Bussing;
using GlazeWM.Infrastructure.Common.Events;
using Microsoft.Extensions.Logging;

namespace GlazeWM.Domain.Windows.EventHandlers
{
internal sealed class WindowShownHandler : IEventHandler<WindowShownEvent>
{
private readonly Bus _bus;
private readonly ILogger<WindowShownHandler> _logger;
private readonly WindowService _windowService;

public WindowShownHandler(Bus bus, WindowService windowService)
public WindowShownHandler(
Bus bus,
ILogger<WindowShownHandler> logger,
WindowService windowService)
{
_bus = bus;
_logger = logger;
_windowService = windowService;
}

Expand All @@ -32,13 +40,21 @@ public void Handle(WindowShownEvent @event)
var window = _windowService.GetWindows()
.FirstOrDefault(window => window.Handle == windowHandle);

// Ignore cases where window is already managed.
if (window is not null || !WindowService.IsHandleManageable(windowHandle))
// Manage the window if it's manageable.
if (window is null && WindowService.IsHandleManageable(windowHandle))
{
_bus.Invoke(new ManageWindowCommand(windowHandle));
_bus.Invoke(new RedrawContainersCommand());
_bus.Invoke(new SyncNativeFocusCommand());
return;
}

if (window is not null)
_logger.LogWindowEvent("Showing window", window);

_bus.Invoke(new ManageWindowCommand(@event.WindowHandle));
_bus.Invoke(new RedrawContainersCommand());
_bus.Invoke(new SyncNativeFocusCommand());
// Update display state if window is already managed.
if (window?.DisplayState == DisplayState.Showing)
window.DisplayState = DisplayState.Shown;
}
}
}
13 changes: 7 additions & 6 deletions GlazeWM.Domain/Windows/Window.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using System.Diagnostics;
using GlazeWM.Domain.Common;
using GlazeWM.Domain.Common.Enums;
using GlazeWM.Domain.Containers;
using GlazeWM.Domain.Workspaces;
using GlazeWM.Infrastructure.WindowsApi;
using static GlazeWM.Infrastructure.WindowsApi.WindowsApiService;

Expand All @@ -14,6 +15,11 @@ public abstract class Window : Container

public IntPtr Handle { get; }

/// <summary>
/// Whether window is shown, hidden, or in an intermediary state.
/// </summary>
public DisplayState DisplayState { get; set; } = DisplayState.Shown;

/// <summary>
/// The placement of the window when floating. Initialized with window's placement on launch
/// and updated on resize/move whilst floating.
Expand All @@ -39,11 +45,6 @@ protected Window(IntPtr handle, Rect floatingPlacement, RectDelta borderDelta)
BorderDelta = borderDelta;
}

/// <summary>
/// Windows are displayed if their parent workspace is displayed.
/// </summary>
public bool IsDisplayed => WorkspaceService.GetWorkspaceFromChildContainer(this).IsDisplayed;

public string ProcessName =>
WindowService.GetProcessOfHandle(Handle)?.ProcessName ?? string.Empty;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ public CommandResponse Handle(FocusWorkspaceCommand command)
// Save currently focused workspace as recent for command "recent"
_workspaceService.MostRecentWorkspace = focusedWorkspace;

_logger.LogDebug("WorkspaceToFocus: {WorkspaceToFocusName}", workspaceToFocus.Name);

// Set focus to the last focused window in workspace. If the workspace has no descendant
// windows, then set focus to the workspace itself.
_logger.LogDebug("WorkspaceToFocus: {WorkspaceToFocusName}", workspaceToFocus.Name);
var containerToFocus = workspaceToFocus.HasChildren()
? workspaceToFocus.LastFocusedDescendant
: workspaceToFocus;
Expand All @@ -70,11 +71,8 @@ public CommandResponse Handle(FocusWorkspaceCommand command)
_containerService.HasPendingFocusSync = true;

// Display the workspace to switch focus to.
if (focusedWorkspace.Parent == workspaceToFocus.Parent)
{
_containerService.ContainersToRedraw.Add(displayedWorkspace);
_containerService.ContainersToRedraw.Add(workspaceToFocus);
}
_containerService.ContainersToRedraw.Add(displayedWorkspace);
_containerService.ContainersToRedraw.Add(workspaceToFocus);

// Get empty workspace to destroy (if any are found). Cannot destroy empty workspaces if
// they're the only workspace on the monitor or are pending focus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,19 @@ public CommandResponse Handle(MoveWindowToWorkspaceCommand command)

var focusTarget = WindowService.GetFocusTargetAfterRemoval(windowToMove);

// Since the workspace that gets displayed is the last focused child, focus needs to be
// reassigned to the displayed workspace.
var targetMonitor = targetWorkspace.Parent as Monitor;
var focusResetTarget = targetWorkspace.IsDisplayed ? null : targetMonitor.LastFocusedDescendant;

if (windowToMove is TilingWindow)
MoveTilingWindowToWorkspace(windowToMove as TilingWindow, targetWorkspace);
else
_bus.Invoke(new MoveContainerWithinTreeCommand(windowToMove, targetWorkspace, false));

if (focusResetTarget is not null)
_bus.Invoke(new SetFocusedDescendantCommand(focusResetTarget));

// Reassign focus to descendant within the current workspace. Need to call
// `SetFocusedDescendantCommand` for when commands like `FocusWorkspaceCommand` are called
// immediately afterwards and they should behave as if `focusTarget` is the focused
Expand Down