Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f7906d3
Say why a group's taskbar icons are not combined
leafOfTree Oct 23, 2026
2c17bda
Add shortcuts to save a workspace and restore the last one
leafOfTree Oct 23, 2026
a08ec22
Show the workspace shortcuts on the Shortcuts page
leafOfTree Oct 23, 2026
333fbd7
Keep the session restore description to one line
leafOfTree Oct 23, 2026
df93688
List only groups of several windows in the diagnostics report's taskb…
leafOfTree Oct 23, 2026
7a14001
Slide notices in and out at the bottom right of the screen
leafOfTree Oct 23, 2026
8e2f06c
Quote the workspace's name in its notices, with no count after a colon
leafOfTree Oct 23, 2026
729e5e1
Show notices as a card of fixed size with a bold title above the message
leafOfTree Oct 23, 2026
ad16680
Keep notices up while pointed at, with a close button, and errors unt…
leafOfTree Oct 23, 2026
becc420
Inset the reason under Combine taskbar icons so it does not read as a…
leafOfTree Oct 23, 2026
e555b6f
Bring a window's tabs up with it when WindowTabs runs with other rights
leafOfTree Oct 23, 2026
73452be
Keep a window's tabs over it when it rises with no change of foregrou…
leafOfTree Oct 23, 2026
699a2c4
Put an admin WindowTabs' strip back over its window as the window is …
leafOfTree Oct 23, 2026
9a7346f
Put an admin WindowTabs' strip back at every change of window order, …
leafOfTree Oct 23, 2026
3c6fd20
Look at an admin WindowTabs' strip every fifth of a second, now that …
leafOfTree Oct 23, 2026
a3e6dfb
Keep tabs over split panes and an admin WindowTabs' windows by listen…
leafOfTree Oct 23, 2026
d583c82
Say that an admin WindowTabs keeps normal windows' taskbar buttons ap…
leafOfTree Oct 23, 2026
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ right-drag asks, and **Ctrl + Z** in Explorer undoes it.
or below and for moving the focus between panes are under **Settings → Shortcuts**, unset by
default.
- **Workspaces:** save groups and window positions, then restore them onto open windows;
apps that are not running can be launched and their windows restored.
apps that are not running can be launched and their windows restored. Shortcuts to save
one and to restore the last saved or restored one are under **Settings → Shortcuts**,
unset by default.
- **Taskbar:** optionally combine each group into one taskbar button, which shows every
app in the group.
- **Languages:** English, 中文, 日本語.
Expand All @@ -86,7 +88,9 @@ To update, exit WindowTabs from the tray and replace the exe. Settings are kept.
- **Odd behavior after upgrading?** **Settings → Support → Reset to defaults**
(your old settings are backed up first).
- **Run as administrator?** Not needed and not recommended. Windows blocks some
input between admin and normal windows.
input between admin and normal windows, and groups of normal windows keep a
taskbar button each even with **Combine taskbar icons** on; the group's tab menu
says why.
- **Dropping files on a tab does nothing?** Windows blocks dragging from an app into
one running as administrator.

Expand Down
15 changes: 15 additions & 0 deletions WtProgram/GroupPlugins/SuperBarPlugin.fs
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ type SuperBarPlugin(canShareInput:IntPtr -> bool, ?desktopOf:IntPtr -> Guid opti
let mutable desktopSubscription : IDisposable option = None
let mutable disposed = false
let mutable exitCleanedUp = false
let mutable currentCombineState = SingleWindow

new() = SuperBarPlugin(fun hwnd -> Services.get<WindowGroup>().canShareInput hwnd)

Expand Down Expand Up @@ -832,6 +833,12 @@ type SuperBarPlugin(canShareInput:IntPtr -> bool, ?desktopOf:IntPtr -> Guid opti

member _.taskbarVisibilityChanges = taskbarVisibility.changeCount
member _.isAwaitingDesktopPlacement = placementRetry.Enabled
member _.combineState = currentCombineState

/// The tab menu and the diagnostics report say why the group's windows keep their own buttons.
member private this.setCombineState state =
currentCombineState <- state
TaskbarCombineStates.set this.ts.hwnd state

/// A window without a taskbar button is on no virtual desktop and shows on all of them;
/// a window the taskbar window owns follows that window's desktop instead.
Expand Down Expand Up @@ -910,11 +917,16 @@ type SuperBarPlugin(canShareInput:IntPtr -> bool, ?desktopOf:IntPtr -> Guid opti
let desktop = placementDesktop
placementAttempts.Observe(memberSet,desktop,this.canCombine)
if not this.canCombine || (desktop.IsNone && placedOwner=IntPtr.Zero) then
this.setCombineState(
if members.count<=1 then SingleWindow
elif not this.canCombine then DifferentPermissions
else DesktopUnknown)
this.clearTaskbarButton()
members.iter(fun hwnd -> this.showInTaskbar(hwnd,true))
// The asynchronous desktop query publishes completion; no timer polls an unknown answer.
placementRetry.Stop()
elif placedOwner=IntPtr.Zero && not(placementAttempts.TryBegin()) then
this.setCombineState PlacementFailed
members.iter(fun hwnd -> this.showInTaskbar(hwnd,true))
this.retryPlacement()
elif members.isEmpty.not then
Expand Down Expand Up @@ -1003,10 +1015,12 @@ type SuperBarPlugin(canShareInput:IntPtr -> bool, ?desktopOf:IntPtr -> Guid opti
active |> Option.iter button.markActive

if this.adopt desktop then
this.setCombineState Combined
placementRetry.Stop()
placementAttempts.Succeeded()
members.iter(fun hwnd -> this.showInTaskbar(hwnd,false))
else
this.setCombineState PlacementFailed
// Keep each application's desktop and button until the proxy is safely placed.
this.clearTaskbarButton()
members.iter(fun hwnd -> this.showInTaskbar(hwnd,true))
Expand Down Expand Up @@ -1146,6 +1160,7 @@ type SuperBarPlugin(canShareInput:IntPtr -> bool, ?desktopOf:IntPtr -> Guid opti
if not exitCleanedUp then
exitCleanedUp <- true
disposed <- true
TaskbarCombineStates.remove this.ts.hwnd
desktopSubscription |> Option.iter(fun subscription -> subscription.Dispose())
desktopSubscription <- None
iconSubscription |> Option.iter(fun subscription -> subscription.Dispose())
Expand Down
14 changes: 14 additions & 0 deletions WtProgram/ManagerViewService/Views/DiagnosticsView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ type DiagnosticsView() =
| SettingsFile.AppData -> "AppData"
| SettingsFile.WorkingDirectory -> "working directory"))
| _ -> ()
// Whether each group's taskbar icons are combined, and if not why: a group's windows can
// keep their own buttons while its tab menu shows combining on. A group of one window has
// nothing to combine.
result.["taskbarGroups"] <-
JArray(groups.list |> List.filter(fun group -> group.windows.count>1) |> List.map(fun group ->
let state =
match TaskbarCombineStates.tryGet group.hwnd with
| Some Combined -> "combined"
| Some SingleWindow -> "single window"
| Some DifferentPermissions -> "not combined: different permissions"
| Some DesktopUnknown -> "not combined: virtual desktop unknown"
| Some PlacementFailed -> "not combined: placement on the virtual desktop failed"
| None -> "off"
JObject(JProperty("windows",group.windows.count),JProperty("taskbar",state)) |> box) |> List.toArray)
// The longest part, and optional, comes last.
if includeWindows.Checked then result.["windows"] <- windowDetails()
result
Expand Down
13 changes: 8 additions & 5 deletions WtProgram/ManagerViewService/Views/HotKeyView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ open System.Windows.Forms

type HotKeyView() =
let panel,table = SettingsUi.page()
// Setting id for each program hotkey, so a conflict can name the other action.
let actions = ["nextTab","next-tab";"prevTab","previous-tab";"searchTabs","search-tabs";"newTab","new-tab";"closeTab","close-tab";"numberLeader","number-leader"
"splitPickRight","split-pick-right";"splitPickBelow","split-pick-below"
"splitFocusLeft","split-focus-left";"splitFocusRight","split-focus-right";"splitFocusUp","split-focus-up";"splitFocusDown","split-focus-down"]
// Setting id for each program hotkey, so a conflict can name the other action. Read from the
// catalog: a list of its own here left new shortcuts off the page.
let actions = SettingsCatalog.all |> List.choose(fun item -> match item.binding with Shortcut(key,_) -> Some(key,item.id) | _ -> None)
let isSplit (key:string) = key.StartsWith "split"
let isWorkspace (key:string) = key.EndsWith "Workspace"
let hotKey key =
let editor = new SettingsShortcutInput(Font=SettingsUi.bodyFont(),Shortcut=Services.program.getHotKey key)
editor.Changed.Add(fun _ ->
Expand Down Expand Up @@ -38,7 +38,7 @@ type HotKeyView() =
SettingsUi.add table keyboard
SettingsUi.note keyboard (tr Strings.Shortcuts.keyboardNote)
for (key,editor),(_,id) in List.zip editors actions do
if key<>"numberLeader" && not(isSplit key) then SettingsUi.settingRow keyboard id editor
if key<>"numberLeader" && not(isSplit key) && not(isWorkspace key) then SettingsUi.settingRow keyboard id editor
// Rows inset under a toggle and collapsed while it is off.
let dependOn (toggle:SettingsToggle) (rows:SettingsRow list) =
for row in rows do SettingsUi.indentDependentRow row
Expand Down Expand Up @@ -149,6 +149,9 @@ type HotKeyView() =
let split = SettingsUi.sectionCard table (tr Strings.Shortcuts.splitView)
for (key,editor),(_,id) in List.zip editors actions do
if isSplit key then SettingsUi.settingRow split id editor
let workspaces = SettingsUi.sectionCard table (tr Strings.Pages.workspaces)
for (key,editor),(_,id) in List.zip editors actions do
if isWorkspace key then SettingsUi.settingRow workspaces id editor
let pointer = SettingsUi.sectionCard table (tr Strings.Shortcuts.mouse)
SettingsBindings.toggleRow pointer "activate-on-hover"
SettingsBindings.toggleRow pointer "shift-scroll"
Expand Down
6 changes: 6 additions & 0 deletions WtProgram/ManagerViewService/Views/WorkspaceView.fs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ type WorkspaceView() as this =
if not this.wm.isReadOnly then this.list.RowActions <- [EditGlyph;DeleteGlyph]
loaded <- true
this.reload()
// A workspace saved by shortcut meanwhile shows here, and is not lost at this page's next save.
let saved = WorkspaceModel.saved.Subscribe(fun source ->
if not (obj.ReferenceEquals(source,this.wm)) then
this.wm.loadSettings()
this.reload())
panel.Disposed.Add(fun _ -> saved.Dispose())
panel

/// Rebuilds the rows from the model in the order saved, keeping expansion and selection.
Expand Down
Loading
Loading