diff --git a/docs/configure/plugins/bar.md b/docs/configure/plugins/bar.md index 9e4b3c84c..245d5809d 100644 --- a/docs/configure/plugins/bar.md +++ b/docs/configure/plugins/bar.md @@ -79,7 +79,10 @@ The `FocusedWindowWidget` displays the title of the focused window. ### Workspace Widget -The `WorkspaceWidget` displays the name of the current workspace. +The `WorkspaceWidget` displays: + +- the active workspace on the bar's monitor +- the workspaces which can be opened on the monitor - i.e., [sticky workspaces for the monitor](../core/workspaces.md#sticky-workspaces) and non-sticky workspaces ### Tree Layout Widget diff --git a/docs/script/plugins/bar.md b/docs/script/plugins/bar.md index da21ec30b..96b90282b 100644 --- a/docs/script/plugins/bar.md +++ b/docs/script/plugins/bar.md @@ -34,6 +34,8 @@ List<BarComponent> rightComponents = new() - [FocusedWindowWidget](<xref:Whim.Bar.FocusedWindowWidget.CreateComponent(System.Func{Whim.IWindow,System.String})>) - [WorkspaceWidget](xref:Whim.Bar.WorkspaceWidget.CreateComponent) +More information about each widget can be found [here](../../configure/plugins/bar.md#widgets). + ## Example Config ```csharp diff --git a/src/Whim.Bar.Tests/Workspace/WorkspaceWidgetViewModelTests.cs b/src/Whim.Bar.Tests/Workspace/WorkspaceWidgetViewModelTests.cs index 17d024808..fdd20d771 100644 --- a/src/Whim.Bar.Tests/Workspace/WorkspaceWidgetViewModelTests.cs +++ b/src/Whim.Bar.Tests/Workspace/WorkspaceWidgetViewModelTests.cs @@ -1,4 +1,3 @@ -using AutoFixture; using NSubstitute; using Whim.TestUtils; using Windows.Win32.Graphics.Gdi; @@ -6,282 +5,224 @@ namespace Whim.Bar.Tests; -public class WorkspaceWidgetViewModelCustomization : ICustomization -{ - public void Customize(IFixture fixture) - { - IContext context = fixture.Freeze<IContext>(); - - // The workspace manager should have a single workspace - using IWorkspace workspace = fixture.Create<IWorkspace>(); - workspace.Id.Returns(Guid.NewGuid()); - context.WorkspaceManager.GetEnumerator().Returns((_) => new List<IWorkspace>() { workspace }.GetEnumerator()); - - fixture.Inject(context); - } -} - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] public class WorkspaceWidgetViewModelTests { - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceAdded_AlreadyExists(IContext context, IMonitor monitor) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void WorkspaceWidgetViewModel_Ctor(IContext ctx, MutableRootSector root) { // Given - IWorkspace workspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)100); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); + + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace2); // When - context.WorkspaceManager.WorkspaceAdded += Raise.Event<EventHandler<WorkspaceAddedEventArgs>>( - context.WorkspaceManager, - new WorkspaceAddedEventArgs() { Workspace = workspace } - ); + WorkspaceWidgetViewModel sut = new(ctx, monitor); // Then - Assert.Single(viewModel.Workspaces); - Assert.Equal(workspace, viewModel.Workspaces[0].Workspace); - context.Butler.Pantry.Received(1).GetMonitorForWorkspace(workspace); + Assert.Equal(2, sut.Workspaces.Count); + Assert.Same(workspace1, sut.Workspaces[0].Workspace); + Assert.Same(workspace2, sut.Workspaces[1].Workspace); + Assert.False(sut.Workspaces[0].ActiveOnMonitor); + Assert.True(sut.Workspaces[1].ActiveOnMonitor); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceAdded(IContext context, IMonitor monitor, IWorkspace addedWorkspace) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void WorkspaceAdded(IContext ctx, MutableRootSector root) { // Given - IWorkspace workspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)100); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); - // When - context.WorkspaceManager.WorkspaceAdded += Raise.Event<EventHandler<WorkspaceAddedEventArgs>>( - context.WorkspaceManager, - new WorkspaceAddedEventArgs() { Workspace = addedWorkspace } - ); + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace2); - // Then - Assert.Equal(2, viewModel.Workspaces.Count); - context.Butler.Pantry.Received(1).GetMonitorForWorkspace(workspace); - context.Butler.Pantry.Received(1).GetMonitorForWorkspace(addedWorkspace); - } + WorkspaceWidgetViewModel sut = new(ctx, monitor); - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceRemoved(IContext context, IMonitor monitor) - { - // Given - IWorkspace workspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + Workspace workspace3 = StoreTestUtils.CreateWorkspace(ctx); + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace3); // When - context.WorkspaceManager.WorkspaceRemoved += Raise.Event<EventHandler<WorkspaceRemovedEventArgs>>( - context.WorkspaceManager, - new WorkspaceRemovedEventArgs() { Workspace = workspace } - ); + root.WorkspaceSector.QueueEvent(new WorkspaceAddedEventArgs() { Workspace = workspace3 }); + root.DispatchEvents(); // Then - Assert.Empty(viewModel.Workspaces); + Assert.Equal(3, sut.Workspaces.Count); + Assert.Same(workspace1, sut.Workspaces[0].Workspace); + Assert.Same(workspace2, sut.Workspaces[1].Workspace); + Assert.Same(workspace3, sut.Workspaces[2].Workspace); + Assert.False(sut.Workspaces[0].ActiveOnMonitor); + Assert.True(sut.Workspaces[1].ActiveOnMonitor); + Assert.False(sut.Workspaces[2].ActiveOnMonitor); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceRemoved_DoesNotExist( - IContext context, - IMonitor monitor, - IWorkspace removedWorkspace - ) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void WorkspaceRemoved(IContext ctx, MutableRootSector root) { // Given - IWorkspace workspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)100); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace3 = StoreTestUtils.CreateWorkspace(ctx); - // When - context.WorkspaceManager.WorkspaceRemoved += Raise.Event<EventHandler<WorkspaceRemovedEventArgs>>( - context.WorkspaceManager, - new WorkspaceRemovedEventArgs() { Workspace = removedWorkspace } - ); + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2, workspace3); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace2); - // Then - Assert.Single(viewModel.Workspaces); - Assert.Equal(workspace, viewModel.Workspaces[0].Workspace); - } - - #region WorkspaceManager_MonitorWorkspaceChanged - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_MonitorWorkspaceChanged_Deactivate( - IContext context, - IMonitor monitor, - IWorkspace currentWorkspace - ) - { - // Given - IWorkspace previousWorkspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + WorkspaceWidgetViewModel sut = new(ctx, monitor); // When - context.Butler.MonitorWorkspaceChanged += Raise.Event<EventHandler<MonitorWorkspaceChangedEventArgs>>( - context.WorkspaceManager, - new MonitorWorkspaceChangedEventArgs() - { - Monitor = monitor, - PreviousWorkspace = previousWorkspace, - CurrentWorkspace = currentWorkspace, - } - ); + root.WorkspaceSector.Workspaces = root.WorkspaceSector.Workspaces.Remove(workspace1.Id); + root.WorkspaceSector.QueueEvent(new WorkspaceRemovedEventArgs() { Workspace = workspace1 }); + root.DispatchEvents(); // Then - WorkspaceModel model = viewModel.Workspaces[0]; - Assert.False(model.ActiveOnMonitor); + Assert.Equal(2, sut.Workspaces.Count); + Assert.Same(workspace2, sut.Workspaces[0].Workspace); + Assert.Same(workspace3, sut.Workspaces[1].Workspace); + Assert.True(sut.Workspaces[0].ActiveOnMonitor); + Assert.False(sut.Workspaces[1].ActiveOnMonitor); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_MonitorWorkspaceChanged_Activate( - IContext context, - IMonitor monitor, - IWorkspace addedWorkspace - ) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void MonitorWorkspaceChanged_WrongMonitor(IContext ctx, MutableRootSector root) { // Given - monitor.Handle.Returns((HMONITOR)100); + IMonitor monitor1 = StoreTestUtils.CreateMonitor((HMONITOR)100); + IMonitor monitor2 = StoreTestUtils.CreateMonitor((HMONITOR)200); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); - IWorkspace workspace = context.WorkspaceManager.First(); - context.Butler.Pantry.GetMonitorForWorkspace(workspace).Returns(monitor); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor1, workspace1); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor2, workspace2); - // Add workspace - context.WorkspaceManager.WorkspaceAdded += Raise.Event<EventHandler<WorkspaceAddedEventArgs>>( - context.WorkspaceManager, - new WorkspaceAddedEventArgs() { Workspace = addedWorkspace } - ); - - // Verify that the correct workspace is active on the monitor - WorkspaceModel existingModel = viewModel.Workspaces[0]; - WorkspaceModel addedWorkspaceModel = viewModel.Workspaces[1]; - Assert.True(existingModel.ActiveOnMonitor); - Assert.False(addedWorkspaceModel.ActiveOnMonitor); + WorkspaceWidgetViewModel sut = new(ctx, monitor1); // When - context.Butler.MonitorWorkspaceChanged += Raise.Event<EventHandler<MonitorWorkspaceChangedEventArgs>>( - context.WorkspaceManager, - new MonitorWorkspaceChangedEventArgs() - { - Monitor = monitor, - PreviousWorkspace = existingModel.Workspace, - CurrentWorkspace = addedWorkspaceModel.Workspace, - } + root.MapSector.QueueEvent( + new MonitorWorkspaceChangedEventArgs() { Monitor = monitor2, CurrentWorkspace = workspace2 } ); + root.DispatchEvents(); // Then - Assert.False(existingModel.ActiveOnMonitor); - Assert.True(addedWorkspaceModel.ActiveOnMonitor); + Assert.Equal(2, sut.Workspaces.Count); + Assert.Same(workspace1, sut.Workspaces[0].Workspace); + Assert.Same(workspace2, sut.Workspaces[1].Workspace); + Assert.True(sut.Workspaces[0].ActiveOnMonitor); + Assert.False(sut.Workspaces[1].ActiveOnMonitor); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_MonitorWorkspaceChanged_DifferentMonitor( - IContext context, - IMonitor monitor, - IWorkspace addedWorkspace, - IMonitor otherMonitor - ) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void MonitorWorkspaceChanged_CorrectMonitor(IContext ctx, MutableRootSector root) { // Given - monitor.Handle.Returns((HMONITOR)100); - - IWorkspace workspace = context.WorkspaceManager.First(); - context.Butler.Pantry.GetMonitorForWorkspace(workspace).Returns(monitor); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)100); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); - // Add workspace - context.WorkspaceManager.WorkspaceAdded += Raise.Event<EventHandler<WorkspaceAddedEventArgs>>( - context.WorkspaceManager, - new WorkspaceAddedEventArgs() { Workspace = addedWorkspace } - ); + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace1); - // Verify that the correct workspace is active on the monitor - WorkspaceModel existingModel = viewModel.Workspaces[0]; - WorkspaceModel addedWorkspaceModel = viewModel.Workspaces[1]; - Assert.True(existingModel.ActiveOnMonitor); - Assert.False(addedWorkspaceModel.ActiveOnMonitor); + WorkspaceWidgetViewModel sut = new(ctx, monitor); // When - context.Butler.MonitorWorkspaceChanged += Raise.Event<EventHandler<MonitorWorkspaceChangedEventArgs>>( - context.WorkspaceManager, - new MonitorWorkspaceChangedEventArgs() - { - Monitor = otherMonitor, - PreviousWorkspace = existingModel.Workspace, - CurrentWorkspace = addedWorkspaceModel.Workspace, - } + root.MapSector.QueueEvent( + new MonitorWorkspaceChangedEventArgs() { Monitor = monitor, CurrentWorkspace = workspace2 } ); + root.DispatchEvents(); // Then - Assert.True(existingModel.ActiveOnMonitor); - Assert.False(addedWorkspaceModel.ActiveOnMonitor); + Assert.Equal(2, sut.Workspaces.Count); + Assert.Same(workspace1, sut.Workspaces[0].Workspace); + Assert.Same(workspace2, sut.Workspaces[1].Workspace); + Assert.False(sut.Workspaces[0].ActiveOnMonitor); + Assert.True(sut.Workspaces[1].ActiveOnMonitor); } - #endregion - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceRenamed_ExistingWorkspace(IContext context, IMonitor monitor) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void WorkspaceRenamed_WrongMonitor(IContext ctx, MutableRootSector root) { // Given - IWorkspace workspace = context.WorkspaceManager.First(); - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor1 = StoreTestUtils.CreateMonitor((HMONITOR)100); + IMonitor monitor2 = StoreTestUtils.CreateMonitor((HMONITOR)200); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); + + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor1, workspace1); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor2, workspace2); + + WorkspaceWidgetViewModel sut = new(ctx, monitor1); + WorkspaceModel workspaceModel = sut.Workspaces[0]; // When - // Then - Assert.PropertyChanged( - viewModel.Workspaces[0], - nameof(WorkspaceModel.Name), + CustomAssert.DoesNotPropertyChange( + h => workspaceModel.PropertyChanged += h, + h => workspaceModel.PropertyChanged -= h, () => { - context.WorkspaceManager.WorkspaceRenamed += Raise.Event<EventHandler<WorkspaceRenamedEventArgs>>( - context.WorkspaceManager, - new WorkspaceRenamedEventArgs() { Workspace = workspace, PreviousName = "Old Name" } + root.WorkspaceSector.QueueEvent( + new WorkspaceRenamedEventArgs() { Workspace = workspace2, PreviousName = "Old Name" } ); + root.DispatchEvents(); } ); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - public void WorkspaceManager_WorkspaceRenamed_NonExistingWorkspace( - IContext context, - IMonitor monitor, - IWorkspace renamedWorkspace - ) + [Theory, AutoSubstituteData<StoreCustomization>] + internal void WorkspaceRenamed_CorrectMonitor(IContext ctx, MutableRootSector root) { // Given - WorkspaceWidgetViewModel viewModel = new(context, monitor); + IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)100); + Workspace workspace1 = StoreTestUtils.CreateWorkspace(ctx); + Workspace workspace2 = StoreTestUtils.CreateWorkspace(ctx); - // Verify that property changed is not raised + StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace1, workspace2); + StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace1); - bool propertyChangedRaised = false; - WorkspaceModel model = viewModel.Workspaces[0]; + WorkspaceWidgetViewModel sut = new(ctx, monitor); + WorkspaceModel workspaceModel = sut.Workspaces[0]; // When - model.PropertyChanged += (sender, args) => propertyChangedRaised = true; - - context.WorkspaceManager.WorkspaceRenamed += Raise.Event<EventHandler<WorkspaceRenamedEventArgs>>( - context.WorkspaceManager, - new WorkspaceRenamedEventArgs() { Workspace = renamedWorkspace, PreviousName = "Old Name" } + Assert.PropertyChanged( + sut.Workspaces[0], + nameof(workspaceModel.Name), + () => + { + root.WorkspaceSector.QueueEvent( + new WorkspaceRenamedEventArgs() { Workspace = workspace1, PreviousName = "Old Name" } + ); + root.DispatchEvents(); + } ); - - // Then - Assert.False(propertyChangedRaised); } - [Theory, AutoSubstituteData<WorkspaceWidgetViewModelCustomization>] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Usage", - "NS5000:Received check.", - Justification = "The analyzer is wrong" - )] - public void Dispose(IContext context, IMonitor monitor) + [Theory, AutoSubstituteData] + public void Dispose(IContext ctx, IMonitor monitor) { // Given - WorkspaceWidgetViewModel viewModel = new(context, monitor); + WorkspaceWidgetViewModel sut = new(ctx, monitor); // When - viewModel.Dispose(); + sut.Dispose(); // Then - context.WorkspaceManager.Received(1).WorkspaceAdded -= Arg.Any<EventHandler<WorkspaceAddedEventArgs>>(); - context.WorkspaceManager.Received(1).WorkspaceRemoved -= Arg.Any<EventHandler<WorkspaceRemovedEventArgs>>(); - context.Butler.Received(1).MonitorWorkspaceChanged -= Arg.Any<EventHandler<MonitorWorkspaceChangedEventArgs>>(); - context.WorkspaceManager.Received(1).WorkspaceRenamed -= Arg.Any<EventHandler<WorkspaceRenamedEventArgs>>(); + ctx.Store.WorkspaceEvents.Received(1).WorkspaceAdded += Arg.Any<EventHandler<WorkspaceAddedEventArgs>>(); + ctx.Store.WorkspaceEvents.Received(1).WorkspaceRemoved += Arg.Any<EventHandler<WorkspaceRemovedEventArgs>>(); + ctx.Store.MapEvents.Received(1).MonitorWorkspaceChanged += Arg.Any< + EventHandler<MonitorWorkspaceChangedEventArgs> + >(); + ctx.Store.WorkspaceEvents.Received(1).WorkspaceRenamed += Arg.Any<EventHandler<WorkspaceRenamedEventArgs>>(); + + ctx.Store.WorkspaceEvents.Received(1).WorkspaceAdded -= Arg.Any<EventHandler<WorkspaceAddedEventArgs>>(); + ctx.Store.WorkspaceEvents.Received(1).WorkspaceRemoved -= Arg.Any<EventHandler<WorkspaceRemovedEventArgs>>(); + ctx.Store.MapEvents.Received(1).MonitorWorkspaceChanged -= Arg.Any< + EventHandler<MonitorWorkspaceChangedEventArgs> + >(); + ctx.Store.WorkspaceEvents.Received(1).WorkspaceRenamed -= Arg.Any<EventHandler<WorkspaceRenamedEventArgs>>(); } } diff --git a/src/Whim.Bar/Workspace/WorkspaceWidgetViewModel.cs b/src/Whim.Bar/Workspace/WorkspaceWidgetViewModel.cs index 1088ed66d..512874f63 100644 --- a/src/Whim.Bar/Workspace/WorkspaceWidgetViewModel.cs +++ b/src/Whim.Bar/Workspace/WorkspaceWidgetViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace Whim.Bar; @@ -31,44 +32,40 @@ public WorkspaceWidgetViewModel(IContext context, IMonitor monitor) _context = context; Monitor = monitor; - _context.WorkspaceManager.WorkspaceAdded += WorkspaceManager_WorkspaceAdded; - _context.WorkspaceManager.WorkspaceRemoved += WorkspaceManager_WorkspaceRemoved; - _context.Butler.MonitorWorkspaceChanged += Butler_MonitorWorkspaceChanged; - _context.WorkspaceManager.WorkspaceRenamed += WorkspaceManager_WorkspaceRenamed; + _context.Store.WorkspaceEvents.WorkspaceAdded += WorkspaceEvents_WorkspaceAdded; + _context.Store.WorkspaceEvents.WorkspaceRemoved += WorkspaceEvents_WorkspaceRemoved; + _context.Store.MapEvents.MonitorWorkspaceChanged += MapEvents_MonitorWorkspaceChanged; + _context.Store.WorkspaceEvents.WorkspaceRenamed += WorkspaceEvents_WorkspaceRenamed; - // Populate the list of workspaces - foreach (IWorkspace workspace in _context.WorkspaceManager) - { - IMonitor? monitorForWorkspace = _context.Butler.Pantry.GetMonitorForWorkspace(workspace); - Workspaces.Add(new WorkspaceModel(context, this, workspace, Monitor.Handle == monitorForWorkspace?.Handle)); - } + UpdateWorkspacesCollection(); } - private void WorkspaceManager_WorkspaceAdded(object? sender, WorkspaceEventArgs args) + private void UpdateWorkspacesCollection() { - if (Workspaces.Any(model => model.Workspace.Id == args.Workspace.Id)) - { - return; - } + Workspaces.Clear(); - IMonitor? monitorForWorkspace = _context.Butler.Pantry.GetMonitorForWorkspace(args.Workspace); - Workspaces.Add( - new WorkspaceModel(_context, this, args.Workspace, Monitor.Handle == monitorForWorkspace?.Handle) - ); - } + IReadOnlyList<IWorkspace> workspaces = + _context.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(Monitor.Handle)).ValueOrDefault ?? []; - private void WorkspaceManager_WorkspaceRemoved(object? sender, WorkspaceEventArgs args) - { - WorkspaceModel? workspaceModel = Workspaces.FirstOrDefault(model => model.Workspace.Id == args.Workspace.Id); - if (workspaceModel == null) + foreach (IWorkspace workspace in workspaces) { - return; - } + IMonitor? monitorForWorkspace = _context + .Store.Pick(Pickers.PickMonitorByWorkspace(workspace.Id)) + .ValueOrDefault; - Workspaces.Remove(workspaceModel); + Workspaces.Add( + new WorkspaceModel(_context, this, workspace, Monitor.Handle == monitorForWorkspace?.Handle) + ); + } } - private void Butler_MonitorWorkspaceChanged(object? sender, MonitorWorkspaceChangedEventArgs args) + private void WorkspaceEvents_WorkspaceAdded(object? sender, WorkspaceEventArgs args) => + UpdateWorkspacesCollection(); + + private void WorkspaceEvents_WorkspaceRemoved(object? sender, WorkspaceEventArgs args) => + UpdateWorkspacesCollection(); + + private void MapEvents_MonitorWorkspaceChanged(object? sender, MonitorWorkspaceChangedEventArgs args) { if (args.Monitor.Handle != Monitor.Handle) { @@ -81,7 +78,7 @@ private void Butler_MonitorWorkspaceChanged(object? sender, MonitorWorkspaceChan } } - private void WorkspaceManager_WorkspaceRenamed(object? sender, WorkspaceRenamedEventArgs e) + private void WorkspaceEvents_WorkspaceRenamed(object? sender, WorkspaceRenamedEventArgs e) { WorkspaceModel? workspace = Workspaces.FirstOrDefault(m => m.Workspace.Id == e.Workspace.Id); if (workspace == null) @@ -100,10 +97,10 @@ protected virtual void Dispose(bool disposing) if (disposing) { // dispose managed state (managed objects) - _context.WorkspaceManager.WorkspaceAdded -= WorkspaceManager_WorkspaceAdded; - _context.WorkspaceManager.WorkspaceRemoved -= WorkspaceManager_WorkspaceRemoved; - _context.Butler.MonitorWorkspaceChanged -= Butler_MonitorWorkspaceChanged; - _context.WorkspaceManager.WorkspaceRenamed -= WorkspaceManager_WorkspaceRenamed; + _context.Store.WorkspaceEvents.WorkspaceAdded -= WorkspaceEvents_WorkspaceAdded; + _context.Store.WorkspaceEvents.WorkspaceRemoved -= WorkspaceEvents_WorkspaceRemoved; + _context.Store.MapEvents.MonitorWorkspaceChanged -= MapEvents_MonitorWorkspaceChanged; + _context.Store.WorkspaceEvents.WorkspaceRenamed -= WorkspaceEvents_WorkspaceRenamed; } // free unmanaged resources (unmanaged objects) and override finalizer diff --git a/src/Whim.Tests/Store/MapSector/MapPickersTests.cs b/src/Whim.Tests/Store/MapSector/MapPickersTests.cs index fadbd6c76..ccef7cb0d 100644 --- a/src/Whim.Tests/Store/MapSector/MapPickersTests.cs +++ b/src/Whim.Tests/Store/MapSector/MapPickersTests.cs @@ -422,7 +422,7 @@ internal void PickExplicitStickyMonitorIndicesByWorkspace_Success(IContext ctx, } } -public class PickValidMonitorForWorkspaceTests +public class PickValidMonitorByWorkspaceTests { [Theory, AutoSubstituteData<StoreCustomization>] internal void TargetMonitorIsValid(IContext ctx, MutableRootSector root) @@ -438,7 +438,7 @@ internal void TargetMonitorIsValid(IContext ctx, MutableRootSector root) ); // When we get the monitor - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(workspace.Id, (HMONITOR)1)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(workspace.Id, (HMONITOR)1)); // Then we get the target monitor Assert.True(result.IsSuccessful); @@ -464,7 +464,7 @@ internal void FallbackToLastMonitor(IContext ctx, MutableRootSector root) ); // When we try to use monitor 1 - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(workspace.Id, (HMONITOR)1)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(workspace.Id, (HMONITOR)1)); // Then we get monitor 2 since it's the last valid monitor used Assert.True(result.IsSuccessful); @@ -490,7 +490,7 @@ internal void FallbackToFirstAvailableMonitor(IContext ctx, MutableRootSector ro ); // When we try to use monitor 2 - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(workspace.Id, (HMONITOR)2)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(workspace.Id, (HMONITOR)2)); // Then we get monitor 1 as it's the first valid monitor Assert.True(result.IsSuccessful); @@ -513,7 +513,7 @@ internal void NoValidMonitors(IContext ctx, MutableRootSector root) ); // When we try to get a valid monitor - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(workspace.Id)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(workspace.Id)); // Then we get an error since there are no valid monitors and no fallback monitors Assert.False(result.IsSuccessful); @@ -531,7 +531,7 @@ internal void UseActiveMonitorWhenNoTargetSpecified(IContext ctx, MutableRootSec root.MonitorSector.ActiveMonitorHandle = (HMONITOR)2; // When we don't specify a target monitor - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(workspace.Id)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(workspace.Id)); // Then we get the active monitor Assert.True(result.IsSuccessful); @@ -545,10 +545,110 @@ internal void WorkspaceDoesNotExist(IContext ctx) Guid nonExistentWorkspaceId = Guid.NewGuid(); // When we try to get a valid monitor - var result = ctx.Store.Pick(Pickers.PickValidMonitorForWorkspace(nonExistentWorkspaceId)); + var result = ctx.Store.Pick(Pickers.PickValidMonitorByWorkspace(nonExistentWorkspaceId)); // Then we get an error Assert.False(result.IsSuccessful); Assert.Contains("not found", result.Error!.Message); } } + +public class PickStickyWorkspacesByMonitorTests +{ + [Theory, AutoSubstituteData<StoreCustomization>] + internal void MonitorNotFound(IContext ctx) + { + // Given we have a monitor that doesn't exist + HMONITOR nonExistentHandle = (HMONITOR)999; + + // When we try to get workspaces for the monitor + var result = ctx.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(nonExistentHandle)); + + // Then we get an error + Assert.False(result.IsSuccessful); + Assert.Contains("not found", result.Error!.Message); + } + + [Theory, AutoSubstituteData<StoreCustomization>] + internal void GetStickyWorkspaces(IContext ctx, MutableRootSector root) + { + // Given we have three workspaces, two sticky to monitor 1 + var workspace1 = CreateWorkspace(ctx); + var workspace2 = CreateWorkspace(ctx); + var workspace3 = CreateWorkspace(ctx); + AddWorkspacesToManager(ctx, root, workspace1, workspace2, workspace3); + + IMonitor monitor = CreateMonitor((HMONITOR)1); + AddMonitorsToManager(ctx, root, monitor); + + root.MapSector.StickyWorkspaceMonitorIndexMap = root + .MapSector.StickyWorkspaceMonitorIndexMap.SetItem(workspace1.Id, [0, 1]) + .SetItem(workspace2.Id, [0, 4]); + + // When we get the workspaces for the monitor + var result = ctx.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(monitor.Handle)); + + // Then we get the sticky workspaces in workspace order + Assert.True(result.IsSuccessful); + Assert.Equal(3, result.Value.Count); // All workspaces (2 sticky + 1 non-sticky) + Assert.Equal(workspace1, result.Value[0]); // First in workspace order + Assert.Equal(workspace2, result.Value[1]); // Second in workspace order + Assert.Equal(workspace3, result.Value[2]); // Non-sticky workspace + } + + [Theory, AutoSubstituteData<StoreCustomization>] + internal void GetAllWorkspacesWhenNoneSticky(IContext ctx, MutableRootSector root) + { + // Given we have three workspaces but none are sticky + var workspace1 = CreateWorkspace(ctx); + var workspace2 = CreateWorkspace(ctx); + var workspace3 = CreateWorkspace(ctx); + AddWorkspacesToManager(ctx, root, workspace1, workspace2, workspace3); + + IMonitor monitor = CreateMonitor((HMONITOR)1); + AddMonitorsToManager(ctx, root, monitor); + + // When we get the workspaces for the monitor + var result = ctx.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(monitor.Handle)); + + // Then we get all workspaces in workspace order + Assert.True(result.IsSuccessful); + Assert.Equal(3, result.Value.Count); + Assert.Equal(workspace1, result.Value[0]); + Assert.Equal(workspace2, result.Value[1]); + Assert.Equal(workspace3, result.Value[2]); + } + + [Theory, AutoSubstituteData<StoreCustomization>] + internal void InvalidMonitorIndicesAreIgnored(IContext ctx, MutableRootSector root) + { + // Given we have a workspace with both valid and invalid monitor indices + var workspace = CreateWorkspace(ctx); + AddWorkspacesToManager(ctx, root, workspace); + + IMonitor monitor1 = CreateMonitor((HMONITOR)1); + IMonitor monitor2 = CreateMonitor((HMONITOR)2); + AddMonitorsToManager(ctx, root, monitor1, monitor2); // Only indices 0 and 1 are valid + + root.MapSector.StickyWorkspaceMonitorIndexMap = root.MapSector.StickyWorkspaceMonitorIndexMap.SetItem( + workspace.Id, + [-1, 0, 1, 2] // Invalid indices: -1 and 2 + ); + + // When we get the workspaces for monitor1 (index 0) + var result = ctx.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(monitor1.Handle)); + + // Then the workspace is included because index 0 is valid, ignoring invalid indices + Assert.True(result.IsSuccessful); + Assert.Single(result.Value); + Assert.Equal(workspace, result.Value[0]); + + // And when we get workspaces for monitor2 (index 1) + result = ctx.Store.Pick(Pickers.PickStickyWorkspacesByMonitor(monitor2.Handle)); + + // Then the workspace is included because index 1 is valid, ignoring invalid indices + Assert.True(result.IsSuccessful); + Assert.Single(result.Value); + Assert.Equal(workspace, result.Value[0]); + } +} diff --git a/src/Whim/Store/MapSector/MapPickers.cs b/src/Whim/Store/MapSector/MapPickers.cs index f82fe7c61..bf9392cf4 100644 --- a/src/Whim/Store/MapSector/MapPickers.cs +++ b/src/Whim/Store/MapSector/MapPickers.cs @@ -300,10 +300,17 @@ out ImmutableArray<int> monitorIndices /// </item> /// </list> /// </summary> - /// <param name="workspaceId"></param> - /// <param name="monitorHandle"></param> - /// <returns></returns> - public static PurePicker<Result<HMONITOR>> PickValidMonitorForWorkspace( + /// <param name="workspaceId"> + /// The ID of the workspace to get the monitor for. + /// </param> + /// <param name="monitorHandle"> + /// The preferred monitor to use. If not provided, the last monitor the workspace was activated on will next be tried. + /// </param> + /// <returns> + /// The first valid monitor for the workspace, when passed to <see cref="IStore.Pick{TResult}(PurePicker{TResult})"/>. + /// If the workspace can't be found, then an error is returned. + /// </returns> + public static PurePicker<Result<HMONITOR>> PickValidMonitorByWorkspace( WorkspaceId workspaceId, HMONITOR monitorHandle = default ) => @@ -359,4 +366,80 @@ public static PurePicker<Result<HMONITOR>> PickValidMonitorForWorkspace( return Result.FromException<HMONITOR>(StoreExceptions.NoValidMonitorForWorkspace(workspaceId)); }; + + /// <summary> + /// Retrieves the workspaces which can be shown on the given monitor. + /// </summary> + /// <param name="monitorHandle"> + /// The handle of the monitor to get the workspaces for. + /// </param> + /// <returns> + /// The workspaces which can be shown on the monitor, when passed to <see cref="IStore.Pick{TResult}(PurePicker{TResult})"/>. + /// If the monitor can't be found, then an error is returned. + /// </returns> + public static PurePicker<Result<IReadOnlyList<IWorkspace>>> PickStickyWorkspacesByMonitor(HMONITOR monitorHandle) => + rootSector => + { + IMapSector mapSector = rootSector.MapSector; + IWorkspaceSector workspaceSector = rootSector.WorkspaceSector; + IMonitorSector monitorSector = rootSector.MonitorSector; + + // Verify the monitor exists. + Result<IMonitor> monitorResult = PickMonitorByHandle(monitorHandle)(rootSector); + if (!monitorResult.IsSuccessful) + { + return Result.FromException<IReadOnlyList<IWorkspace>>(monitorResult.Error!); + } + + // Get the index of the monitor. + IMonitor monitor = monitorResult.Value; + ImmutableArray<IMonitor> monitors = monitorSector.Monitors; + int monitorIndex = monitors.IndexOf(monitor); + + // Get the workspaces which can be shown on the monitor. + List<WorkspaceId> processedWorkspaces = []; + List<WorkspaceId> unsortedWorkspaces = []; + + foreach ( + ( + WorkspaceId workspaceId, + ImmutableArray<int> monitorIndices + ) in mapSector.StickyWorkspaceMonitorIndexMap + ) + { + // If the workspace is sticky to the monitor, or it's orphaned. + if ( + monitorIndices.Contains(monitorIndex) + || monitorIndices.All(index => index < 0 || index >= monitors.Length) + ) + { + unsortedWorkspaces.Add(workspaceId); + } + + processedWorkspaces.Add(workspaceId); + } + + // Get the workspaces which can be shown on any monitor. + foreach (WorkspaceId workspaceId in workspaceSector.Workspaces.Keys) + { + if (processedWorkspaces.Contains(workspaceId)) + { + continue; + } + + unsortedWorkspaces.Add(workspaceId); + } + + // Crude sorting. + List<IWorkspace> sortedWorkspaces = []; + foreach (WorkspaceId workspaceId in workspaceSector.WorkspaceOrder) + { + if (unsortedWorkspaces.Contains(workspaceId)) + { + sortedWorkspaces.Add(workspaceSector.Workspaces[workspaceId]); + } + } + + return sortedWorkspaces; + }; } diff --git a/src/Whim/Store/MapSector/Transforms/ActivateWorkspaceTransform.cs b/src/Whim/Store/MapSector/Transforms/ActivateWorkspaceTransform.cs index 00b98978a..747f16445 100644 --- a/src/Whim/Store/MapSector/Transforms/ActivateWorkspaceTransform.cs +++ b/src/Whim/Store/MapSector/Transforms/ActivateWorkspaceTransform.cs @@ -31,7 +31,7 @@ internal override Result<Unit> Execute(IContext ctx, IInternalContext internalCt } Result<HMONITOR> targetMonitorHandleResult = ctx.Store.Pick( - PickValidMonitorForWorkspace(workspace.Id, MonitorHandle) + PickValidMonitorByWorkspace(workspace.Id, MonitorHandle) ); if (!targetMonitorHandleResult.TryGet(out HMONITOR targetMonitorHandle)) {