Skip to content

Commit 5bc6465

Browse files
authored
[GUI] Off-screen locations (#273)
1 parent 41c2b0b commit 5bc6465

File tree

8 files changed

+134
-37
lines changed

8 files changed

+134
-37
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Bugfix: Updated example for "Process Explorer" to correct start- and attach to new instance.
99
- Bugfix: Updated CLI to load settings and fetch listening endpoint.
1010
- Bugfix: When a window gets minimized, WTQ could not longer toggle it.
11+
- Bugfix: When adding an app without any matching criteria, any window would be attached to (now only attaches when at least 1 criterion is present).
1112
- Feature: "Resize" option (defaults to "Always"), can be used to disable resizing the app's window (disables alignment settings). Useful for apps that don't respond well to being resized, like some Electron apps.
1213
```jsonc
1314
"Apps": [
@@ -17,6 +18,7 @@
1718
]
1819
}
1920
```
21+
- Feature: GUI - "Off-Screen Locations" can now be configured from the GUI.
2022

2123
## [v2.0.17] / 2025-08-27
2224
- Breaking change: On Windows, when using a hotkey with the "Shift" modifier and the "KeyChar" in the settings, you need to use the non-shifted character now:

src/10-Core/Wtq/Configuration/WtqSharedOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ public abstract class WtqSharedOptions : IValidatableObject
129129
/// <para>
130130
/// By default, WTQ looks for empty space in this order: Above, Below, Left, Right.
131131
/// </para>
132+
/// <para>
133+
/// If no free space can be found in any of the specified locations, the app will just blink on- and off the screen,
134+
/// without any animation.
135+
/// </para>
132136
/// </summary>
133137
/// <example>
134138
/// <code>

src/10-Core/Wtq/Utils/DefaultCollectionValue.cs

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Wtq.Utils;
2+
3+
/// <summary>
4+
/// Version of <see cref="DefaultValueAttribute"/>, but for collections.
5+
/// </summary>
6+
[AttributeUsage(AttributeTargets.Property)]
7+
public sealed class DefaultCollectionValueAttribute(object[] values) : Attribute
8+
{
9+
public ICollection<object> Values { get; } = Guard.Against.Null(values);
10+
}

src/20-Services/Wtq.Services.UI/Components/Settings/Position/OffScreenLocationsSetting.razor

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,115 @@
22

33
@code {
44

5-
// private void Remove(HotkeyOptions hk)
6-
// {
7-
// Value.Remove(hk);
8-
//
9-
// StateHasChanged();
10-
// }
5+
private List<OffScrLoc> Locs { get; set; }
6+
7+
protected override void OnInitialized()
8+
{
9+
Notifier.OnNotify(() => InvokeAsync(UpdateLocs));
10+
11+
UpdateLocs();
12+
}
13+
14+
private void UpdateLocs()
15+
{
16+
// Get current setting value:
17+
// - App-specific
18+
// - Global
19+
// - Default (currently from constant, instead of attribute)
20+
var locs = Value ?? WtqConstants.DefaultOffScreenLocations; // TODO: Default collection values aren't coming through Value yet.
21+
22+
// Convert value to view model.
23+
Locs = locs.Select(l => new OffScrLoc() { IsActive = true, Loc = l}).ToList();
24+
25+
// Add inactive values.
26+
foreach (var val in WtqConstants.DefaultOffScreenLocations)
27+
{
28+
if (Locs.Any(l => l.Loc == val))
29+
{
30+
continue;
31+
}
32+
33+
Locs.Add(new() { IsActive = false, Loc = val});
34+
}
35+
36+
StateHasChanged();
37+
}
38+
39+
private void SetActive(OffScrLoc loc, bool isActive)
40+
{
41+
loc.IsActive = isActive;
42+
43+
UpdateValue();
44+
}
45+
46+
private void Up(OffScrLoc loc) => MoveLoc(loc, -1);
47+
48+
private void Down(OffScrLoc loc) => MoveLoc(loc, 1);
49+
50+
private void MoveLoc(OffScrLoc loc, int pos)
51+
{
52+
var l = Locs.FirstOrDefault(ll => ll.Loc == loc.Loc);
53+
var idx = Locs.IndexOf(l);
54+
55+
Locs.Remove(l);
56+
57+
var ins = idx + pos;
58+
59+
if (ins < 0)
60+
{
61+
Locs.Add(l);
62+
}
63+
else
64+
{
65+
Locs.Insert(idx + pos, l);
66+
}
67+
68+
UpdateValue();
69+
}
70+
71+
private void UpdateValue() => Value = Locs.Where(l => l.IsActive).Select(l => l.Loc).ToList();
72+
73+
private class OffScrLoc
74+
{
75+
public bool IsActive { get; set; }
76+
77+
public OffScreenLocation Loc { get; set; }
78+
}
1179

1280
}
1381

14-
<WtqSetting TProperty="ICollection<OffScreenLocation>" Get="Get" Set="Set">
82+
<WtqSetting TProperty="ICollection<OffScreenLocation>" Get="Get" Set="Set" Default="Default">
1583
<Content>
1684

17-
TODO
18-
19-
@* <RadzenStack> *@
20-
@* *@
21-
@* @foreach (var hk in Value) *@
22-
@* { *@
23-
@* <Hotkey Options="hk" OnRemove="@(() => Remove(hk))"/> *@
24-
@* } *@
25-
@* *@
26-
@* <RadzenStack> *@
27-
@* <RadzenButton *@
28-
@* Icon="add" *@
29-
@* Size="ButtonSize.Medium" *@
30-
@* Click="@(() => Value.Add(new()))" *@
31-
@* /> *@
32-
@* </RadzenStack> *@
33-
@* *@
34-
@* </RadzenStack> *@
85+
<RadzenStack>
86+
87+
@foreach (var loc in Locs)
88+
{
89+
<RadzenStack Orientation="Orientation.Vertical" Gap="0">
90+
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="8" JustifyContent="JustifyContent.SpaceBetween">
91+
92+
<!-- Active -->
93+
<RadzenCheckBox TValue="bool" @bind-Value="loc.IsActive" Change="v => SetActive(loc, v)"/>
94+
95+
<!-- Label -->
96+
@if (loc.IsActive)
97+
{
98+
<RadzenLabel Text="@loc.Loc.ToString()" Style="flex: 1;" />
99+
}
100+
else
101+
{
102+
<RadzenLabel Text="@loc.Loc.ToString()" Style="flex: 1; text-decoration: line-through;" />
103+
}
104+
105+
<!-- Up/Down -->
106+
<RadzenButton Click="() => Up(loc)" Icon="keyboard_arrow_up" Size="ButtonSize.Medium"/>
107+
<RadzenButton Click="() => Down(loc)" Icon="keyboard_arrow_down" Size="ButtonSize.Medium"/>
108+
109+
</RadzenStack>
110+
</RadzenStack>
111+
}
112+
113+
</RadzenStack>
35114

36115
</Content>
37116
</WtqSetting>

src/20-Services/Wtq.Services.UI/Components/WtqSetting.razor

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
public string? Subtitle => AttrUtils.GetPrompt(Get);
1515

16+
[CascadingParameter]
17+
public Notifier Notifier { get; set; } = null!;
18+
1619
[EditorRequired]
1720
[Parameter]
1821
public RenderFragment Content { get; set; } = null!;
@@ -44,7 +47,12 @@
4447
private IEnumerable<ValidationResult> ValidationResultsForProperty
4548
=> ValidationResults.Where(v => v.MemberNames.Any(m => m.Equals(PropertyName)));
4649

47-
private void Reset() => Set(default);
50+
private void Reset()
51+
{
52+
Set(default);
53+
54+
Notifier.Notify();
55+
}
4856

4957
private void ShowTooltip(ElementReference elementReference, TooltipOptions options)
5058
=> TooltipService.Open(

src/20-Services/Wtq.Services.UI/Pages/App/_Index.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
<WtqSettingNumberSlider TProperty="float?" Get="() => AppOpts.VerticalOffset" Set="v => AppOpts.VerticalOffset = v" Default="() => GlobalOpts.VerticalOffset" />
176176

177177
<!-- Off-screen locations -->
178-
<OffScreenLocationsSetting Get="() => GlobalOpts.OffScreenLocations" Set="v => GlobalOpts.OffScreenLocations = v" Default="() => GlobalOpts.OffScreenLocations" />
178+
<OffScreenLocationsSetting Get="() => AppOpts.OffScreenLocations" Set="v => AppOpts.OffScreenLocations = v" Default="() => GlobalOpts.OffScreenLocations" />
179179

180180
</RadzenStack>
181181
</RadzenFieldset>

src/20-Services/Wtq.Services.UI/Pages/GlobalSettings/_Index.razor

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
<RadzenFieldset Text="Position">
105105
<RadzenStack Gap="1rem">
106106

107+
<!-- Resize -->
108+
<WtqSettingRadio TProperty="Resizing" Get="() => GlobalOpts.Resize" Set="v => GlobalOpts.Resize = v" Default="() => GlobalOpts.Resize" />
109+
107110
<!-- Horizontal screen coverage -->
108111
<WtqSettingNumberSlider TProperty="float?" Get="() => GlobalOpts.HorizontalScreenCoverage" Set="v => GlobalOpts.HorizontalScreenCoverage = v" Default="() => GlobalOpts.HorizontalScreenCoverage" />
109112

@@ -117,7 +120,7 @@
117120
<WtqSettingNumberSlider TProperty="float?" Get="() => GlobalOpts.VerticalOffset" Set="v => GlobalOpts.VerticalOffset = v" Default="() => GlobalOpts.VerticalOffset" />
118121

119122
<!-- Off-screen locations -->
120-
<OffScreenLocationsSetting Get="() => GlobalOpts.OffScreenLocations" Set="v => GlobalOpts.OffScreenLocations = v" />
123+
<OffScreenLocationsSetting Get="() => GlobalOpts.OffScreenLocations" Set="v => GlobalOpts.OffScreenLocations = v" Default="() => GlobalOpts.OffScreenLocations" />
121124

122125
</RadzenStack>
123126
</RadzenFieldset>

0 commit comments

Comments
 (0)