Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open workspaces on a specific monitor without activation #979

Merged
merged 4 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/docs/customize/snippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ context.CommandManager.Add(
);
```

## Skip over active workspaces
## Activate adjacent workspace, skipping over active workspaces

The following commands can be useful on multi-monitor setups. When bound to keybinds, these can be used to cycle through the list of workspaces, skipping over those that are active on other monitors to avoid accidental swapping.

Expand Down Expand Up @@ -80,3 +80,29 @@ context.CommandManager.Add("move_window_to_browser_workspace", "Move window to b
}
});
```

## Activate a workspace on a specific monitor without changing focus

The following command can be used to activate a workspace on a specific monitor without focusing the workspace you are activating. In this example, I am activating a specific workspace on the 3rd monitor.

```csharp
Guid? browserWorkspaceId = context.WorkspaceManager.Add("Browser workspace");

context.CommandManager.Add(
identifier: "activate_browser_workspace_on_monitor_3_no_focus",
title: "Activate browser workspace on monitor 3 no focus",
callback: () =>
{
if (browserWorkspaceId is Guid workspaceId)
{
IMonitor? monitor = context.Store.Pick(Pickers.PickMonitorByIndex(2)).ValueOrDefault;
if (monitor is null)
{
return;
}

context.Store.Dispatch(new ActivateWorkspaceTransform(workspaceId, monitor.Handle, FocusWorkspaceWindow: false));
}
}
);
```
1 change: 1 addition & 0 deletions src/Whim.TestUtils/StoreCustomization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public void Customize(IFixture fixture)

fixture.Inject(_store._root);
fixture.Inject(_store._root.MutableRootSector);
fixture.Inject(_store.Transforms);

_store._root.MutableRootSector.MonitorSector.MonitorsChangedDelay = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ internal void MonitorNotFound(IContext ctx, MutableRootSector rootSector)
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void WorkspaceAlreadyActivatedOnMonitor(IContext ctx, MutableRootSector rootSector)
internal void WorkspaceAlreadyActivatedOnMonitor(
IContext ctx,
MutableRootSector rootSector,
List<object> executedTransforms
)
{
// Given the workspace is already activated on the monitor
Workspace workspace = CreateWorkspace(ctx);
Expand All @@ -81,10 +85,12 @@ internal void WorkspaceAlreadyActivatedOnMonitor(IContext ctx, MutableRootSector

// Then nothing happens
Assert.True(result.IsSuccessful);
Assert.DoesNotContain(executedTransforms, t => t.Equals(new DoWorkspaceLayoutTransform(workspace.Id)));
Assert.DoesNotContain(executedTransforms, t => t is FocusWindowTransform);
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void LayoutOldWorkspace(IContext ctx, MutableRootSector rootSector)
internal void LayoutOldWorkspace(IContext ctx, MutableRootSector rootSector, List<object> executedTransforms)
{
// Given the target monitor has an old workspace
Workspace workspace1 = CreateWorkspace(ctx);
Expand All @@ -104,25 +110,31 @@ internal void LayoutOldWorkspace(IContext ctx, MutableRootSector rootSector)
// When we activate the workspace on the target monitor
var (result, evs) = AssertRaises(ctx, rootSector, sut);

// Then the old workspace is deactivated
// Then the old workspace is deactivated on monitor 1 and the new workspace is activated
Assert.True(result.IsSuccessful);

Assert.Equal(2, evs.Count);

// The event for the first monitor.
Assert.Same(workspace3, evs[0].PreviousWorkspace);
Assert.Same(workspace1, evs[0].CurrentWorkspace);
Assert.Same(monitor3, evs[0].Monitor);

// The event for the second monitor.
Assert.Same(workspace1, evs[1].PreviousWorkspace);
Assert.Same(workspace3, evs[1].CurrentWorkspace);
Assert.Same(monitor1, evs[1].Monitor);

Assert.Equal(workspace3.Id, rootSector.MapSector.MonitorWorkspaceMap[monitor1.Handle]);
Assert.Equal(workspace1.Id, rootSector.MapSector.MonitorWorkspaceMap[monitor3.Handle]);

Assert.Contains(executedTransforms, t => t.Equals(new DoWorkspaceLayoutTransform(workspace1.Id)));
Assert.Contains(executedTransforms, t => t.Equals(new DoWorkspaceLayoutTransform(workspace3.Id)));
Assert.Contains(executedTransforms, t => t.Equals(new FocusWindowTransform(workspace3.Id)));
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void DeactivateOldWorkspace(IContext ctx, MutableRootSector rootSector)
internal void DeactivateOldWorkspace(IContext ctx, MutableRootSector rootSector, List<object> executedTransforms)
{
// Given the target monitor has an old workspace, and the new workspace wasn't previously activated
Workspace workspace1 = CreateWorkspace(ctx);
Expand All @@ -149,5 +161,66 @@ internal void DeactivateOldWorkspace(IContext ctx, MutableRootSector rootSector)
Assert.Same(workspace1, evs[0].PreviousWorkspace);
Assert.Same(workspace3, evs[0].CurrentWorkspace);
Assert.Same(monitor1, evs[0].Monitor);

Assert.DoesNotContain(executedTransforms, t => t.Equals(new DoWorkspaceLayoutTransform(workspace1.Id)));
Assert.Contains(executedTransforms, t => t.Equals(new DoWorkspaceLayoutTransform(workspace3.Id)));
Assert.Contains(executedTransforms, t => t.Equals(new FocusWindowTransform(workspace3.Id)));
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void FocusWorkspaceWindow_ActiveIsOldWorkspace(
IContext ctx,
MutableRootSector rootSector,
List<object> executedTransforms
)
{
// Given the FocusWorkspaceWindow flag is false and the active workspace is the old workspace...
Workspace workspace1 = CreateWorkspace(ctx);
Workspace workspace2 = CreateWorkspace(ctx);

IMonitor monitor1 = CreateMonitor((HMONITOR)1);

PopulateMonitorWorkspaceMap(ctx, rootSector, monitor1, workspace1);
AddWorkspacesToManager(ctx, rootSector, workspace2);

ActivateWorkspaceTransform sut = new(workspace2.Id, monitor1.Handle, FocusWorkspaceWindow: false);

// When we activate the workspace
var (result, evs) = AssertRaises(ctx, rootSector, sut);

// Then the window on the new workspace is focused
Assert.True(result.IsSuccessful);
Assert.Contains(executedTransforms, t => t.Equals(new FocusWindowTransform(workspace2.Id)));
}

[Theory, AutoSubstituteData<StoreCustomization>]
internal void FocusWorkspaceWindow_ActiveIsStillVisible(
IContext ctx,
MutableRootSector rootSector,
List<object> executedTransforms
)
{
// Given the FocusWorkspaceWindow flag is false and the active workspace is still visible...
Workspace workspace1 = CreateWorkspace(ctx);
Workspace workspace2 = CreateWorkspace(ctx);
Workspace workspace3 = CreateWorkspace(ctx);

IMonitor monitor1 = CreateMonitor((HMONITOR)1);
IMonitor monitor2 = CreateMonitor((HMONITOR)2);

PopulateMonitorWorkspaceMap(ctx, rootSector, monitor1, workspace1);
PopulateMonitorWorkspaceMap(ctx, rootSector, monitor2, workspace2);
AddWorkspacesToManager(ctx, rootSector, workspace3);

ActivateWorkspaceTransform sut = new(workspace3.Id, monitor2.Handle, FocusWorkspaceWindow: false);

// When we activate the workspace
var (result, evs) = AssertRaises(ctx, rootSector, sut);

// Then the window on the previously active workspace is focused
Assert.True(result.IsSuccessful);

Assert.Contains(executedTransforms, t => t.Equals(new FocusWindowTransform(workspace1.Id)));
Assert.DoesNotContain(executedTransforms, t => t.Equals(new FocusWindowTransform(workspace3.Id)));
}
}
23 changes: 20 additions & 3 deletions src/Whim/Store/MapSector/Transforms/ActivateWorkspaceTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ namespace Whim;
/// The handle of the monitor to activate the workspace in. If <see langword="null"/>, this will
/// default to the active monitor.
/// </param>
public record ActivateWorkspaceTransform(WorkspaceId WorkspaceId, HMONITOR MonitorHandle = default) : Transform
/// <param name="FocusWorkspaceWindow">
/// If <see langword="true"/>, the last focused window of the <see cref="WorkspaceId"/> will be focused.
/// If <see langword="false"/>, the last focused window of the active workspace will be focused.
/// </param>
public record ActivateWorkspaceTransform(
WorkspaceId WorkspaceId,
HMONITOR MonitorHandle = default,
bool FocusWorkspaceWindow = true
) : Transform
{
internal override Result<Unit> Execute(IContext ctx, IInternalContext internalCtx, MutableRootSector rootSector)
{
Expand Down Expand Up @@ -75,8 +83,17 @@ internal override Result<Unit> Execute(IContext ctx, IInternalContext internalCt
}

// Layout the new workspace.
workspace.DoLayout();
workspace.FocusLastFocusedWindow();
ctx.Store.Dispatch(new DoWorkspaceLayoutTransform(workspace.Id));

if (FocusWorkspaceWindow)
{
ctx.Store.Dispatch(new FocusWindowTransform(workspace.Id));
}
else
{
WorkspaceId activeWorkspaceId = ctx.Store.Pick(PickActiveWorkspaceId());
ctx.Store.Dispatch(new FocusWindowTransform(activeWorkspaceId));
}

mapSector.QueueEvent(
new MonitorWorkspaceChangedEventArgs()
Expand Down