diff --git a/WtProgram/GroupPlugins/SuperBarPlugin.fs b/WtProgram/GroupPlugins/SuperBarPlugin.fs index 9eec97e..f512683 100755 --- a/WtProgram/GroupPlugins/SuperBarPlugin.fs +++ b/WtProgram/GroupPlugins/SuperBarPlugin.fs @@ -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() 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 @@ -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() @@ -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) @@ -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 = @@ -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) @@ -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 @@ -393,7 +431,8 @@ 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() @@ -401,6 +440,8 @@ type SuperBarPlugin() as this = 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() let captures = new TabCaptures() /// A tab that comes to the front is captured once it has painted, for when it is hidden again. @@ -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() @@ -489,6 +531,7 @@ type SuperBarPlugin() as this = let button = match taskbarButton with | null -> + activePreview <- None let button = new TaskBarButton(tbButtonInfo) taskbarButton <- button button @@ -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() @@ -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) @@ -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 diff --git a/tests/GroupLifecycle.fsx b/tests/GroupLifecycle.fsx index 589cf5f..37a3b0a 100644 --- a/tests/GroupLifecycle.fsx +++ b/tests/GroupLifecycle.fsx @@ -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)