Skip to content

Commit

Permalink
feat: add intermediary window display states (glzr-io#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger authored and AryanSolanki637 committed Sep 18, 2023
1 parent 8769737 commit e5c755a
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 35 deletions.
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

0 comments on commit e5c755a

Please sign in to comment.