-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WtqService] Simplified orchestration of app toggling
- Loading branch information
Showing
1 changed file
with
91 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,134 +1,123 @@ | ||
using Microsoft.Extensions.Hosting; | ||
using Wtq.Events; | ||
using Wtq.Services; | ||
|
||
namespace Wtq; | ||
|
||
public sealed class WtqService( | ||
ILogger<WtqService> log, | ||
IOptionsMonitor<WtqOptions> opts, | ||
IWtqAppRepo appRepo, | ||
IWtqBus bus, | ||
IWtqFocusTracker focusTracker) | ||
: IDisposable, IHostedService | ||
/// <summary> | ||
/// Orchestrates toggling on- and off of apps, and sending focus to the right window. | ||
/// </summary> | ||
// TODO: Better name. | ||
public sealed class WtqService : IDisposable, IAsyncInitializable | ||
{ | ||
private readonly ILogger<WtqService> _log = Guard.Against.Null(log); | ||
private readonly IOptionsMonitor<WtqOptions> _opts = Guard.Against.Null(opts); | ||
private readonly IWtqAppRepo _appRepo = Guard.Against.Null(appRepo); | ||
private readonly IWtqBus _bus = Guard.Against.Null(bus); | ||
private readonly IWtqFocusTracker _focusTracker = Guard.Against.Null(focusTracker); | ||
private readonly SemaphoreSlim _lock = new(1); | ||
private readonly ILogger<WtqService> _log; | ||
private readonly IOptionsMonitor<WtqOptions> _opts; | ||
private readonly IWtqAppRepo _appRepo; | ||
private readonly IWtqBus _bus; | ||
private readonly WtqSemaphoreSlim _lock = new(1, 1); | ||
|
||
private WtqWindow? _lastNonWtqWindow; | ||
|
||
public WtqService( | ||
ILogger<WtqService> log, | ||
IOptionsMonitor<WtqOptions> opts, | ||
IWtqAppRepo appRepo, | ||
IWtqBus bus) | ||
{ | ||
_log = Guard.Against.Null(log); | ||
_opts = Guard.Against.Null(opts); | ||
_appRepo = Guard.Against.Null(appRepo); | ||
_bus = Guard.Against.Null(bus); | ||
|
||
private WtqApp? _lastOpen; | ||
private WtqApp? _open; | ||
_bus.OnEvent<WtqAppToggledEvent>(OnAppToggledEventAsync); | ||
_bus.OnEvent<WtqWindowFocusChangedEvent>(OnWindowFocusChangedEventAsync); | ||
} | ||
|
||
public Task InitializeAsync() | ||
{ | ||
// TODO: Currently necessary to make sure this service is constructed. | ||
return Task.CompletedTask; | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_lock.Dispose(); | ||
} | ||
|
||
public Task StartAsync(CancellationToken cancellationToken) | ||
/// <summary> | ||
/// Handles "toggle" events, e.g. where the user pressed a hotkey. | ||
/// </summary> | ||
private async Task OnAppToggledEventAsync(WtqAppToggledEvent ev) | ||
{ | ||
_log.LogInformation("Starting"); | ||
|
||
_bus.OnEvent<WtqToggleAppEvent>(HandleToggleAppEventAsync); | ||
_bus.OnEvent<WtqAppFocusEvent>(HandleAppFocusEventAsync); | ||
// Wait for service-wide lock. | ||
using var l = await _lock.WaitOneSecondAsync().NoCtx(); | ||
|
||
return Task.CompletedTask; | ||
} | ||
// "Switching apps" | ||
// If a previously toggled app (that is not the to-be-toggled app) is still open, close it first. | ||
var open = _appRepo.GetOpen(); | ||
if (open != null && open != ev.App) | ||
{ | ||
_log.LogInformation("Closing app '{AppClosing}', opening app '{AppOpening}'", open, ev.App); | ||
await open.CloseAsync(ToggleModifiers.SwitchingApps).NoCtx(); | ||
await ev.App.OpenAsync(ToggleModifiers.SwitchingApps).NoCtx(); | ||
return; | ||
} | ||
|
||
public Task StopAsync(CancellationToken cancellationToken) | ||
{ | ||
_log.LogInformation("Stopping"); | ||
// "Toggling app" | ||
if (ev.App.IsOpen) | ||
{ | ||
_log.LogInformation("Closing previously open app '{App}'", ev.App); | ||
|
||
return Task.CompletedTask; | ||
} | ||
// Close app. | ||
ev.App.CloseAsync().NoCtx(); | ||
|
||
private async Task HandleAppFocusEventAsync(WtqAppFocusEvent ev) | ||
{ | ||
// If focus moved to a different window, toggle out the current one (if there is an active app, and it's configured as such). | ||
if (ev.App != null && | ||
ev.App == _open && | ||
!ev.GainedFocus && | ||
_opts.CurrentValue.GetHideOnFocusLostForApp(ev.App.Options)) | ||
// Bring focus back to last non-WTQ app. | ||
await (_lastNonWtqWindow?.BringToForegroundAsync() ?? Task.CompletedTask).NoCtx(); | ||
} | ||
else | ||
{ | ||
await _open.CloseAsync().ConfigureAwait(false); | ||
_lastOpen = _open; | ||
_open = null; | ||
_log.LogInformation("Opening previously closed app '{App}'", ev.App); | ||
|
||
// Open app. | ||
ev.App.OpenAsync().NoCtx(); | ||
} | ||
} | ||
|
||
private async Task HandleToggleAppEventAsync(WtqToggleAppEvent ev) | ||
/// <summary> | ||
/// Handles events where the focus moved to another window. | ||
/// </summary> | ||
private async Task OnWindowFocusChangedEventAsync(WtqWindowFocusChangedEvent ev) | ||
{ | ||
try | ||
{ | ||
await _lock.WaitAsync().ConfigureAwait(false); | ||
Guard.Against.Null(ev); | ||
|
||
var app = ev.App; | ||
// Wait for service-wide lock. | ||
using var l = await _lock.WaitOneSecondAsync().NoCtx(); | ||
|
||
// If the action does not point to a single app, toggle the most recent one. | ||
if (app == null) | ||
{ | ||
// If we still have an app open, close it now. | ||
if (_open != null) | ||
{ | ||
await _open.CloseAsync().ConfigureAwait(false); | ||
_lastOpen = _open; | ||
_open = null; | ||
await _focusTracker.FocusLastNonWtqAppAsync().NoCtx(); | ||
return; | ||
} | ||
|
||
// If we don't yet have an app open, open either the most recently used one, or the first one. | ||
if (_lastOpen == null) | ||
{ | ||
// TODO | ||
var first = _appRepo.Apps.FirstOrDefault(); | ||
if (first != null) | ||
{ | ||
await first.OpenAsync().ConfigureAwait(false); | ||
} | ||
|
||
_open = first; | ||
_lastOpen = first; | ||
return; | ||
} | ||
|
||
_open = _lastOpen; | ||
await _open.OpenAsync().ConfigureAwait(false); | ||
return; | ||
} | ||
// Look for apps that are attached to the windows that got- and lost focus, respectively. | ||
var appGotFocus = ev.GotFocusWindow != null ? _appRepo.GetByWindow(ev.GotFocusWindow) : null; | ||
var appLostFocus = ev.LostFocusWindow != null ? _appRepo.GetByWindow(ev.LostFocusWindow) : null; | ||
|
||
if (_open != null) | ||
{ | ||
if (_open == app) | ||
{ | ||
await app.CloseAsync().ConfigureAwait(false); | ||
_lastOpen = _open; | ||
_open = null; | ||
await _focusTracker.FocusLastNonWtqAppAsync().NoCtx(); | ||
} | ||
else | ||
{ | ||
await _open.CloseAsync(ToggleModifiers.SwitchingApps).ConfigureAwait(false); | ||
await app.OpenAsync(ToggleModifiers.SwitchingApps).ConfigureAwait(false); | ||
|
||
_open = app; | ||
} | ||
// If the window that just LOST focus is NOT managed by WTQ, store it for giving back focus to later. | ||
if (ev.LostFocusWindow != null && appLostFocus == null) | ||
{ | ||
_lastNonWtqWindow = ev.LostFocusWindow; | ||
} | ||
|
||
return; | ||
} | ||
// If the app that GOT focus is a WTQ app, toggling will be done in the "app toggled" event handler. | ||
if (appGotFocus != null) | ||
{ | ||
return; | ||
} | ||
|
||
// Open the specified app. | ||
_log.LogInformation("Toggling app {App}", app); | ||
if (await app.OpenAsync().ConfigureAwait(false)) | ||
// If the app that LOST focus is a WTQ app, toggle it off (depending on configuration). | ||
if (appLostFocus != null) | ||
{ | ||
// If the app has "hide on focus lost" set to FALSE, well, don't hide. | ||
if (!_opts.CurrentValue.GetHideOnFocusLostForApp(appLostFocus.Options)) | ||
{ | ||
_open = app; | ||
return; | ||
} | ||
} | ||
finally | ||
{ | ||
_lock.Release(); | ||
|
||
_log.LogInformation("App '{App}' lost focus, closing", appLostFocus); | ||
await appLostFocus.CloseAsync().NoCtx(); | ||
} | ||
} | ||
} |