Skip to content
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
92 changes: 69 additions & 23 deletions WtProgram/GroupPlugins/SuperBarPlugin.fs
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,25 @@ type TaskBarButton(info) as this =
let mutable badgedIcon : TaskbarBadgedIcon option = None
let infoCell = Cell.create(None)
let tabWindowsCell = Cell.create(Map2())
let windowsToDispose = Cell.create(Set2())
/// Each window has a class and a pinned procedure of its own, freed with it.
let windows = Collections.Generic.Dictionary<IntPtr,IDisposable>()
let appId = Guid.NewGuid().ToString()
let addTab hwnd =
taskbar.AddTab(hwnd)
let registerTab(hwndTab) =
taskbar.RegisterTab(hwndTab, this.window.hwnd)
let setTabOrder(hwnd, hwndInsertBefore) =
taskbar.SetTabOrder(hwnd, hwndInsertBefore)
/// Each preview goes to the end in turn, which leaves them in the strip's order.
let sendTabOrder() =
infoCell.value |> Option.iter(fun info ->
info.tabs.map(fst).choose(tabWindowsCell.value.tryFind).iter <| fun (tabWindow:TaskbarTab) ->
taskbar.SetTabOrder(tabWindow.window.hwnd, IntPtr.Zero))
/// A new window can leave its group and join again within moments (Neovide and Alacritty
/// change their title as they start). Explorer can take the new preview's registration after
/// the order that places it, and then shows it first, so the order is sent again once settled.
let resendTabOrder = new System.Windows.Forms.Timer(Interval=500)
do resendTabOrder.Tick.Add(fun _ ->
resendTabOrder.Stop()
sendTabOrder())
let _window = lazy(
let config() : TbButton = infoCell.value.Value

Expand Down Expand Up @@ -257,6 +268,9 @@ type TaskBarButton(info) as this =
do
this.update info

/// How a tab's preview is found from its window.
static member key (hwnd:IntPtr) = hwnd.ToString()

member this.os = _os

member this.window : Window = _window.Force()
Expand Down Expand Up @@ -294,10 +308,13 @@ type TaskBarButton(info) as this =
this.window.setIcons(badged.icon)
previous |> Option.iter(fun icon -> (icon :> IDisposable).Dispose())

this.window.move(info.bounds)
this.window.setText(info.text)
// Each change is a notice to Explorer; updates come with every title and focus change.
let unchanged (read:TbButton -> 'a) = prevInfo |> Option.exists(fun previous -> read previous = read info)
if not(unchanged(fun i -> i.bounds)) then this.window.move(info.bounds)
if not(unchanged(fun i -> i.text)) then this.window.setText(info.text)

info.tabs.iter <| fun (tab, tabConfig) ->
let isNew = not(tabWindowsCell.value.contains tab)
let tabWindow = findOrCreate tabWindowsCell tab <| fun() ->
let config() = infoCell.value.Value.tabs.find(fst >> (=) tab) |> snd
let tabWindow = TaskbarTab(this, config, fun() -> this.window.size)
Expand All @@ -308,11 +325,10 @@ type TaskBarButton(info) as this =
registerTab(tabWindow.window.hwnd)
tabWindow

tabWindow.window.move(info.bounds)
tabWindow.window.setText(tabConfig.text)
let previousIcon = prevInfo |> Option.bind(fun previous ->
previous.tabs.tryFind(fst >> (=) tab) |> Option.map(fun (_,config) -> config.icon))
if previousIcon |> Option.exists(fun icon -> obj.ReferenceEquals(icon,tabConfig.icon)) |> not then
let previous = prevInfo |> Option.bind(fun previous -> previous.tabs.tryFind(fst >> (=) tab)) |> Option.map snd
if isNew || not(unchanged(fun i -> i.bounds)) then tabWindow.window.move(info.bounds)
if isNew || previous |> Option.forall(fun config -> config.text<>tabConfig.text) then tabWindow.window.setText(tabConfig.text)
if isNew || previous |> Option.forall(fun config -> not(obj.ReferenceEquals(config.icon,tabConfig.icon))) then
tabWindow.window.setIcons(tabConfig.icon)

let prevTabOrder =
Expand All @@ -322,8 +338,17 @@ type TaskBarButton(info) as this =

let tabOrder = tabOrder info
if tabOrder.list <> prevTabOrder.list then
tabOrder.iter <| fun tabWindow -> setTabOrder(tabWindow.window.hwnd, IntPtr.Zero)
sendTabOrder()
resendTabOrder.Stop()
resendTabOrder.Start()

/// The order was just changed and is to be sent again.
member this.isResendingTabOrder = resendTabOrder.Enabled

/// Explorer keeps a destroyed preview in the group's list unless it is told the tab closed.
member this.unregisterTab(hwnd:IntPtr) =
taskbar.UnregisterTab(hwnd)

/// A group can announce a window before its tab joins the strip, so the tab may have no preview window yet.
member this.tryTabWindow key = tabWindowsCell.value.tryFind(key) |> Option.map(fun tab -> tab.window)

Expand All @@ -344,16 +369,29 @@ type TaskBarButton(info) as this =

let window = this.os.createWindow wndProc dwmWindow.style dwmWindow.exStyle
TaskbarProxies.add window.hwnd
windowsToDispose.map(fun s -> s.add(window:?>IDisposable))
windows.[window.hwnd] <- window :?> IDisposable
this.os.windowFromHwnd(window.hwnd)

/// A tab's preview leaves with its tab, not with the whole group.
member this.releaseWindow(hwnd:IntPtr) =
match windows.TryGetValue hwnd with
| true,window ->
windows.Remove hwnd |> ignore
window.Dispose()
| _ -> ()

/// The windows this button holds: its own and one per tab.
member this.windowCount = windows.Count

interface IDisposable with
member this.Dispose() =
this.window.destroy()
resendTabOrder.Dispose()
// Previews are unregistered while the window they belong to still exists.
tabWindowsCell.value.items.iter <| fun (tab, tabWindow) ->
tabWindow.Dispose()
tabWindowsCell.set(Map2())
windowsToDispose.value.items.iter <| fun d -> d.Dispose()
for window in List.ofSeq windows.Values do window.Dispose()
windows.Clear()
badgedIcon |> Option.iter(fun icon -> (icon :> IDisposable).Dispose())
badgedIcon <- None

Expand Down Expand Up @@ -393,14 +431,17 @@ and TaskbarTab(parent:TaskBarButton, config,size) =
member this.window : Window = _window
member this.invalidate() = this.window.dwmInvalidateIconicBitmaps()
member this.Dispose() =
_window.destroy()
parent.unregisterTab(_window.hwnd)
parent.releaseWindow(_window.hwnd)

type SuperBarPlugin() as this =
let _os = OS()

let _taskbar = _os.getTaskbar().Value

let mutable taskbarButton = null
/// The preview last shown as active, None while the group is not in front.
let mutable activePreview : IntPtr option = None
let flashingTabs = Collections.Generic.HashSet<string>()
let captures = new TabCaptures()
/// A tab that comes to the front is captured once it has painted, for when it is hidden again.
Expand Down Expand Up @@ -456,7 +497,8 @@ type SuperBarPlugin() as this =
else
window.minimize()
tabs = this.ts.lorder.map <| fun tab ->
tab.GetHashCode().ToString(), {
let (Tab(hwnd)) = tab
TaskBarButton.key hwnd, {
close = fun() ->
let (Tab(hwnd)) = tab
this.os.windowFromHwnd(hwnd).close()
Expand Down Expand Up @@ -489,6 +531,7 @@ type SuperBarPlugin() as this =
let button =
match taskbarButton with
| null ->
activePreview <- None
let button = new TaskBarButton(tbButtonInfo)
taskbarButton <- button
button
Expand All @@ -497,11 +540,13 @@ type SuperBarPlugin() as this =
button


match this.foregroundTab |> Option.bind(fun tab -> button.tryTabWindow(tab.GetHashCode().ToString())) with
| Some(tabWindow) ->
this.taskbar.ActivateTab(button.window.hwnd)
this.taskbar.SetTabActive(tabWindow.hwnd, button.window.hwnd, 0)
| None -> ()
// Only when it changes: the group's titles change far more often than its front tab.
let active = this.foregroundTab |> Option.bind(fun (Tab(hwnd)) -> button.tryTabWindow(TaskBarButton.key hwnd)) |> Option.map(fun w -> w.hwnd)
if active <> activePreview then
activePreview <- active
active |> Option.iter(fun preview ->
this.taskbar.ActivateTab(button.window.hwnd)
this.taskbar.SetTabActive(preview, button.window.hwnd, 0))

member this.onForegroundChanged() =
this.updateTaskbar()
Expand All @@ -511,6 +556,7 @@ type SuperBarPlugin() as this =

member this.onRemoved(hwnd) =
captures.forget hwnd
flashingTabs.Remove(TaskBarButton.key hwnd) |> ignore
this.updateTaskbar()
this.showInTaskbar(hwnd, true)

Expand All @@ -526,9 +572,9 @@ type SuperBarPlugin() as this =

/// A tab stops flashing when its window redraws, which it also does when printed for a
/// preview. Invalidating then would make the taskbar print it again, over and over.
member this.onFlash(hwnd, flash) =
member this.onFlash(Tab(hwnd), flash) =
if taskbarButton <> null then
let key = hwnd.GetHashCode().ToString()
let key = TaskBarButton.key hwnd
taskbarButton.tryTabWindow(key) |> Option.iter(fun tabWindow ->
if flash || flashingTabs.Contains key then
if flash then flashingTabs.Add key |> ignore else flashingTabs.Remove key |> ignore
Expand Down
28 changes: 28 additions & 0 deletions tests/GroupLifecycle.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,34 @@ let main() =
onGroup combined (fun group -> for hwnd in group.windows.items.list do group.removeWindow hwnd)
pumpUntil "Taskbar proxies outlived their combined taskbar button" (fun () -> not(TaskbarProxies.contains proxy.Head))

// Neovide and Alacritty windows join, leave and join again as they start. Explorer can
// register the returning preview after the order that places it and show it first, so a
// changed order is sent again once settled.
do
let tab text : string * TbButtonTab =
text, { icon=Drawing.SystemIcons.Application; text=text; activate=ignore; close=ignore; preview=fun _ -> None }
let info texts : TbButton =
{ icon=Drawing.SystemIcons.Application; text="Order"; bounds=Rect(Pt(-20000,-20000),Sz(320,200))
activate=ignore; toggleMinimizeRestore=ignore; close=ignore; tabs=List2(texts |> List.map tab) }
let button = new TaskBarButton(info ["a";"b"])
try
pumpUntil "First preview order was not sent again" (fun () -> not button.isResendingTabOrder)
button.update(info ["a";"b";"c"])
button.update(info ["a";"b"])
button.update(info ["a";"b";"c"])
check button.isResendingTabOrder "A preview that rejoined is not ordered again"
pumpUntil "Preview order was not sent again" (fun () -> not button.isResendingTabOrder)
button.update(info ["a";"b";"c"])
check (not button.isResendingTabOrder) "An unchanged order is sent again"
// A preview that leaves frees its window, class and procedure then, not with the group.
let leaving = (button.tryTabWindow "c").Value.hwnd
for _ in 1..20 do
button.update(info ["a";"b"])
button.update(info ["a";"b";"c"])
check (button.windowCount = 4) (sprintf "Previews that left kept %d windows" (button.windowCount-4))
check (not(WinUserApi.IsWindow leaving) && not(TaskbarProxies.contains leaving)) "A preview that left kept its window"
finally (button :> IDisposable).Dispose()

// The setting rebuilds open groups that follow it, keeping what their tab menu chose.
api.setValue("combineIconsInTaskbar", box false)
let groupsOf hwnd = desktopApi.groups.list |> List.filter(fun g -> g.windows.contains((=)hwnd)) |> List.map(fun g -> g :?> GroupInfo)
Expand Down