Skip to content

Commit

Permalink
Update ActiveLayout bar widget to a drop-down (#1036)
Browse files Browse the repository at this point in the history
The `ActiveLayoutWidget` has changed from a button which toggles through options to a dropdown. This required the creation of a custom `DropDown` control, in order to control the width of the drop-down button.

This PR also takes the opportunity to rip out some usages of the old Manager API.

---------

Co-authored-by: Andrei Zhvaleuski <[email protected]>
Co-authored-by: Isaac Daly <[email protected]>
  • Loading branch information
3 people authored Nov 5, 2024
1 parent 8206b19 commit 09f8cd3
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 169 deletions.
315 changes: 263 additions & 52 deletions src/Whim.Bar.Tests/ActiveLayout/ActiveLayoutWidgetViewModelTests.cs
Original file line number Diff line number Diff line change
@@ -1,77 +1,288 @@
using NSubstitute;
using Whim.TestUtils;
using Windows.Win32.Graphics.Gdi;
using Xunit;

namespace Whim.Bar.Tests;

[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
public class ActiveLayoutWidgetViewModelTests
{
private static ActiveLayoutWidgetViewModel CreateSut(IContext context, IMonitor monitor) => new(context, monitor);
private static ActiveLayoutWidgetViewModel CreateSut(IContext ctx, IMonitor monitor) => new(ctx, monitor);

[Theory, AutoSubstituteData]
public void WorkspaceManager_ActiveLayoutEngineChanged(IContext context, IMonitor monitor, IWorkspace workspace)
private static ILayoutEngine CreateLayoutEngine(string name)
{
// Given
ActiveLayoutWidgetViewModel viewModel = CreateSut(context, monitor);

// When
// Then
Assert.PropertyChanged(
viewModel,
nameof(viewModel.ActiveLayoutEngine),
() =>
context.WorkspaceManager.ActiveLayoutEngineChanged += Raise.Event<
EventHandler<ActiveLayoutEngineChangedEventArgs>
>(
context.WorkspaceManager,
new ActiveLayoutEngineChangedEventArgs()
{
Workspace = workspace,
PreviousLayoutEngine = workspace.ActiveLayoutEngine,
CurrentLayoutEngine = workspace.ActiveLayoutEngine,
}
)
);
ILayoutEngine layoutEngine = Substitute.For<ILayoutEngine>();
layoutEngine.Name.Returns(name);
return layoutEngine;
}

[Theory, AutoSubstituteData]
public void WorkspaceManager_MonitorWorkspaceChanged(IContext context, IMonitor monitor, IWorkspace workspace)
internal void Constructor_SetsEventListeners(IContext ctx)
{
// Given
ActiveLayoutWidgetViewModel viewModel = CreateSut(context, monitor);

// When
// Then
Assert.PropertyChanged(
viewModel,
nameof(viewModel.ActiveLayoutEngine),
() =>
context.Butler.MonitorWorkspaceChanged += Raise.Event<EventHandler<MonitorWorkspaceChangedEventArgs>>(
context.Butler,
new MonitorWorkspaceChangedEventArgs() { Monitor = monitor, CurrentWorkspace = workspace }
)
);
// GIVEN a monitor and view model
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);

// THEN all required event listeners are registered with the context store
Assert.NotNull(sut);
ctx.Store.WorkspaceEvents.Received().ActiveLayoutEngineChanged += Arg.Any<
EventHandler<ActiveLayoutEngineChangedEventArgs>
>();
ctx.Store.MapEvents.Received().MonitorWorkspaceChanged += Arg.Any<
EventHandler<MonitorWorkspaceChangedEventArgs>
>();
ctx.Store.WindowEvents.Received().WindowFocused += Arg.Any<EventHandler<WindowFocusedEventArgs>>();
}

[Theory, AutoSubstituteData]
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Usage",
"NS5000:Received check.",
Justification = "The analyzer is wrong"
)]
public void Dispose(IContext context, IMonitor monitor)
internal void Dispose_RemovesEventListeners(IContext ctx)
{
// Given
ActiveLayoutWidgetViewModel viewModel = CreateSut(context, monitor);
// GIVEN a monitor and initialized view model
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);

// When
viewModel.Dispose();
// WHEN the view model is disposed
sut.Dispose();

// Then
context.WorkspaceManager.Received(1).ActiveLayoutEngineChanged -= Arg.Any<
// THEN all event listeners are properly unregistered from the context store
ctx.Store.WorkspaceEvents.Received().ActiveLayoutEngineChanged -= Arg.Any<
EventHandler<ActiveLayoutEngineChangedEventArgs>
>();
context.Butler.Received(1).MonitorWorkspaceChanged -= Arg.Any<EventHandler<MonitorWorkspaceChangedEventArgs>>();
ctx.Store.MapEvents.Received().MonitorWorkspaceChanged -= Arg.Any<
EventHandler<MonitorWorkspaceChangedEventArgs>
>();
ctx.Store.WindowEvents.Received().WindowFocused -= Arg.Any<EventHandler<WindowFocusedEventArgs>>();
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void Store_ActiveLayoutEngineChanged_UpdatesProperty(
IContext ctx,
MutableRootSector root,
ILayoutEngine previousLayoutEngine,
ILayoutEngine currentLayoutEngine
)
{
// GIVEN an initialized view model monitoring a specific workspace
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx);
StoreTestUtils.AddWorkspacesToManager(ctx, root, workspace);

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
bool propertyChanged = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ActiveLayoutWidgetViewModel.ActiveLayoutEngine))
{
propertyChanged = true;
}
};

// WHEN the active layout engine changes in the workspace
root.WorkspaceSector.QueueEvent(
new ActiveLayoutEngineChangedEventArgs()
{
Workspace = workspace,
PreviousLayoutEngine = previousLayoutEngine,
CurrentLayoutEngine = currentLayoutEngine,
}
);
root.WorkspaceSector.DispatchEvents();

// THEN the view model property is updated and notifies of the change
Assert.True(propertyChanged);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void Store_MonitorWorkspaceChanged_SameMonitor_UpdatesProperties(IContext ctx, MutableRootSector root)
{
// GIVEN
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ILayoutEngine layoutEngine1 = CreateLayoutEngine("Layout1");
ILayoutEngine layoutEngine2 = CreateLayoutEngine("Layout2");
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx) with
{
LayoutEngines = [layoutEngine1, layoutEngine2],
};
StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace);

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
List<string> propertyChangedNames = [];
sut.PropertyChanged += (s, e) => propertyChangedNames.Add(e.PropertyName ?? string.Empty);

// WHEN
root.MapSector.QueueEvent(
new MonitorWorkspaceChangedEventArgs
{
Monitor = monitor,
CurrentWorkspace = workspace,
PreviousWorkspace = null,
}
);
root.MapSector.DispatchEvents();

// THEN
Assert.Contains(nameof(ActiveLayoutWidgetViewModel.LayoutEngines), propertyChangedNames);
Assert.Contains(nameof(ActiveLayoutWidgetViewModel.ActiveLayoutEngine), propertyChangedNames);
Assert.Collection(
sut.LayoutEngines,
item => Assert.Equal("Layout1", item),
item => Assert.Equal("Layout2", item)
);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void Store_MonitorWorkspaceChanged_DifferentMonitor_DoesNotUpdateProperties(
IContext ctx,
MutableRootSector root
)
{
// GIVEN
IMonitor widgetMonitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
IMonitor differentMonitor = StoreTestUtils.CreateMonitor((HMONITOR)2);
ILayoutEngine layoutEngine1 = CreateLayoutEngine("Layout1");
ILayoutEngine layoutEngine2 = CreateLayoutEngine("Layout2");
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx) with
{
LayoutEngines = [layoutEngine1, layoutEngine2],
};
StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, differentMonitor, workspace);

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, widgetMonitor);
List<string> propertyChangedNames = [];
sut.PropertyChanged += (s, e) => propertyChangedNames.Add(e.PropertyName ?? string.Empty);

// WHEN
root.MapSector.QueueEvent(
new MonitorWorkspaceChangedEventArgs
{
Monitor = differentMonitor,
CurrentWorkspace = workspace,
PreviousWorkspace = null,
}
);
root.MapSector.DispatchEvents();

// THEN
Assert.DoesNotContain(nameof(ActiveLayoutWidgetViewModel.LayoutEngines), propertyChangedNames);
Assert.DoesNotContain(nameof(ActiveLayoutWidgetViewModel.ActiveLayoutEngine), propertyChangedNames);
Assert.Empty(sut.LayoutEngines);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void WindowEvents_WindowFocused_ClosesDropDown(IContext ctx, MutableRootSector root, IWindow window)
{
// GIVEN a view model with an open dropdown
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
sut.IsDropDownOpen = true;

// WHEN a window focus event occurs
root.WindowSector.QueueEvent(new WindowFocusedEventArgs { Window = window });
root.WindowSector.DispatchEvents();

// THEN the dropdown is automatically closed
Assert.False(sut.IsDropDownOpen);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void ActiveLayoutEngine_Get_ReturnsEmptyString_WhenNoLayoutEngine(IContext ctx)
{
// GIVEN a view model with no associated workspace or layout engine
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
// No layout engine or workspace is populated in the store

// WHEN requesting the active layout engine name
string result = sut.ActiveLayoutEngine;

// THEN an empty string is returned
Assert.Equal("", result);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void ActiveLayoutEngine_Get_ReturnsLayoutEngineName(IContext ctx, MutableRootSector root)
{
// GIVEN a view model with a workspace containing a specific layout engine
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ILayoutEngine layoutEngine = CreateLayoutEngine("TestLayout");
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx) with { LayoutEngines = [layoutEngine] };
StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace);

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);

// WHEN requesting the active layout engine name
string result = sut.ActiveLayoutEngine;

// THEN the correct layout engine name is returned
Assert.Equal("TestLayout", result);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void ActiveLayoutEngine_Set_DoesNothing_WhenNoWorkspace(IContext ctx)
{
// GIVEN a view model with no associated workspace
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
// No workspace is populated in the store
bool propertyChanged = false;
sut.PropertyChanged += (s, e) => propertyChanged = true;

// WHEN attempting to set a new layout engine
sut.ActiveLayoutEngine = "NewLayout";

// THEN no property change occurs since there is no workspace to modify
Assert.False(propertyChanged);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void ActiveLayoutEngine_Set_DoesNotDispatch_WhenSameLayoutEngine(IContext ctx, List<object> transforms)
{
// GIVEN a view model with a workspace and active layout engine
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ILayoutEngine layoutEngine = CreateLayoutEngine("TestLayout");
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx) with { LayoutEngines = [layoutEngine] };

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
bool propertyChanged = false;
sut.PropertyChanged += (s, e) => propertyChanged = true;

// WHEN setting the same layout engine name
sut.ActiveLayoutEngine = "TestLayout";

// THEN no changes are dispatched and no property change is triggered
Assert.Empty(transforms);
Assert.False(propertyChanged);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void ActiveLayoutEngine_Set_Dispatches_WhenDifferentLayoutEngine(
IContext ctx,
MutableRootSector root,
List<object> transforms
)
{
// GIVEN a view model with a workspace and specific layout engine
IMonitor monitor = StoreTestUtils.CreateMonitor((HMONITOR)1);
ILayoutEngine layoutEngine = CreateLayoutEngine("TestLayout");
Workspace workspace = StoreTestUtils.CreateWorkspace(ctx) with { LayoutEngines = [layoutEngine] };
StoreTestUtils.PopulateMonitorWorkspaceMap(ctx, root, monitor, workspace);

ActiveLayoutWidgetViewModel sut = CreateSut(ctx, monitor);
bool propertyChanged = false;
sut.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(ActiveLayoutWidgetViewModel.ActiveLayoutEngine))
{
propertyChanged = true;
}
};

// WHEN setting a different layout engine name
sut.ActiveLayoutEngine = "NewLayout";

// THEN a transform is dispatched and the property change is notified
Assert.Contains(transforms, t => t.Equals(new SetLayoutEngineFromNameTransform(workspace.Id, "NewLayout")));
Assert.True(propertyChanged);
}
}
55 changes: 0 additions & 55 deletions src/Whim.Bar.Tests/ActiveLayout/NextLayoutEngineCommandTests.cs

This file was deleted.

Loading

0 comments on commit 09f8cd3

Please sign in to comment.