Skip to content

Commit

Permalink
feat: focus override based on timer (#396)
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger authored Sep 13, 2023
1 parent c9083eb commit 0c42b0b
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ internal sealed class SyncNativeFocusHandler : ICommandHandler<SyncNativeFocusCo
{
private readonly Bus _bus;
private readonly ContainerService _containerService;
private readonly WindowService _windowService;

public SyncNativeFocusHandler(
Bus bus,
ContainerService containerService,
WindowService windowService)
public SyncNativeFocusHandler(Bus bus, ContainerService containerService)
{
_bus = bus;
_containerService = containerService;
_windowService = windowService;
}

public CommandResponse Handle(SyncNativeFocusCommand command)
Expand All @@ -37,7 +32,7 @@ public CommandResponse Handle(SyncNativeFocusCommand command)
var handleToFocus = focusedContainer switch
{
Window window => window.Handle,
Workspace => _windowService.DesktopWindowHandle,
Workspace => GetDesktopWindow(),
_ => throw new Exception("Invalid container type to focus. This is a bug."),
};

Expand All @@ -49,6 +44,8 @@ public CommandResponse Handle(SyncNativeFocusCommand command)
_bus.Emit(new NativeFocusSyncedEvent(focusedContainer));
_bus.Emit(new FocusChangedEvent(focusedContainer));

_containerService.HasPendingFocusSync = false;

return CommandResponse.Ok;
}
}
Expand Down
6 changes: 0 additions & 6 deletions GlazeWM.Domain/Containers/ContainerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ public class ContainerService
/// </summary>
public string ActiveBindingMode { get; set; }

/// <summary>
/// If set, this container overrides the target container to set focus to on the next
/// focus window event (ie. `EVENT_SYSTEM_FOREGROUND`).
/// </summary>
public Container PendingFocusContainer { get; set; }

private readonly UserConfigService _userConfigService;

public ContainerService(UserConfigService userConfigService)
Expand Down
22 changes: 16 additions & 6 deletions GlazeWM.Domain/Windows/CommandHandlers/UnmanageWindowHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,45 @@ internal sealed class UnmanageWindowHandler : ICommandHandler<UnmanageWindowComm
{
private readonly Bus _bus;
private readonly ContainerService _containerService;
private readonly WindowService _windowService;

public UnmanageWindowHandler(Bus bus, ContainerService containerService)
public UnmanageWindowHandler(
Bus bus,
ContainerService containerService,
WindowService windowService)
{
_bus = bus;
_containerService = containerService;
_windowService = windowService;
}

public CommandResponse Handle(UnmanageWindowCommand command)
{
var window = command.Window;

// Get container to switch focus to after the window has been removed.
var focusTarget = WindowService.GetFocusTargetAfterRemoval(window);
var focusedContainer = _containerService.FocusedContainer;
var focusTarget = window == focusedContainer
? WindowService.GetFocusTargetAfterRemoval(window)
: null;

if (window is IResizable)
_bus.Invoke(new DetachAndResizeContainerCommand(window));
else
_bus.Invoke(new DetachContainerCommand(window));

_bus.Emit(new WindowUnmanagedEvent(window.Id, window.Handle));

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.PendingFocusContainer = focusTarget;
_containerService.HasPendingFocusSync = true;

_bus.Emit(new WindowUnmanagedEvent(window.Id, window.Handle));
_windowService.UnmanagedOrMinimizedStopwatch.Restart();

return CommandResponse.Ok;
}
Expand Down
39 changes: 9 additions & 30 deletions GlazeWM.Domain/Windows/EventHandlers/WindowFocusedHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,6 @@ public WindowFocusedHandler(
public void Handle(WindowFocusedEvent @event)
{
var windowHandle = @event.WindowHandle;
var pendingFocusContainer = _containerService.PendingFocusContainer;

// Override the container to set focus to (ie. when changing focus after a window is
// closed or hidden).
// TODO: This can be simplified a lot. Might also need to handle case where window
// is null or not displayed.
if (pendingFocusContainer is not null)
{
// If the container gaining focus is the pending focus container, then reset it.
if (pendingFocusContainer is Window && @event.WindowHandle == (pendingFocusContainer as Window).Handle)
{
_bus.Invoke(new SetFocusedDescendantCommand(pendingFocusContainer));
_bus.Emit(new FocusChangedEvent(pendingFocusContainer));
_containerService.PendingFocusContainer = null;
return;
}

var isDesktopWindow = windowHandle == _windowService.DesktopWindowHandle;

if (pendingFocusContainer is Workspace && isDesktopWindow)
{
_bus.Invoke(new SetFocusedDescendantCommand(pendingFocusContainer));
_bus.Emit(new FocusChangedEvent(pendingFocusContainer));
_containerService.PendingFocusContainer = null;
return;
}

_bus.InvokeAsync(new SyncNativeFocusCommand());
return;
}

var window = _windowService.GetWindows()
.FirstOrDefault(window => window.Handle == @event.WindowHandle);
Expand All @@ -77,6 +47,15 @@ public void Handle(WindowFocusedEvent @event)
if (window == focusedContainer)
return;

var unmanagedStopwatch = _windowService.UnmanagedOrMinimizedStopwatch;

if (unmanagedStopwatch?.ElapsedMilliseconds < 100)
{
_logger.LogDebug("Overriding native focus.");
_bus.Invoke(new SyncNativeFocusCommand());
return;
}

// Update the WM's focus state.
_bus.Invoke(new SetFocusedDescendantCommand(window));
_bus.Emit(new FocusChangedEvent(window));
Expand Down
16 changes: 12 additions & 4 deletions GlazeWM.Domain/Windows/EventHandlers/WindowMinimizedHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,22 @@ public void Handle(WindowMinimizedEvent @event)
Id = window.Id
};

// Get container to switch focus to after the window has been minimized.
var focusTarget = WindowService.GetFocusTargetAfterRemoval(window);
// Get container to switch focus to after the window has been minimized. Need to
// get focus target prior to calling `ReplaceContainerCommand`.
var focusedContainer = _containerService.FocusedContainer;
var focusTarget = window == focusedContainer
? WindowService.GetFocusTargetAfterRemoval(window)
: null;

_bus.Invoke(new ReplaceContainerCommand(minimizedWindow, window.Parent, window.Index));

// Focus should be reassigned to appropriate container.
_bus.Invoke(new SetFocusedDescendantCommand(focusTarget));
_containerService.HasPendingFocusSync = true;
if (focusTarget is not null)
{
_bus.Invoke(new SetFocusedDescendantCommand(focusTarget));
_containerService.HasPendingFocusSync = true;
_windowService.UnmanagedOrMinimizedStopwatch.Restart();
}

_containerService.ContainersToRedraw.Add(workspace);
_bus.Invoke(new RedrawContainersCommand());
Expand Down
15 changes: 3 additions & 12 deletions GlazeWM.Domain/Windows/WindowService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public class WindowService
public List<IntPtr> IgnoredHandles { get; set; } = new();

/// <summary>
/// Handle to the desktop window.
/// Time since a previously focused window was unmanaged or minimized. Used to
/// decide whether to override incoming focus events.
/// </summary>
public IntPtr DesktopWindowHandle { get; } = GetDesktopWindowHandle();
public Stopwatch UnmanagedOrMinimizedStopwatch { get; } = new();

private readonly ContainerService _containerService;
private readonly MonitorService _monitorService;
Expand Down Expand Up @@ -127,16 +128,6 @@ public static List<IntPtr> GetAllWindowHandles()
return windowHandles;
}

private static IntPtr GetDesktopWindowHandle()
{
return GetAllWindowHandles().Find(handle =>
{
var className = GetClassNameOfHandle(handle);
var process = GetProcessOfHandle(handle);
return className == "Progman" && process.ProcessName == "explorer";
});
}

public static WindowStylesEx GetWindowStylesEx(IntPtr handle)
{
return unchecked((WindowStylesEx)GetWindowLongPtr(handle, GWLEXSTYLE).ToInt64());
Expand Down

0 comments on commit 0c42b0b

Please sign in to comment.