diff --git a/src/Whim.FocusIndicator/FocusIndicatorPlugin.cs b/src/Whim.FocusIndicator/FocusIndicatorPlugin.cs index 74f744d02..af7096b0a 100644 --- a/src/Whim.FocusIndicator/FocusIndicatorPlugin.cs +++ b/src/Whim.FocusIndicator/FocusIndicatorPlugin.cs @@ -1,6 +1,8 @@ using System; using System.Text.Json; -using Microsoft.UI.Xaml; +using System.Threading; +using System.Threading.Tasks; +using Windows.Win32.Foundation; namespace Whim.FocusIndicator; @@ -10,8 +12,10 @@ public class FocusIndicatorPlugin : IFocusIndicatorPlugin private bool _isEnabled = true; private readonly IContext _context; private readonly FocusIndicatorConfig _focusIndicatorConfig; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly CancellationToken _cancellationToken; private FocusIndicatorWindow? _focusIndicatorWindow; - private DispatcherTimer? _dispatcherTimer; + private int _lastFocusStartTime; private bool _disposedValue; /// @@ -31,17 +35,23 @@ public FocusIndicatorPlugin(IContext context, FocusIndicatorConfig focusIndicato { _context = context; _focusIndicatorConfig = focusIndicatorConfig; + + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; } /// public void PreInitialize() { _context.FilterManager.AddTitleMatchFilter(FocusIndicatorConfig.Title); - - _context.WindowManager.WindowMoveStart += WindowManager_WindowMoveStart; _context.WindowManager.WindowFocused += WindowManager_WindowFocused; } + private void WindowManager_WindowFocused(object? sender, WindowFocusedEventArgs e) + { + _lastFocusStartTime = Environment.TickCount; + } + /// public void PostInitialize() { @@ -52,97 +62,84 @@ public void PostInitialize() _focusIndicatorWindow.Activate(); _focusIndicatorWindow.Hide(_context); - // Only subscribe to workspace changes once the indicator window has been created - we shouldn't - // show a window which doesn't yet exist (it'll just crash Whim). - _context.WorkspaceManager.WorkspaceLayoutStarted += WorkspaceManager_WorkspaceLayoutStarted; - _context.WorkspaceManager.WorkspaceLayoutCompleted += WorkspaceManager_WorkspaceLayoutCompleted; + Task.Factory.StartNew( + ContinuousPolling, + _cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default + ); } - private void DispatcherTimer_Tick(object? sender, object e) + private void ContinuousPolling() { - Logger.Debug("Focus indicator timer ticked"); - Hide(); + while (true) + { + Poll(); + Thread.Sleep(16); + } } - private void WindowManager_WindowMoveStart(object? sender, WindowMoveStartedEventArgs e) => Hide(); - - private void WindowManager_WindowFocused(object? sender, WindowFocusedEventArgs e) + private void Poll() { - if (!_isEnabled) + if (_isEnabled) { - Logger.Debug("Focus indicator is disabled"); - return; - } + if (_focusIndicatorConfig.FadeEnabled) + { + int now = Environment.TickCount; + if (now - _lastFocusStartTime >= _focusIndicatorConfig.FadeTimeout.TotalMilliseconds) + { + Hide(); + return; + } + } - if (e.Window == null) + // If the fade is not enabled, or the fade is not over, show the focus indicator. + Show(); + } + else if (IsVisible) { Hide(); - return; } - - Show(); } - private void WorkspaceManager_WorkspaceLayoutStarted(object? sender, WorkspaceEventArgs e) => Hide(); - - private void WorkspaceManager_WorkspaceLayoutCompleted(object? sender, WorkspaceEventArgs e) => Show(); - /// public void Show(IWindow? window = null) { - Logger.Debug("Showing focus indicator"); - IWorkspace activeWorkspace = _context.WorkspaceManager.ActiveWorkspace; - window ??= activeWorkspace.LastFocusedWindow; - if (window == null) - { - Logger.Debug("No window to show focus indicator for"); - Hide(); - return; - } + Logger.Verbose("Showing focus indicator"); - // Get the window rectangle. - IWindowState? windowRect = activeWorkspace.TryGetWindowState(window); - if (windowRect == null) + HWND handle = window?.Handle ?? default; + if (handle == default) { - Logger.Error($"Could not find window rectangle for window {window}"); - Hide(); - return; + if (_context.Store.Pick(Pickers.PickLastFocusedWindowHandle()).TryGet(out HWND hwnd)) + { + handle = hwnd; + } + else + { + Logger.Verbose("No last focused window to show focus indicator for"); + Hide(); + return; + } } - if (windowRect.WindowSize == WindowSize.Minimized) + IRectangle? rect = _context.NativeManager.DwmGetWindowRectangle(handle); + if (rect == null) { - Logger.Debug($"Window {window} is minimized"); + Logger.Error($"Could not find window rectangle for window {handle}"); Hide(); return; } IsVisible = true; - _focusIndicatorWindow?.Activate(windowRect); - - // If the fade is enabled, start the timer. - if (_focusIndicatorConfig.FadeEnabled) - { - _dispatcherTimer?.Stop(); - - _dispatcherTimer = new DispatcherTimer(); - _dispatcherTimer.Tick += DispatcherTimer_Tick; - _dispatcherTimer.Interval = _focusIndicatorConfig.FadeTimeout; - _dispatcherTimer.Start(); - } + _focusIndicatorWindow?.Activate(handle, rect); } /// private void Hide() { - Logger.Debug("Hiding focus indicator"); + Logger.Verbose("Hiding focus indicator"); _focusIndicatorWindow?.Hide(_context); IsVisible = false; - - if (_dispatcherTimer != null) - { - _dispatcherTimer.Stop(); - _dispatcherTimer.Tick -= DispatcherTimer_Tick; - } } /// @@ -154,6 +151,12 @@ public void Toggle() } else { + // Reset the last focus start time so the fade timer starts over. + if (_focusIndicatorConfig.FadeEnabled) + { + _lastFocusStartTime = Environment.TickCount; + } + Show(); } } @@ -167,6 +170,12 @@ public void ToggleEnabled() _isEnabled = !_isEnabled; if (_isEnabled) { + // Reset the last focus start time so the fade timer starts over. + if (_focusIndicatorConfig.FadeEnabled) + { + _lastFocusStartTime = Environment.TickCount; + } + Show(); } else @@ -183,11 +192,10 @@ protected virtual void Dispose(bool disposing) if (disposing) { // dispose managed state (managed objects) - _context.WindowManager.WindowFocused -= WindowManager_WindowFocused; - _context.WorkspaceManager.WorkspaceLayoutStarted -= WorkspaceManager_WorkspaceLayoutStarted; - _context.WorkspaceManager.WorkspaceLayoutCompleted -= WorkspaceManager_WorkspaceLayoutCompleted; + _cancellationTokenSource.Dispose(); _focusIndicatorWindow?.Dispose(); _focusIndicatorWindow?.Close(); + _context.WindowManager.WindowFocused -= WindowManager_WindowFocused; } // free unmanaged resources (unmanaged objects) and override finalizer diff --git a/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml b/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml index fd57f7ad6..0c3a57917 100644 --- a/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml +++ b/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml @@ -5,5 +5,18 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + + + + + + \ No newline at end of file diff --git a/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml.cs b/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml.cs index 9cb0a3c63..f9f48a98a 100644 --- a/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml.cs +++ b/src/Whim.FocusIndicator/FocusIndicatorWindow.xaml.cs @@ -1,4 +1,5 @@ -using Windows.Win32.UI.WindowsAndMessaging; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; namespace Whim.FocusIndicator; @@ -27,19 +28,19 @@ public FocusIndicatorWindow(IContext context, FocusIndicatorConfig focusIndicato /// /// Activates the window behind the given window. /// - /// The window to show the indicator for. - public void Activate(IWindowState targetWindowState) + /// The handle of the window to activate behind. + /// The rectangle of the window to activate behind. + public void Activate(HWND handle, IRectangle windowRectangle) { - Logger.Debug("Activating focus indicator window"); - IRectangle focusedWindowRect = targetWindowState.Rectangle; + Logger.Verbose("Activating focus indicator window"); int borderSize = FocusIndicatorConfig.BorderSize; IRectangle borderRect = new Rectangle() { - X = focusedWindowRect.X - borderSize, - Y = focusedWindowRect.Y - borderSize, - Height = focusedWindowRect.Height + (borderSize * 2), - Width = focusedWindowRect.Width + (borderSize * 2) + X = windowRectangle.X - borderSize, + Y = windowRectangle.Y - borderSize, + Height = windowRectangle.Height + (borderSize * 2), + Width = windowRectangle.Width + (borderSize * 2) }; // Prevent the window from being activated. @@ -53,7 +54,7 @@ public void Activate(IWindowState targetWindowState) _window.Handle, WindowSize.Normal, borderRect, - targetWindowState.Window.Handle, + handle, SET_WINDOW_POS_FLAGS.SWP_NOREDRAW | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE ) ); diff --git a/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs b/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs index c33aa7533..b8555f047 100644 --- a/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs +++ b/src/Whim.Tests/Store/WorkspaceSector/WorkspacePickersTests.cs @@ -254,6 +254,69 @@ internal void PickLastFocusedWindow_NoLastFocusedWindow(IContext ctx, MutableRoo Assert.False(result.IsSuccessful); } + [Theory, AutoSubstituteData] + internal void PickLastFocusedWindowHandle_DefaultWorkspace(IContext ctx, MutableRootSector root) + { + // Given the workspaces and windows + Workspace workspace = CreateWorkspace(ctx); + IWindow lastFocusedWindow = Setup_LastFocusedWindow(ctx, root, workspace); + + root.WorkspaceSector.Workspaces = root.WorkspaceSector.Workspaces.SetItem( + workspace.Id, + workspace with + { + LastFocusedWindowHandle = lastFocusedWindow.Handle + } + ); + + // When we get the last focused window handle + Result result = ctx.Store.Pick(Pickers.PickLastFocusedWindowHandle()); + + // Then we get the last focused window handle + Assert.True(result.IsSuccessful); + Assert.Equal(lastFocusedWindow.Handle, result.Value); + } + + [Theory, AutoSubstituteData] + internal void PickLastFocusedWindowHandle_WorkspaceNotFound(IContext ctx, MutableRootSector root) + { + // Given the workspaces and windows exist, but the workspace to search for doesn't exist + Workspace workspace = CreateWorkspace(ctx); + IWindow lastFocusedWindow = Setup_LastFocusedWindow(ctx, root, workspace); + + root.WorkspaceSector.Workspaces = root.WorkspaceSector.Workspaces.SetItem( + workspace.Id, + workspace with + { + LastFocusedWindowHandle = lastFocusedWindow.Handle + } + ); + + Guid workspaceToSearchFor = Guid.NewGuid(); + + // When we get the last focused window handle + Result result = ctx.Store.Pick(Pickers.PickLastFocusedWindowHandle(workspaceToSearchFor)); + + // Then we get an error + Assert.False(result.IsSuccessful); + } + + [Theory, AutoSubstituteData] + internal void PickLastFocusedWindowHandle_NoLastFocusedWindow(IContext ctx, MutableRootSector root) + { + // Given the workspaces and windows, but the last focused window isn't set + Workspace workspace = CreateWorkspace(ctx); + IWindow lastFocusedWindow = Setup_LastFocusedWindow(ctx, root, workspace); + + root.WorkspaceSector.Workspaces = root.WorkspaceSector.Workspaces.SetItem(workspace.Id, workspace); + + // When we get the last focused window handle + Result result = ctx.Store.Pick(Pickers.PickLastFocusedWindowHandle()); + + // Then we get an error + Assert.False(result.IsSuccessful); + } + private static IWindow Setup_WindowPosition(IContext ctx, MutableRootSector root, Workspace workspace) { IMonitor monitor = CreateMonitor((HMONITOR)1); diff --git a/src/Whim/Native/DeferWindowPosHandle.cs b/src/Whim/Native/DeferWindowPosHandle.cs index ad336ac90..c9b0a83ea 100644 --- a/src/Whim/Native/DeferWindowPosHandle.cs +++ b/src/Whim/Native/DeferWindowPosHandle.cs @@ -44,7 +44,7 @@ public sealed class DeferWindowPosHandle : IDisposable /// internal DeferWindowPosHandle(IContext context, IInternalContext internalContext) { - Logger.Debug("Creating new WindowDeferPosHandle"); + Logger.Verbose("Creating new WindowDeferPosHandle"); _context = context; _internalContext = internalContext; } @@ -85,7 +85,9 @@ IEnumerable windowStates /// public void DeferWindowPos(DeferWindowPosState posState, bool forceTwoPasses = false) { - Logger.Debug($"Adding window {posState.Handle} after {posState.HandleInsertAfter} with flags {posState.Flags}"); + Logger.Verbose( + $"Adding window {posState.Handle} after {posState.HandleInsertAfter} with flags {posState.Flags}" + ); if (forceTwoPasses) { @@ -109,11 +111,11 @@ public void DeferWindowPos(DeferWindowPosState posState, bool forceTwoPasses = f /// public void Dispose() { - Logger.Debug("Disposing WindowDeferPosHandle"); + Logger.Verbose("Disposing WindowDeferPosHandle"); if (_windowStates.Count == 0 && _minimizedWindowStates.Count == 0) { - Logger.Debug("No windows to set position for"); + Logger.Verbose("No windows to set position for"); return; } @@ -129,7 +131,7 @@ public void Dispose() } } - Logger.Debug($"Setting window position {numPasses} times for {_windowStates.Count} windows"); + Logger.Verbose($"Setting window position {numPasses} times for {_windowStates.Count} windows"); // Set the window positions for non-minimized windows first, then minimized windows. // This was done to prevent the minimized windows being hidden, and Windows focusing the previous window. @@ -165,7 +167,7 @@ public void Dispose() } } - Logger.Debug("Finished setting window position"); + Logger.Verbose("Finished setting window position"); } private void SetWindowPos(DeferWindowPosState source) diff --git a/src/Whim/Native/NativeManager.cs b/src/Whim/Native/NativeManager.cs index 1b5b0bc9b..b945ce054 100644 --- a/src/Whim/Native/NativeManager.cs +++ b/src/Whim/Native/NativeManager.cs @@ -77,7 +77,7 @@ public bool MinimizeWindow(HWND hwnd) public bool ShowWindowNoActivate(HWND hwnd) { - Logger.Debug($"Showing window HWND {hwnd} no activate"); + Logger.Verbose($"Showing window HWND {hwnd} no activate"); return (bool)PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOWNOACTIVATE); } diff --git a/src/Whim/Store/Store.cs b/src/Whim/Store/Store.cs index 640f87c31..1b203b3e7 100644 --- a/src/Whim/Store/Store.cs +++ b/src/Whim/Store/Store.cs @@ -106,7 +106,7 @@ public TResult Pick(Picker picker) { return Task.Run(() => { - Logger.Debug($"Entering task, executing picker {picker}"); + Logger.Verbose($"Entering task, executing picker {picker}"); try { @@ -120,7 +120,7 @@ public TResult Pick(Picker picker) }).Result; } - Logger.Debug($"Executing picker {picker}"); + Logger.Verbose($"Executing picker {picker}"); return PickFn(picker); } @@ -133,7 +133,7 @@ public TResult Pick(PurePicker picker) { return Task.Run(() => { - Logger.Debug($"Entering task, executing picker {picker}"); + Logger.Verbose($"Entering task, executing picker {picker}"); try { _lock.EnterReadLock(); @@ -146,7 +146,7 @@ public TResult Pick(PurePicker picker) }).Result; } - Logger.Debug($"Executing picker {picker}"); + Logger.Verbose($"Executing picker {picker}"); return PurePickFn(picker); } diff --git a/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs b/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs index 28be9234b..bd182f8b2 100644 --- a/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs +++ b/src/Whim/Store/WorkspaceSector/WorkspacePickers.cs @@ -80,6 +80,11 @@ private static Result BaseWorkspacePicker( Func> operation ) { + if (workspaceId == default) + { + workspaceId = PickActiveWorkspaceId()(rootSector); + } + if (!rootSector.WorkspaceSector.Workspaces.TryGetValue(workspaceId, out Workspace? workspace)) { return Result.FromException(StoreExceptions.WorkspaceNotFound(workspaceId)); @@ -145,8 +150,8 @@ internal static IEnumerable GetWorkspaceWindows(IRootSector rootSector, /// /// Get the last focused window in the provided workspace. /// - /// The workspace to get the last focused window for. - public static PurePicker> PickLastFocusedWindow(WorkspaceId workspaceId) => + /// The workspace to get the last focused window for. Defaults to the active workspace + public static PurePicker> PickLastFocusedWindow(WorkspaceId workspaceId = default) => (IRootSector rootSector) => BaseWorkspacePicker( workspaceId, @@ -162,6 +167,27 @@ public static PurePicker> PickLastFocusedWindow(WorkspaceId work } ); + /// + /// Get the last focused window handle in the provided workspace. + /// + /// The workspace to get the last focused window handle for. Defaults to the active workspace + /// + public static PurePicker> PickLastFocusedWindowHandle(WorkspaceId workspaceId = default) => + (IRootSector rootSector) => + BaseWorkspacePicker( + workspaceId, + rootSector, + workspace => + { + if (workspace.LastFocusedWindowHandle.IsNull) + { + return Result.FromException(new WhimException("No last focused window in workspace")); + } + + return Result.FromValue(workspace.LastFocusedWindowHandle); + } + ); + /// /// Get the window position in the provided workspace. ///