Skip to content

Commit

Permalink
Add sticky monitor workspaces to the core (#1071)
Browse files Browse the repository at this point in the history
This PR includes changes to add sticky monitor workspaces to Whim's core.

## Sticky Monitor Workspaces

The `IMapSector` now has two new dictionaries:

- `StickyWorkspaceMonitorIndexMap` - a dictionary that maps workspaces to the monitor indices they are sticky to.
- `WorkspaceLastMonitorMap` - a dictionary that maps a workspace to the last monitor it was on.

These are used to store and determine which monitors workspace can be displayed on.

Saved workspaces will regain their sticky monitor indices during startup, via the `CoreSavedStateManager`.

## Code Clean-up and Improvements

- `IWindowManager.CreateWindow` has been replaced by `IContext.CreateWindow`.
- Replaced some references to the legacy API.
- Reduced the build time for local development by reducing code generation occurrences.
  • Loading branch information
dalyIsaac authored Nov 10, 2024
1 parent 2fe3ff2 commit 1fe72a1
Show file tree
Hide file tree
Showing 25 changed files with 1,050 additions and 323 deletions.
55 changes: 41 additions & 14 deletions docs/script/core/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,33 @@

[!INCLUDE[Workspace overview](../../_includes/core/workspace-overview.md)]

## Configuration
## Creating Workspaces

Workspaces can be created using the <xref:Whim.AddWorkspaceTransform> command. The command returns the ID of the newly created workspace.

```csharp
Guid stickyWorkspaceId = context.Store.Dispatch(
new AddWorkspaceTransform(
Name: "Sticky",
)
);
```

The following can be specified when creating a workspace:

- `Name`: The name of the workspace. This defaults to "Workspace n", where `n` is the number of workspaces created so far.
- `CreateLeafLayoutEngines`: A list of layout engines to use for the workspace. This defaults to the layout engines specified in the <xref:Whim.SetCreateLayoutEnginesTransform> command - see [Default Layout Engines](#default-layout-engines) for more.
- `MonitorIndices`: A list of monitor indices to use for the workspace - see [Sticky Workspaces](#sticky-workspaces) for more.

## Default Layout Engines

The <xref:Whim.SetCreateLayoutEnginesTransform> lets you specify the default layout engines for workspaces. For example, the following config sets up three workspaces, and two layout engines:

```csharp
// Set up workspaces.
Guid browserWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform("Browser")).Value;
Guid ideWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform("IDE")).Value;
Guid altWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform("Alt")).Value;
Guid browserWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform(Name: "Browser")).Value;
Guid ideWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform(Name: "IDE")).Value;
Guid altWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform(Name: "Alt")).Value;

// Set up layout engines.
context.Store.Dispatch(
Expand All @@ -29,8 +47,8 @@ It's also possible to customize the layout engines for a specific workspace:
```csharp
context.Store.Dispatch(
new AddWorkspaceTransform(
"Alt",
new CreateLeafLayoutEngine[]
Name: "Alt",
CreateLeafLayoutEngines: new CreateLeafLayoutEngine[]
{
(id) => new FocusLayoutEngine(id)
}
Expand All @@ -40,11 +58,26 @@ context.Store.Dispatch(

The available layout engines are listed in [Layout Engines](./layout-engines.md).

## Sticky Workspaces

Sticky workspaces can only be displayed on specific monitors. To create a sticky workspace, specify the monitor indices when creating the workspace:

```csharp
Guid stickyWorkspaceId = context.Store.Dispatch(
new AddWorkspaceTransform(
Name: "Sticky",
MonitorIndices: new int[] { 0, 1 }
)
);
```

The workspace will only be displayed on the first and second monitors (0-based index). For more on monitors, see the [Monitors](./monitors.md) page.

## Example Command

```csharp
Guid browserWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform("Browser")).Value;
Guid ideWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform("IDE")).Value;
Guid browserWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform(Name: "Browser")).Value;
Guid ideWorkspaceId = context.Store.Dispatch(new AddWorkspaceTransform(Name: "IDE")).Value;

context.CommandManager.Add(
"merge_workspace_with_browser",
Expand All @@ -57,9 +90,3 @@ context.CommandManager.Add(
```

For more, see the [Store](./store.md) and [Commands](../../configure/core/commands.md) pages.

## Sticky Workspaces

Sticky workspaces are being worked on in [this GitHub issue](https://github.com/dalyIsaac/Whim/issues/660).

👷🏗️🚧
27 changes: 6 additions & 21 deletions scripts/Generate-SourceFromSchema.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,10 @@ Write-Host "Generating source from schema..."
$schemaPath = ".\src\Whim.Yaml\schema.json"
$outputPath = ".\src\Whim.Yaml\Generated"
$metadataPath = "$outputPath\metadata.json"
$now = Get-Date
if ($env:CI) {
$gitSha = $env:GITHUB_SHA
}
else {
$gitSha = (git rev-parse HEAD)
}

function Test-Regenerate {
param (
[string]$schemaPath = ".\src\Whim.Yaml\schema.json",
[string]$outputPath = ".\src\Whim.Yaml\Generated"
)
$schemaHash = (Get-FileHash $schemaPath -Algorithm SHA256).Hash

function Test-Regenerate {
if (!(Test-Path $outputPath)) {
Write-Host "Output directory does not exist, generating..."
New-Item $outputPath -ItemType Directory
Expand All @@ -31,18 +21,13 @@ function Test-Regenerate {
}

$metadata = Get-Content $metadataPath | ConvertFrom-Json
if ($metadata.gitRef -ne $gitSha) {
Write-Host "Git ref has changed since last generation, regenerating..."
return $true
}

$schemaLastWriteTime = (Get-Item $schemaPath).LastWriteTime
if ($metadata.lastWriteTime -lt $schemaLastWriteTime) {
if ($metadata.schemaHash -ne $schemaHash) {
Write-Host "Schema has changed since last generation, regenerating..."
Write-Host "Old hash: $($metadata.schemaHash)"
Write-Host "New hash: $schemaHash"
return $true
}

Write-Host "Schema has not changed since last generation, skipping..."
return $false
}

Expand All @@ -60,5 +45,5 @@ dotnet tool run generatejsonschematypes `
# If not in CI, write metadata file
if ($LASTEXITCODE -eq 0 -and $null -eq $env:CI) {
Write-Host "Writing metadata file..."
@{ gitRef = $gitSha; lastWriteTime = $now } | ConvertTo-Json | Set-Content $metadataPath
@{ schemaHash = $schemaHash } | ConvertTo-Json | Set-Content $metadataPath
}
2 changes: 1 addition & 1 deletion src/Whim.Bar/BarWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public BarWindow(IContext context, BarConfig barConfig, IMonitor monitor)
UIElementExtensions.InitializeComponent(this, "Whim.Bar", "BarWindow");

IWindow window = _context
.WindowManager.CreateWindow(this.GetHandle())
.CreateWindow(this.GetHandle())
.OrInvoke(() => throw new BarException("Window was unexpectedly null"));

WindowState = new WindowState()
Expand Down
44 changes: 25 additions & 19 deletions src/Whim.Tests/Context/CoreSavedStateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ private static CoreSavedState CreateSavedState()
{
new(
"workspace1",
new() { new SavedWindow(1, new(0, 0, 100, 100)), new SavedWindow(2, new(100, 100, 100, 100)) }
new() { new SavedWindow(1, new(0, 0, 100, 100)), new SavedWindow(2, new(100, 100, 100, 100)) },
null
),
new(
"workspace2",
new() { new SavedWindow(3, new(200, 200, 100, 100)), new SavedWindow(4, new(300, 300, 100, 100)) }
new() { new SavedWindow(3, new(200, 200, 100, 100)), new SavedWindow(4, new(300, 300, 100, 100)) },
null
),
}
);
Expand Down Expand Up @@ -92,9 +94,12 @@ public void PostInitialize(IContext ctx)
Assert.Null(sut.SavedState);
}

private static string Setup_ContextState(IContext ctx)
private static string Setup_ContextState(IContext ctx, MutableRootSector root)
{
ctx.MonitorManager.PrimaryMonitor.WorkingArea.Returns(new Rectangle<int>(0, 0, 1000, 1000));
IMonitor monitor = CreateMonitor((HMONITOR)123);
monitor.WorkingArea.Returns(new Rectangle<int>(0, 0, 1000, 1000));
AddMonitorsToManager(ctx, root, monitor);
root.MonitorSector.PrimaryMonitorHandle = monitor.Handle;

// Create four windows.
IWindow[] windows = new IWindow[4];
Expand All @@ -105,10 +110,10 @@ private static string Setup_ContextState(IContext ctx)
}

// Create two workspaces with two windows each
IWorkspace workspace1 = Substitute.For<IWorkspace>();
workspace1.Name.Returns("workspace1");
workspace1
.ActiveLayoutEngine.DoLayout(Arg.Any<IRectangle<int>>(), Arg.Any<IMonitor>())
ILayoutEngine engine1 = Substitute.For<ILayoutEngine>();
Workspace workspace1 = CreateWorkspace(ctx) with { BackingName = "workspace1", LayoutEngines = [engine1] };
engine1
.DoLayout(Arg.Any<IRectangle<int>>(), Arg.Any<IMonitor>())
.Returns(
new List<IWindowState>()
{
Expand All @@ -127,10 +132,10 @@ private static string Setup_ContextState(IContext ctx)
}
);

IWorkspace workspace2 = Substitute.For<IWorkspace>();
workspace2.Name.Returns("workspace2");
workspace2
.ActiveLayoutEngine.DoLayout(Arg.Any<IRectangle<int>>(), Arg.Any<IMonitor>())
ILayoutEngine engine2 = Substitute.For<ILayoutEngine>();
Workspace workspace2 = CreateWorkspace(ctx) with { BackingName = "workspace2", LayoutEngines = [engine2] };
engine2
.DoLayout(Arg.Any<IRectangle<int>>(), Arg.Any<IMonitor>())
.Returns(
new List<IWindowState>()
{
Expand All @@ -150,8 +155,7 @@ private static string Setup_ContextState(IContext ctx)
);

// Load the workspaces into the context.
ctx.WorkspaceManager.GetEnumerator()
.Returns(new List<IWorkspace>() { workspace1, workspace2 }.GetEnumerator());
AddWorkspacesToManager(ctx, root, workspace1, workspace2);

// Create the expected JSON.
return JsonSerializer.Serialize(
Expand All @@ -160,26 +164,28 @@ private static string Setup_ContextState(IContext ctx)
{
new(
"workspace1",
new() { new SavedWindow(1, new(0, 0, 0.1, 0.1)), new SavedWindow(2, new(0.1, 0.1, 0.1, 0.1)) }
new() { new SavedWindow(1, new(0, 0, 0.1, 0.1)), new SavedWindow(2, new(0.1, 0.1, 0.1, 0.1)) },
null
),
new(
"workspace2",
new()
{
new SavedWindow(3, new(0.2, 0.2, 0.1, 0.1)),
new SavedWindow(4, new(0.3, 0.3, 0.1, 0.1)),
}
},
null
),
}
)
);
}

[Theory, AutoSubstituteData]
public void Dispose_SavesState(IContext ctx)
[Theory, AutoSubstituteData<StoreCustomization>]
internal void Dispose_SavesState(IContext ctx, MutableRootSector root)
{
// Given
string expectedJson = Setup_ContextState(ctx);
string expectedJson = Setup_ContextState(ctx, root);
ctx.FileManager.SavedStateDir.Returns("savedStateDir");

CoreSavedStateManager sut = new(ctx);
Expand Down
Loading

0 comments on commit 1fe72a1

Please sign in to comment.