├── .gitignore ├── LICENSE ├── README.md ├── plugin-window-helper.ahk └── readme ├── menu-multi.gif ├── menu.gif └── mouse.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .history/ 2 | Class_Console/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rand Scullard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plug-In Window Helper 2 | 3 | (PWH for short) 😀 4 | 5 | An [AutoHotkey](https://www.autohotkey.com/) script for users of Ableton Live and Bitwig Studio. Get 6 | instant access to your plug-in windows from anywhere in your DAW, and hide them when they're in the 7 | way! 8 | 9 | ## Features 10 | 11 | ### Menu Hotkey 12 | 13 | Click a hotkey to display a menu of plug-in windows, organized by track: 14 | 15 | ![Menu Animation](readme/menu.gif) 16 | 17 | ### Quick-Toggle Hotkey 18 | 19 | Click to instantly hide all visible plug-in windows; a second click brings back just the windows 20 | that were visible. 21 | 22 | ## Requirements 23 | 24 | You need [AutoHotkey](https://www.autohotkey.com/download/) version 1.1.33 or later. 25 | 26 | PWH has been tested with the following: 27 | 28 | - Ableton Live versions 10 and 11 29 | - Bitwig Studio version 4 30 | - Windows 10 31 | 32 | It should work with earlier versions as well - give it a try and let me know! 33 | 34 | ## Setup 35 | 36 | Download 37 | [plugin-window-helper.ahk](https://raw.githubusercontent.com/RandScullard/plugin-window-helper/master/plugin-window-helper.ahk) 38 | from GitHub and save it somewhere reasonable on your PC. (If you're an AutoHotkey user, you probably 39 | already have a folder where you keep your scripts.) 40 | 41 | To give PWH a try, you can just double-click the `plugin-window-helper.ahk` file in the Windows File 42 | Explorer. The AutoHotkey icon will appear in the system tray so you know it's running. If you like 43 | it, you'll probably want to [make it start automatically with 44 | Windows](https://www.autohotkey.com/docs/FAQ.htm#Startup). 45 | 46 | ### Advanced Setup 47 | 48 | If you already have an AutoHotkey script with your favorite handy functions, simply 49 | include PWH in the auto-execute section of your script and call the `PWHInit` function: 50 | 51 | #Include plugin-window-helper.ahk 52 | PWHInit() 53 | 54 | PWH has been carefully crafted to **not** cause unwanted changes in your script - if you run into 55 | any problems, please open an issue here on GitHub. 56 | 57 | **Note:** You can customize PWH by passing parameters to the `PWHInit` function - see the 58 | Customization section below. 59 | 60 | ## How To Use 61 | 62 | PWH's hotkeys are assigned to the **mouse side buttons**: 63 | 64 |       ![Mouse](readme/mouse.svg) 65 | 66 | The hotkeys are also assigned to **touchpad gestures**: 67 | 68 | - **Menu:** Three-finger tap 69 | - **Quick-Toggle:** Four-finger tap 70 | 71 | The Quick-Toggle hotkey is also assigned to the **Pause** key. (Not the ⏯️ key, but the old-school 72 | Pause key, next to Scroll Lock on a standard desktop keyboard.) 73 | 74 | **Note:** If you don't like these hotkey assignments, you can change them! See the Customization 75 | section below. 76 | 77 | ### Using the Menu 78 | 79 | In the menu, a normal click on an entry shows that window and hides all others. This is great when 80 | you want to work with one plug-in at a time, but you can also **Ctrl+click** or **right-click** to 81 | hide and show multiple windows (keeping the menu open): 82 | 83 |       ![Menu Animation](readme/menu-multi.gif) 84 | 85 | Hit Esc or click anywhere outside the menu to dismiss it. 86 | 87 | **Time-Saving Tips** 88 | 89 | - Press and hold the mouse side button to open the menu, drag to the entry you want, and release the 90 | button - all in one smooth motion. 91 | - When you have multiple plug-in windows open and you want one of the windows to be in front of the 92 | others, use Ctrl+click or right-click to **double-click** the window's menu entry. This will 93 | bring the window to the front. 94 | - Another way to show a hidden window is to **double-click** your DAW's Open Plug-In Window button. 95 | Sometimes this is more convenient than using the PWH menu. 96 | 97 | **Keep in Mind** 98 | 99 | PWH can only hide and show plug-in windows that are already open. Unfortunately, it can't 100 | open them for you! This means: 101 | 102 | - Each time you open a project, you will need to manually open the plug-in windows you want to work 103 | with, just like you did before. 104 | - Don't close plug-in windows - use PWH to hide them. 105 | 106 | ## Customization 107 | 108 | Customize the behavior of PWH by passing parameters to the `PWHInit` function: 109 | 110 | PWHInit(AbletonTheme, MenuHotkeys, QuickToggleHotkeys) 111 | 112 | - If you are running the PWH script standalone, edit `plugin-window-helper.ahk` and find the one and 113 | only call to `PWHInit` near the top of the file. Modify it to add parameters. 114 | - If you've included PWH into your AutoHotkey script, then the call to `PWHInit` is in your script; 115 | add the parameters there. 116 | 117 | **AbletonTheme** 118 | 119 | If you're using Ableton Live, pass the name of your Live theme to make PWH's colors match: 120 | `"Light"`, `"Mid Light"`, `"Mid Dark"`, `"Dark"` 121 | 122 | The default value is `"Mid Light"`. Bitwig Studio does not support themes, so if you're using Bitwig 123 | this parameter is ignored. 124 | 125 | **MenuHotkeys, QuickToggleHotkeys** 126 | 127 | To customize the hotkeys that will open the PWH menu and trigger the quick-toggle feature, use these 128 | two parameters. Each one must be an array of strings where each string is the name of a 129 | [hotkey](https://www.autohotkey.com/docs/Hotkeys.htm), including any modifier symbols. 130 | 131 | The default values are: 132 | 133 | - MenuHotkeys: `["#^+F22", "*XButton1"]` 134 | - QuickToggleHotkeys: `["#^+F24", "*XButton2", "Pause"]` 135 | 136 | Please note: 137 | 138 | - Win+Ctrl+Shift+F22 is generated by Windows when you do a three-finger tap on the touchpad, and 139 | Win+Ctrl+Shift+F24 is generated for a four-finger tap. 140 | - Mouse button hotkeys should include the `*` wildcard prefix so they will trigger even if any 141 | modifier keys (Ctrl, etc.) are held down. 142 | - The topic of hotkeys can get pretty deep, and it goes way beyond the scope of this text. Please 143 | consult the [AutoHotkey documentation](https://www.autohotkey.com/docs/Hotkeys.htm) if you run 144 | into any trouble. 145 | 146 | ## License 147 | 148 | MIT License 149 | 150 | Copyright (c) 2021 Rand Scullard -------------------------------------------------------------------------------- /plugin-window-helper.ahk: -------------------------------------------------------------------------------- 1 | ; plugin-window-helper.ahk 2 | ; version 1.0.0 3 | 4 | ; Copyright (c) 2021 Rand Scullard 5 | 6 | ; Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 7 | ; associated documentation files (the "Software"), to deal in the Software without restriction, 8 | ; including without limitation the rights to use, copy, modify, merge, publish, distribute, 9 | ; sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 10 | ; furnished to do so, subject to the following conditions: 11 | 12 | ; The above copyright notice and this permission notice shall be included in all copies or 13 | ; substantial portions of the Software. 14 | 15 | ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 16 | ; NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | ; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | ; DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 19 | ; OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | #Requires AutoHotkey v1.1.33+ 23 | 24 | 25 | ; ----------------------------------------------------------------------------------------------- 26 | ; See https://github.com/AfterLemon/Class_Console 27 | ; Uncomment the following to create a log window to use for debugging: 28 | 29 | ; #Include Class_Console\Class_Console.ahk 30 | ; global pwhConsole 31 | ; Class_Console("pwhConsole",0,0,800,1100) 32 | ; pwhConsole.show() 33 | ; ----------------------------------------------------------------------------------------------- 34 | 35 | 36 | ; Windows constants: 37 | global EVENT_OBJECT_SHOW := 0x8002 38 | global GW_OWNER := 4 39 | global HWND_BOTTOM := 1 40 | global MONITOR_DEFAULTTONEAREST := 2 41 | global SS_NOPREFIX := 0x80 42 | global SWP_SHOWWINDOW := 0x40 43 | global WINEVENT_OUTOFCONTEXT := 0x0000 44 | global WINEVENT_SKIPOWNPROCESS := 0x0002 45 | global WM_LBUTTONUP := 0x0202 46 | global WM_MBUTTONUP := 0x0208 47 | global WM_RBUTTONUP := 0x0205 48 | global WS_EX_STATICEDGE := 0x20000 49 | global WS_EX_TRANSPARENT := 0x20 50 | global WS_VISIBLE := 0x10000000 51 | 52 | ; Menu command IDs besides the ones associated with plug-in windows. Note that they are negative to 53 | ; avoid conflicting with the positive command IDs for plug-in windows: 54 | global PWH_CMDID_HIDE_ALL := -1 55 | global PWH_CMDID_SHOW_ALL := -2 56 | 57 | global pwhAbletonTheme 58 | global pwhOpenMenuTickCount 59 | global pwhIsQuickToggleRunning 60 | global pwhIsBuildingGui 61 | global pwhGuiHwnd 62 | global pwhDawMainProcess 63 | global pwhDawProcesses 64 | global pwhIsBitwig 65 | global pwhOrigActiveWindow 66 | global pwhPluginWindows := [] 67 | global pwhPluginWindowsLastState := {} 68 | global pwhPluginWindowsRestore := {} 69 | global pwhWindowShowEvents := {} 70 | global pwhHighlightHwnd 71 | global pwhMenuItemControls 72 | global pwhBorderWidth 73 | 74 | 75 | ; ----------------------------------------------------------------------------------------------- 76 | ; If this script is running standalone, we call PWHInit here in the auto-execute section. 77 | ; When this script is included in another script, THAT script is responsible for calling PWHInit. 78 | if(!A_IsCompiled and A_LineFile = A_ScriptFullPath) 79 | PWHInit() 80 | ; ----------------------------------------------------------------------------------------------- 81 | 82 | 83 | PWHInit(abletonTheme := "Mid Light", menuHotkeys := 0, quickToggleHotkeys := 0) 84 | { 85 | ; Win+Ctrl+Shift+F22 is generated by Windows when you do a three-finger tap on the touchpad, and 86 | ; Win+Ctrl+Shift+F24 represents a four-finger tap. Since AutoHotkey doesn't allow arrays for 87 | ; optional parameter default values, we have to assign the default hotkeys here. 88 | if(!menuHotkeys) 89 | menuHotkeys := ["#^+F22", "*XButton1"] 90 | if(!quickToggleHotkeys) 91 | quickToggleHotkeys := ["#^+F24", "*XButton2", "Pause"] 92 | 93 | pwhAbletonTheme := abletonTheme 94 | 95 | ; This group helps us find the main DAW window as well as various popups created by the DAW, such 96 | ; as the settings window. 97 | GroupAdd, pwhDawGroup, ahk_class Ableton Live Window Class 98 | GroupAdd, pwhDawGroup, ahk_class bitwig 99 | 100 | ; This group is used to find all of the DAW's plug-in windows. 101 | GroupAdd, pwhPluginGroup, ahk_class AbletonVstPlugClass 102 | GroupAdd, pwhPluginGroup, ahk_class Vst3PlugWindow 103 | GroupAdd, pwhPluginGroup, ahk_class vst3window 104 | 105 | ; When we look for popup windows owned by plug-in windows, we need to ignore any IME windows 106 | ; created by the OS and linked to the active plug-in window without the DAW's knowledge. 107 | GroupAdd, pwhIgnoreGroup, ahk_class IME 108 | 109 | ; AutoHotkey's built-in GUI control click handling fires on button-down, but we want our menu 110 | ; items to trigger on button-up, so we have to handle the button-up messages ourselves. 111 | fnOnGuiButtonUp := Func("PWHOnGuiButtonUp") 112 | OnMessage(WM_LBUTTONUP, fnOnGuiButtonUp) 113 | OnMessage(WM_MBUTTONUP, fnOnGuiButtonUp) 114 | OnMessage(WM_RBUTTONUP, fnOnGuiButtonUp) 115 | 116 | ; See PWHOnWindowShowEvent. 117 | cb := RegisterCallback("PWHOnWindowShowEvent", "Fast") 118 | pwhWinEventHook := DllCall("SetWinEventHook" 119 | , "UInt", EVENT_OBJECT_SHOW 120 | , "UInt", EVENT_OBJECT_SHOW 121 | , "Ptr", 0 122 | , "Ptr", cb 123 | , "UInt", 0 124 | , "UInt", 0 125 | , "UInt", WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS) 126 | 127 | ; Register our hotkeys. We do this using the Hotkey command rather than "traditional" hotkeys 128 | ; because (a) this allows us to make the hotkeys configurable via parameters to PWHInit, and (b) 129 | ; we don't want to terminate the auto-execute section when this script is included into another 130 | ; AutoHotkey script... 131 | 132 | fnIsDawWinActive := Func("PWHIsDawWinActive") 133 | fnIsGuiActive := Func("PWHIsGuiActive") 134 | fnIsDawWinOrGuiActive := Func("PWHIsDawWinOrGuiActive") 135 | 136 | Hotkey, If, % fnIsDawWinActive 137 | 138 | for i, hk in menuHotkeys 139 | Hotkey, %hk%, PWHOpenMenu 140 | 141 | Hotkey, If, % fnIsGuiActive 142 | 143 | for i, hk in menuHotkeys 144 | Hotkey, %hk% Up, PWHOnGuiActiveMenuHotkeyUp 145 | 146 | Hotkey, If, % fnIsDawWinOrGuiActive 147 | 148 | for i, hk in quickToggleHotkeys 149 | Hotkey, %hk%, PWHQuickToggle 150 | 151 | Hotkey, If 152 | 153 | ; Register a function to run when this script exits. 154 | OnExit("PWHOnExit") 155 | } 156 | 157 | PWHOnExit() 158 | { 159 | ; This function runs when this script exits... 160 | 161 | ; See PWHOnWindowShowEvent. 162 | DllCall("UnhookWinEvent", "Ptr", pwhWinEventHook) 163 | } 164 | 165 | PWHOnThreadStart() 166 | { 167 | ; Every newly launched thread starts off with certain default settings that this script needs to 168 | ; override. Normally we would override the defaults in the auto-execute section, but this script 169 | ; can be included into another script, so we don't want to change any global settings. There's a 170 | ; call to this function at every thread entry point in this script. 171 | 172 | ; Make this script run as fast as possible by disabling all AutoHotkey delays. 173 | SetBatchLines, -1 174 | SetWinDelay, -1 175 | SetControlDelay, -1 176 | 177 | ; This script's main feature is working with hidden windows. 178 | DetectHiddenWindows, On 179 | } 180 | 181 | PWHOpenMenu() 182 | { 183 | PWHOnThreadStart() 184 | 185 | ; If the user clicks the buttons for PWHOpenMenu and PWHQuickToggle at around the same time, the 186 | ; menu can open while the quick-toggle process is still running. This would cause the menu to be 187 | ; out of sync with the actual state of the plug-in windows, so we prevent it. 188 | if(pwhIsQuickToggleRunning) 189 | return 190 | 191 | ; Remember the time the menu was opened - see PWHOnGuiActiveMenuHotkeyUp. 192 | pwhOpenMenuTickCount := A_TickCount 193 | 194 | ; Get the process identifier(s) of whichever DAW instance happens to be active at the moment. 195 | PWHGetDawProcesses() 196 | 197 | ; Remember the active window so we can reactivate it when the menu is dismissed. 198 | WinGet, pwhOrigActiveWindow, ID, A 199 | 200 | ; Get the list of open plug-in windows owned by this DAW. We have to store this in a global so we 201 | ; can access it again from event handlers, such as when the user clicks a menu item. 202 | pwhPluginWindows := PWHGetPluginWindows() 203 | 204 | ; If there are no plug-in windows open, there's not much we can do. 205 | if(pwhPluginWindows.Length() = 0) 206 | { 207 | Msgbox, 0, Plug-In Window Helper, You need to open at least one plug-in window before using this feature. 208 | PWHReactivateDawWindow() 209 | return 210 | } 211 | 212 | ; Sort the list of plug-in windows by track name + plug-in name. 213 | PWHSort(pwhPluginWindows, Func("PWHCompareSortKey")) 214 | 215 | ; Build the menu GUI, display it, and activate it. 216 | PWHBuildGui() 217 | 218 | ; When the user does anything to activate another window besides our GUI, close the GUI. This 219 | ; happens, for example, when the user clicks in another application window, or hits the Start 220 | ; menu, or hits Alt+Tab, etc. In this scenario, we just need to destroy the GUI and not 221 | ; reactivate the DAW window, since the user's action has already made another window active and 222 | ; we don't want to mess with that. 223 | WinWaitNotActive, % "ahk_id " pwhGuiHwnd 224 | if(PWHIsGuiOpen()) 225 | PWHDestroy() 226 | } 227 | 228 | PWHQuickToggle() 229 | { 230 | PWHOnThreadStart() 231 | 232 | ; If the user clicks the buttons for PWHOpenMenu and PWHQuickToggle at around the same time, 233 | ; quick-toggle can be invoked while the menu GUI is still under construction. This would cause 234 | ; the menu to be out of sync with the actual state of the plug-in windows, so we prevent it. 235 | if(pwhIsBuildingGui) 236 | return 237 | 238 | pwhIsQuickToggleRunning := true 239 | 240 | ; We allow the user to invoke quick-toggle while the menu GUI is open (sometimes the user hits 241 | ; the menu hotkey when they meant to quick-toggle). In this case we just close the GUI and 242 | ; proceed as usual. 243 | if(PWHIsGuiOpen()) 244 | PWHDestroy() 245 | 246 | ; Get the process identifier(s) of whichever DAW instance happens to be active at the moment. 247 | PWHGetDawProcesses() 248 | 249 | ; Clear out any active window we may have remembered from the last menu invocation. We don't want 250 | ; to try and reactivate it in the context of quick-toggle. 251 | pwhOrigActiveWindow := 0 252 | 253 | ; Get the list of open plug-in windows owned by this DAW and see if any is visible. This 254 | ; determines whether we are hiding all the visible windows or re-showing them again. 255 | pluginWindows := PWHGetPluginWindows() 256 | anyVisible := false 257 | for i, window in pluginWindows 258 | { 259 | if(window.isVisible) 260 | { 261 | anyVisible := true 262 | break 263 | } 264 | } 265 | 266 | if(anyVisible) 267 | { 268 | ; There is at least one plug-in window visible, so our job is to remember which ones are 269 | ; visible and then hide them all... 270 | 271 | ; Store a copy of the plug-in window state for this DAW instance, so we can re-show the windows 272 | ; later. Since we support multiple DAW instances, we have to be able to store state for each 273 | ; DAW instance separately. Note that this state never gets cleaned up (until the script exits) 274 | ; but it's a small amount of data, and the user is probably not opening and closing the DAW 275 | ; all that frequently. 276 | pwhPluginWindowsRestore[pwhDawMainProcess] := PWHClone(pluginWindows) 277 | 278 | ; Hide all the visible windows and update the state to reflect that they are hidden. 279 | for i, window in pluginWindows 280 | { 281 | window.isVisible := false 282 | PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible) 283 | } 284 | 285 | ; Store the plug-in window state to reflect that all windows are hidden. Then if the DAW tries 286 | ; to auto-show any of these windows, we will be able to quickly hide them again. 287 | pwhPluginWindowsLastState[pwhDawMainProcess] := pluginWindows 288 | } 289 | else 290 | { 291 | ; There are no plug-in windows visible, so we need to find out which ones were last visible 292 | ; and, if possible, re-show them... 293 | 294 | ; If we have any stored state to restore 295 | if(pwhPluginWindowsRestore[pwhDawMainProcess].Length() > 0) 296 | { 297 | ; Find at least one stored plug-in window that was formerly visible AND still exists (the 298 | ; user could have closed the window since we stored the state). 299 | for i, window in pwhPluginWindowsRestore[pwhDawMainProcess] 300 | { 301 | if(window.isVisible and WinExist("ahk_id " window.hwnd)) 302 | { 303 | anyVisible := true 304 | break 305 | } 306 | } 307 | 308 | ; If we found one, we can safely use the stored state. (If we don't use the stored state, 309 | ; we will use the "live" pluginWindows state that we retrieved above, and that we already 310 | ; know does not contain any visible windows.) 311 | if(anyVisible) 312 | pluginWindows := pwhPluginWindowsRestore[pwhDawMainProcess] 313 | } 314 | 315 | ; If we get to this point and we have not found a formerly-visible window to re-show, then we 316 | ; want to show ALL of the hidden plug-in windows. (There isn't anything else we can reasonably 317 | ; do in this situation, and at least this shows SOMETHING.) Note that if there just aren't any 318 | ; windows at all, pluginWindows will be empty; see below where we display a message box. 319 | if(!anyVisible) 320 | { 321 | for i, window in pluginWindows 322 | window.isVisible := true 323 | } 324 | 325 | anyVisible := false 326 | 327 | if(pluginWindows.Length() > 0) 328 | { 329 | ; Before we re-show the windows, we have to update the stored state so that the code in 330 | ; PWHOnWindowShowTimer won't immediately re-hide them! 331 | pwhPluginWindowsLastState[pwhDawMainProcess] := pluginWindows 332 | 333 | ; Depending what part of the code last stored the list, it may be sorted by name. We need 334 | ; it sorted by z-order, back-to-front. This way when we re-show the windows, they will be 335 | ; in the same z-order as when they were hidden. 336 | PWHSort(pluginWindows, Func("PWHCompareZOrderDesc")) 337 | 338 | ; Now re-show the windows that were formerly visible and still exist. 339 | for i, window in pluginWindows 340 | { 341 | if(window.isVisible and WinExist("ahk_id " window.hwnd)) 342 | { 343 | PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible) 344 | anyVisible := true 345 | } 346 | } 347 | } 348 | 349 | ; We didn't find any plug-in windows to show, so we throw up our hands. 350 | if(!anyVisible) 351 | Msgbox, 0, Plug-In Window Helper, You need to open at least one plug-in window before using this feature. 352 | } 353 | 354 | ; Activate the DAW main window or its frontmost plug-in window. 355 | PWHReactivateDawWindow() 356 | 357 | pwhIsQuickToggleRunning := false 358 | } 359 | 360 | PWHOnGuiActiveMenuHotkeyUp() 361 | { 362 | PWHOnThreadStart() 363 | 364 | ; We open the menu when the user presses the menu hotkey, and we close it when they release the 365 | ; hotkey. If they move the mouse slightly while clicking, or if the menu opens so that the mouse 366 | ; is positioned over a menu command, the menu could disappear instantly. To avoid this, we 367 | ; remember the time the menu was opened and ignore any button release that happens very shortly 368 | ; afterwards. 369 | elapsed := A_TickCount - pwhOpenMenuTickCount 370 | if(elapsed < 500) 371 | return 372 | 373 | if(PWHIsMouseOverGui()) 374 | { 375 | SendEvent, {Blind}{LButton Up} 376 | } 377 | else 378 | { 379 | PWHDestroy() 380 | PWHReactivateDawWindow() 381 | } 382 | } 383 | 384 | PWHDestroy() 385 | { 386 | ; AutoHotkey freaks out if we destroy the GUI while we're still in the process of assembling it, 387 | ; so ignore any requests to destroy it while it's being built. (This can happen if several 388 | ; hotkeys are triggered at around the same time.) 389 | if(!pwhIsBuildingGui) 390 | { 391 | Gui, % pwhGuiHwnd ":Destroy" 392 | SetTimer, PWHOnHighlightTimer, Delete 393 | } 394 | } 395 | 396 | PWHIsGuiOpen() 397 | { 398 | return WinExist("ahk_id " pwhGuiHwnd) 399 | } 400 | 401 | PWHIsGuiActive() 402 | { 403 | return WinActive("ahk_id " pwhGuiHwnd) 404 | } 405 | 406 | PWHIsMouseOverGui() 407 | { 408 | MouseGetPos, , , mouseWin 409 | return (mouseWin = pwhGuiHwnd) 410 | } 411 | 412 | PWHIsDawWinActive() 413 | { 414 | ; Note that this function returns true if any of the windows belonging to the DAW process is 415 | ; active, including the main window, any popup window, or any plug-in window. 416 | WinGet, pname, ProcessName, A 417 | return RegExMatch(pname, "^(Ableton Live|Bitwig)") > 0 418 | } 419 | 420 | PWHIsDawWinOrGuiActive() 421 | { 422 | return PWHIsDawWinActive() or PWHIsGuiActive() 423 | } 424 | 425 | PWHGetDawProcesses() 426 | { 427 | ; Note that this function is always called from contexts where we already know that the DAW is 428 | ; the active application. 429 | 430 | ; Get the frontmost window that is either the DAW main window or one of its popups (but not 431 | ; a plug-in window - these may run in child processes). 432 | WinGet, dawMainWindow, ID, ahk_group pwhDawGroup 433 | 434 | ; Get the process ID from the window. This is the main process of the DAW, meaning it is not one 435 | ; of the plug-in-hosting child processes. 436 | WinGet, pwhDawMainProcess, PID, ahk_id %dawMainWindow% 437 | 438 | ; Remember which DAW this is - this affects the look and feel of our GUI. 439 | WinGet, pname, ProcessName, ahk_id %dawMainWindow% 440 | pwhIsBitwig := RegExMatch(pname, "^Bitwig") > 0 441 | 442 | ; Get the PID and parent PID of every running process belonging to a DAW. 443 | procs := [] 444 | for proc in ComObjGet("winmgmts:").ExecQuery("select Handle, ParentProcessId from Win32_Process where Name like 'Ableton Live%' or Name like 'Bitwig%'") 445 | procs.Push({ pid: proc.Handle, parentPid: proc.ParentProcessId }) 446 | 447 | ; Build a collection of PIDs containing the main DAW process and all of its descendant processes. 448 | ; This is keyed by PID so PWHGetPluginWindows can quickly decide if a window belongs to this DAW. 449 | pwhDawProcesses := { (pwhDawMainProcess): true } 450 | for i, pid in PWHGetDescendantProcesses(pwhDawMainProcess, procs) 451 | pwhDawProcesses[pid] := true 452 | } 453 | 454 | PWHGetDescendantProcesses(parentPid, procs) 455 | { 456 | pids := [] 457 | for i, proc in procs 458 | { 459 | if(proc.parentPid = parentPid) 460 | { 461 | pids.Push(proc.pid) 462 | pids.Push(PWHGetDescendantProcesses(proc.pid, procs)*) 463 | } 464 | } 465 | 466 | return pids 467 | } 468 | 469 | PWHGetPluginWindows() 470 | { 471 | pluginWindows := [] 472 | windowsByHwnd := {} 473 | 474 | ; Get every window in the system that is categorized as a DAW plug-in window (see pwhPluginGroup). 475 | WinGet, winList, List, ahk_group pwhPluginGroup 476 | Loop, %winList% 477 | { 478 | hwnd := winList%A_Index% 479 | 480 | ; We are only interested in popup windows belonging to the active DAW's process(es). 481 | WinGet, pid, PID, ahk_id %hwnd% 482 | if(!pwhDawProcesses[pid]) 483 | continue 484 | 485 | WinGetTitle, title, ahk_id %hwnd% 486 | WinGetClass, className, ahk_id %hwnd% 487 | 488 | ; The plug-in window title contains two important pieces of information: The track name and the 489 | ; plug-in name, separated by a forward slash. Since the slash is our only clue, we can handle 490 | ; slashes in the track name, but a slash in the plug-in name would mess us up. (So far we have 491 | ; not encountered a plug-in with a slash in its name.) 492 | 493 | titleParts := StrSplit(title, "/") 494 | 495 | ; In Ableton the plug-in name comes first, and in Bitwig it comes last. Once we have the plug-in 496 | ; name, we remove it so we can concatenate the remaining parts into the track name. 497 | pluginNameIdx := pwhIsBitwig ? titleParts.Length() : 1 498 | pluginName := Trim(titleParts[pluginNameIdx]) 499 | titleParts.RemoveAt(pluginNameIdx) 500 | 501 | ; Now whatever parts are left get concatenated into the track name. (If the track name 502 | ; contains no slashes, there will only be one part left.) 503 | trackName := "" 504 | for i, titlePart in titleParts 505 | { 506 | if(i > 1) 507 | trackName .= "/" 508 | trackName .= titlePart 509 | } 510 | trackName := Trim(trackName) 511 | 512 | ; Now we know all we need to about this plug-in window. (Note that we fill in ownedHwnds in the 513 | ; next step.) Add it to the array... 514 | 515 | window := { hwnd: hwnd 516 | , ownedHwnds: [] 517 | , trackName: trackName 518 | , pluginName: pluginName 519 | , className: className 520 | , zOrder: A_Index 521 | , sortKey: trackName "`t" pluginName 522 | , isVisible: PWHIsWinVisible(hwnd) } 523 | 524 | pluginWindows.Push(window) 525 | 526 | ; We use this map of hwnd to window to improve the performance of the next step. 527 | windowsByHwnd[hwnd] := window 528 | } 529 | 530 | ; Now fill in the ownedHwnds array for each plug-in window. Note that we only look one window 531 | ; deep; we have not yet encountered a plug-in that opens a popup that in turn opens another 532 | ; popup... 533 | 534 | ; Get every window in the system. 535 | WinGet, winList, List 536 | Loop, %winList% 537 | { 538 | hwnd := winList%A_Index% 539 | 540 | ; We are only interested in owned popup windows. 541 | ownerHwnd := DllCall("GetWindow", "Ptr", hwnd, "UInt", GW_OWNER, "Ptr") 542 | if(ownerHwnd) 543 | { 544 | ; See if this popup window's owner is one of the plug-in windows we retrieved in the 545 | ; previous step. 546 | window := windowsByHwnd[ownerHwnd] 547 | 548 | ; We need to ignore any IME windows created by the OS and linked to the active plug-in 549 | ; window without the DAW's knowledge. Only add this popup window to the plug-in window's 550 | ; ownedHwnds if it is not in the "ignore" group. 551 | if(window and !WinExist("ahk_id " hwnd " ahk_group pwhIgnoreGroup")) 552 | window.ownedHwnds.Push(hwnd) 553 | } 554 | } 555 | 556 | ; This array is ordered by z-order, front-to-back. 557 | return pluginWindows 558 | } 559 | 560 | PWHBuildGui() 561 | { 562 | ; We need to keep track of when we're building the GUI so we don't try to do anything else at the 563 | ; same time. 564 | pwhIsBuildingGui := true 565 | 566 | ; Bitwig uses different colors (see PWHGetThemeColors) as well as a different font and spacing. 567 | ; All of the differences are captured in themeColors and the following constants... 568 | 569 | themeColors := PWHGetThemeColors() 570 | 571 | if(pwhIsBitwig) 572 | { 573 | FONT_NAME := "Segoe UI" 574 | FONT_WEIGHT := "normal" 575 | FONT_SIZE := 10 576 | CHECK_MARK_FONT_SIZE := 13 577 | ITEM_SPACE_X := 14 578 | ITEM_SPACE_Y := 6 579 | SEP_SPACE_X := 6 580 | SEP_SPACE_Y := 7 581 | MARGIN_L := 10 582 | MARGIN_R := 20 583 | MARGIN_T := 6 584 | MARGIN_B := 5 585 | CHECK_MARK_X := 9 586 | CHECK_MARK_OFFSET_Y := 0 587 | ITEM_LABEL_X := 30 588 | SCALE_BORDERS := false 589 | } 590 | else 591 | { 592 | FONT_NAME := "Arial" 593 | FONT_WEIGHT := "bold" 594 | FONT_SIZE := 8 595 | CHECK_MARK_FONT_SIZE := 11 596 | ITEM_SPACE_X := 10 597 | ITEM_SPACE_Y := 4 598 | SEP_SPACE_X := 0 599 | SEP_SPACE_Y := 6 600 | MARGIN_L := 8 601 | MARGIN_R := 20 602 | MARGIN_T := 6 603 | MARGIN_B := 5 604 | CHECK_MARK_X := 6 605 | CHECK_MARK_OFFSET_Y := -2 606 | ITEM_LABEL_X := 22 607 | SCALE_BORDERS := true 608 | } 609 | 610 | ; We need to use this special combination of options to get controls that will paint properly to 611 | ; show the controls behind them. This is what enables the menu highlight on mouse hover. 612 | TRANSPARENT := "BackgroundTrans E" WS_EX_TRANSPARENT 613 | 614 | ; We don't want our menu to have a caption or be listed in the taskbar, and it must appear on top 615 | ; of all the DAW windows. The Label option establishes a naming convention for the GUI's callback 616 | ; functions. 617 | Gui, New, +HwndpwhGuiHwnd -Caption +ToolWindow +AlwaysOnTop +LabelPWHOnGui 618 | 619 | ; We determine our own left, right, and top margins, but we let AutoHotkey create a bottom margin 620 | ; after the last menu item. 621 | Gui, Margin, 0, %MARGIN_B% 622 | 623 | ; Set the font and background colors for the GUI. 624 | Gui, Font, % FONT_WEIGHT " s" FONT_SIZE " c" themeColors.textColor, %FONT_NAME% 625 | Gui, Color, % themeColors.backColor, % themeColors.backColor 626 | 627 | ; Add the caption for the list of plug-in windows. 628 | Gui, Add, Text, hwndhwnd x%MARGIN_L% y%MARGIN_T%, Plug-In Windows: 629 | 630 | ; We need to take the caption's width into account when we figure out how wide the menu should 631 | ; be. (We account for the rest of the menu items below.) 632 | GuiControlGet, textPos, Pos, %hwnd% 633 | menuWidth := textPosX + textPosW 634 | 635 | menuItems := [] 636 | maxX := 0 637 | 638 | ; Add a menu item for each plug-in window. Each menu item has three Text controls: Check mark, 639 | ; track name, and plug-in name. We need to use three separate Text controls to get the three 640 | ; columns aligned properly. 641 | for i, window in pwhPluginWindows 642 | { 643 | textHwnds := [] 644 | 645 | ; Check mark: Note that we use a glyph from the Webdings font. Apply a y-offset to get it to 646 | ; line up properly with the text. 647 | Gui, Font, % "s" CHECK_MARK_FONT_SIZE, Webdings 648 | Gui, Add, Text, % "hwndcheckMarkHwnd " TRANSPARENT " x" CHECK_MARK_X " y+" (ITEM_SPACE_Y + CHECK_MARK_OFFSET_Y) 649 | Gui, Font, % "s" FONT_SIZE, %FONT_NAME% 650 | textHwnds.Push(checkMarkHwnd) 651 | 652 | ; Track name: Note that we have to undo the check mark's y-offset. SS_NOPREFIX turns off 653 | ; special handling of & characters in the track name. 654 | Gui, Add, Text, % "hwndhwnd x" ITEM_LABEL_X " yp+" -CHECK_MARK_OFFSET_Y " " TRANSPARENT " " SS_NOPREFIX, % window.trackName 655 | textHwnds.Push(hwnd) 656 | 657 | ; Plug-in name: Leave a bit of horizontal space between the track name and plug-in name, but at 658 | ; this stage, the plug-in names do not yet line up vertically. 659 | Gui, Add, Text, % "hwndhwnd x+" ITEM_SPACE_X " yp " TRANSPARENT " " SS_NOPREFIX, % window.pluginName 660 | textHwnds.Push(hwnd) 661 | 662 | ; Accumulate the x-position of the rightmost plug-in name control. We will line up all of the 663 | ; plug-in names at this position. 664 | GuiControlGet, textPos, Pos, %hwnd% 665 | maxX := Max(maxX, textPosX) 666 | 667 | ; Keep some information about each menu item for use in the next steps. Note that the command 668 | ; ID of a plug-in window menu item is just its ordinal. 669 | menuItems.Push({ cmdID: i, checkMarkHwnd: checkMarkHwnd, textHwnds: textHwnds }) 670 | } 671 | 672 | ; Now walk through the menu items and line up the plug-in names so they appear in a column. 673 | ; The plug-in name is in the last Text control in each menu item's textHwnds array. 674 | for i, menuItem in menuItems 675 | GuiControl, Move, % menuItem.textHwnds[menuItem.textHwnds.Length()], % "x" maxX 676 | 677 | ; Calculate the menu width as the right edge of the widest menu item, plus a margin. Note that we 678 | ; don't need to account for the Hide All and Show All items because they are narrower than the 679 | ; "Plug-In Windows:" caption. 680 | for i, menuItem in menuItems 681 | { 682 | GuiControlGet, textPos, Pos, % menuItem.textHwnds[menuItem.textHwnds.Length()] 683 | menuWidth := Max(menuWidth, textPosX + textPosW) 684 | } 685 | menuWidth += MARGIN_R 686 | 687 | ; Add a horizontal separator line. 688 | PWHAddGuiColorBlock(SEP_SPACE_X, "+" SEP_SPACE_Y, menuWidth - (SEP_SPACE_X * 2), 1, themeColors.separatorColor) 689 | 690 | ; Add the Hide All and Show All items. Note that they have command IDs that will not conflict 691 | ; with any of the plug-in window menu items. They also do not have check marks... 692 | 693 | Gui, Add, Text, hwndhwnd x%ITEM_LABEL_X% y+%SEP_SPACE_Y% %TRANSPARENT%, Hide All 694 | menuItems.Push({ cmdID: PWH_CMDID_HIDE_ALL, textHwnds: [hwnd] }) 695 | 696 | Gui, Add, Text, hwndhwnd x%ITEM_LABEL_X% y+%ITEM_SPACE_Y% %TRANSPARENT%, Show All 697 | menuItems.Push({ cmdID: PWH_CMDID_SHOW_ALL, textHwnds: [hwnd] }) 698 | 699 | pwhMenuItemControls := [] 700 | 701 | ; Now that we've added all the menu items, create a clickable control for each one. We can't just 702 | ; make the existing text controls clickable because there is space around and between the 703 | ; controls, and the user can click in the spaces. 704 | for i, menuItem in menuItems 705 | { 706 | ; We want to position the clickable control directly on top of the menu item, so we need the 707 | ; y-position and height of the item. 708 | GuiControlGet, textPos, Pos, % menuItem.textHwnds[menuItem.textHwnds.Length()] 709 | y := textPosY - (ITEM_SPACE_Y / 2) 710 | h := textPosH + ITEM_SPACE_Y 711 | 712 | ; Add the clickable control so it spans the entire width of the menu. Note that it must have a 713 | ; click handler (even one that does nothing) so it will get detected when we call MouseGetPos. 714 | Gui, Add, Text, % "hwndclickableHwnd gPWHOnMenuItemClick x0 y" y " w" menuWidth " h" h " " TRANSPARENT 715 | 716 | ; We keep a global array of menu item controls so we can use this info when handling clicks 717 | ; and highlighting menu items on mouse hover. 718 | pwhMenuItemControls.Push({ cmdID: menuItem.cmdID, clickableHwnd: clickableHwnd, checkMarkHwnd: menuItem.checkMarkHwnd, textHwnds: menuItem.textHwnds }) 719 | } 720 | 721 | ; We highlight a menu item on mouse hover by positioning a colored block behind the highlighted 722 | ; item. For now, just create the colored block and hide it. It gets shown and positioned in 723 | ; PWHOnHighlightTimer. 724 | pwhHighlightHwnd := PWHAddGuiColorBlock(0, 0, 1, 1, themeColors.highlightBackColor) 725 | GuiControl, Hide, %pwhHighlightHwnd% 726 | 727 | ; Show the GUI offscreen, so we can measure it and position it properly - we don't want it to 728 | ; visibly "jump". PWHPositionGuiOnScreen will move it back on screen to its final position. 729 | Gui, Show, x-999999 y-999999 730 | 731 | ; Update the check marks to reflect the visible/hidden status of each plug-in window. 732 | PWHUpdateCheckMarks() 733 | 734 | ; Add borders around the edges of the menu. 735 | PWHAddGuiBorders(themeColors, SCALE_BORDERS) 736 | 737 | ; Now that everything is finally set, we can move the menu to a position on-screen relative to 738 | ; the mouse pointer. 739 | PWHPositionGuiOnScreen() 740 | 741 | ; The simplest way to update the mouse hover menu highlight is to use a timer, because that way 742 | ; we can detect when the mouse moves outside the GUI window and clear the highlight. (Getting 743 | ; mouse messages from outside our window is possible but a lot more complicated.) Luckily, the 744 | ; CPU cost of doing this on a timer (even for large menus) is imperceptible. 745 | SetTimer, PWHOnHighlightTimer, 50 746 | 747 | pwhIsBuildingGui := false 748 | } 749 | 750 | PWHOnGuiEscape() 751 | { 752 | ; Close the menu when the user hits Escape. 753 | PWHOnThreadStart() 754 | PWHDestroy() 755 | PWHReactivateDawWindow() 756 | } 757 | 758 | PWHOnMenuItemClick() 759 | { 760 | ; We need this do-nothing click handler so the control will get detected when we call 761 | ; MouseGetPos. The actual click handling is done in PWHOnGuiButtonUp. 762 | } 763 | 764 | PWHAddGuiBorders(themeColors, scaleBorders) 765 | { 766 | ; Because of DPI scaling, we can't position the borders with AutoHotkey's Gui commands; depending 767 | ; on the scale factor there would be rounding errors and the borders wouldn't line up exactly. We 768 | ; have to use WinGetPos and ControlMove, which operate on actual, unscaled pixels... 769 | 770 | ; First add four color blocks to the GUI, one for each border (top, bottom, left, right). They 771 | ; are initially 1x1 in scaled units. 772 | borderHwnds := [] 773 | Loop, 4 774 | borderHwnds.Push(PWHAddGuiColorBlock(0, 0, 1, 1, themeColors.borderColor)) 775 | 776 | ; Get the size of the GUI window in pixels. 777 | WinGetPos, , , guiWidth, guiHeight, ahk_id %pwhGuiHwnd% 778 | 779 | ; We want borders that either scale in thickness according to the DPI scale factor (Ableton), or 780 | ; are always exactly one pixel thick (Bitwig). If we want them to scale, we measure the width and 781 | ; height (in pixels) of one of the 1x1 color blocks we created above. 782 | if(scaleBorders) 783 | ControlGetPos, , , borderWidth, borderHeight, , % "ahk_id " borderHwnds[1] 784 | else 785 | borderWidth := borderHeight := 1 786 | 787 | ; Now we can position each border in unscaled pixel units. 788 | ControlMove, , 0, 0, guiWidth, borderHeight, % "ahk_id " borderHwnds[1] ; top 789 | ControlMove, , 0, 0, borderWidth, guiHeight, % "ahk_id " borderHwnds[2] ; left 790 | ControlMove, , 0, guiHeight - borderHeight, guiWidth, borderHeight, % "ahk_id " borderHwnds[3] ; bottom 791 | ControlMove, , guiWidth - borderWidth, 0, borderWidth, guiHeight, % "ahk_id " borderHwnds[4] ; right 792 | 793 | ; Remember the border width; we will need it when we position the menu item highlight. 794 | pwhBorderWidth := borderWidth 795 | } 796 | 797 | PWHAddGuiColorBlock(x, y, w, h, color) 798 | { 799 | ; In the AutoHotkey community, the generally accepted method for creating a solid block of an 800 | ; arbitrary color is to use a Progress control with the desired background color. Note that we 801 | ; have to turn off the WS_EX_STATICEDGE style to get rid of the border. 802 | Gui, Add, Progress, % "hwndhwnd x" x " y" y " w" w " h" h " Background" color 803 | Control, ExStyle, % -WS_EX_STATICEDGE, , % "ahk_id " hwnd 804 | return hwnd 805 | } 806 | 807 | PWHPositionGuiOnScreen() 808 | { 809 | ; Note that before this function is called, we have the GUI positioned so it is "hidden" offscreen. 810 | 811 | ; Get the mouse position in screen coords. 812 | CoordMode, Mouse, Screen 813 | MouseGetPos, x, y 814 | 815 | ; Get the working area (excluding the taskbar) of the monitor the mouse is on. 816 | hMonitor := PWHGetMouseMonitor() 817 | monInfo := PWHGetMonitorInfo(hMonitor) 818 | 819 | ; Get the dimensions of the GUI window. 820 | WinGetPos, , , width, height, ahk_id %pwhGuiHwnd% 821 | 822 | ; Move the GUI to its final position: Aligned to the mouse pointer, but not extending outside the 823 | ; screen bounds. 824 | x := PWHConstrainCoord(x, width, monInfo.workLeft, monInfo.workRight) 825 | y := PWHConstrainCoord(y, height, monInfo.workTop, monInfo.workBottom) 826 | Gui, Show, x%x% y%y% 827 | } 828 | 829 | PWHConstrainCoord(coord, extent, screenMin, screenMax) 830 | { 831 | ; Normally we display the GUI so its top left is at the mouse position. However, depending on the 832 | ; mouse location and GUI size, the GUI could extend off the edge of the screen. To avoid this, we 833 | ; first try to flip the GUI to the opposite side of the mouse pointer, and if it won't fit there 834 | ; either, we shift it the minimum distance needed to get it entirely on-screen. Note that we do 835 | ; NOT handle the case where the GUI is actually wider or taller than the screen - to handle this 836 | ; would require a scrollable GUI. 837 | 838 | if(coord + extent > screenMax) 839 | { 840 | if(coord - extent >= screenMin) 841 | coord -= extent 842 | else 843 | coord -= (coord + extent) - screenMax 844 | } 845 | 846 | return coord 847 | } 848 | 849 | PWHOnGuiButtonUp(wParam, lParam, msg, hwnd) 850 | { 851 | ; AutoHotkey's built-in GUI control click handling fires on button-down, but we want our menu 852 | ; items to trigger on button-up, so we have to handle the button-up messages ourselves... 853 | 854 | PWHOnThreadStart() 855 | 856 | ; Get the command ID for the menu item the mouse pointer was over when the button was released. 857 | cmdID := 0 858 | for i, menuItemCtl in pwhMenuItemControls 859 | { 860 | if(menuItemCtl.clickableHwnd = hwnd) 861 | { 862 | cmdID := menuItemCtl.cmdID 863 | break 864 | } 865 | } 866 | 867 | ; A real cmdID is never zero, so this means the mouse was not over any menu item; we're done. 868 | if(cmdID = 0) 869 | return 870 | 871 | ; When the user clicks with the right mouse button or control-clicks with any button, we toggle 872 | ; the visibility of the selected plug-in window, instead of showing that plug-in window and hiding 873 | ; all others. We also keep the menu open to allow the user to toggle more windows. Note that 874 | ; toggle mode doesn't change the behavior of Hide All and Show All; it just keeps the menu open. 875 | isToggle := (msg = WM_RBUTTONUP) or GetKeyState("Control", "P") 876 | 877 | ; First, update the isVisible flag for each entry in the pwhPluginWindows array... 878 | 879 | if(cmdID = PWH_CMDID_HIDE_ALL or cmdID = PWH_CMDID_SHOW_ALL) 880 | { 881 | for i, window in pwhPluginWindows 882 | window.isVisible := (cmdID = PWH_CMDID_SHOW_ALL) 883 | } 884 | else if(isToggle) 885 | { 886 | window := pwhPluginWindows[cmdID] 887 | window.isVisible := !window.isVisible 888 | } 889 | else 890 | { 891 | for i, window in pwhPluginWindows 892 | window.isVisible := (i = cmdID) 893 | } 894 | 895 | ; If we're in toggle mode, the menu will stay open, so we need to update the check mark for each 896 | ; plug-in window menu item. 897 | if(isToggle) 898 | PWHUpdateCheckMarks() 899 | 900 | ; Update the stored plug-in window state with a copy of the new visible/hidden state. Then if the 901 | ; DAW tries to auto-show any of the hidden windows later, we will be able to quickly hide them again. 902 | pwhPluginWindowsLastState[pwhDawMainProcess] := PWHClone(pwhPluginWindows) 903 | 904 | ; When we show a plug-in window, we also bring it to the front of the z-order and activate it. 905 | ; Sort the array by z-order, back to front, so the windows that we are about to show will 906 | ; preserve their existing z-order. This is most noticeable when you do Hide All followed by Show 907 | ; All; the windows get re-shown in the same z-order they were in before. (Note that hiding a 908 | ; window does not affect the z-order.) 909 | PWHSort(pwhPluginWindows, Func("PWHCompareZOrderDesc")) 910 | 911 | ; Now actually show or hide each window according to its isVisible flag... 912 | 913 | for i, window in pwhPluginWindows 914 | { 915 | ; Note that this function does nothing if the window's visibility already matches the 916 | ; isVisible flag. changedVisible tells us whether anything actually happened. 917 | changedVisible := PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible) 918 | 919 | ; There's a weird behavior where, if we're in toggle mode (i.e. the menu stays open) and we 920 | ; show a hidden plug-in window, then when the user next dismisses the GUI, one of the visible 921 | ; plug-in windows further back in the z-order will swap places with another plug-in window. 922 | ; Activating the GUI window at this point makes this weird behavior go away. Unfortunately it 923 | ; also slows down the Show All command a bit in toggle mode. (We really shouldn't need to do 924 | ; this because we are about to activate the GUI window at the end of this function, but go 925 | ; figure.) 926 | if(isToggle and window.isVisible and changedVisible) 927 | WinActivate, ahk_id %pwhGuiHwnd% 928 | } 929 | 930 | ; Refresh the array of plug-in windows that drives the GUI. Since we've probably just messed with 931 | ; the z-order, we want the z-order stored in the array to be correct. Note that the array needs 932 | ; to be sorted to match the menu order. 933 | pwhPluginWindows := PWHGetPluginWindows() 934 | PWHSort(pwhPluginWindows, Func("PWHCompareSortKey")) 935 | 936 | ; If we just ran any command EXCEPT Hide All, update the plug-in window state that supports the 937 | ; quick-toggle feature. We don't update it on Hide All because then quick-toggle would be useless 938 | ; immediately after Hide All (it would always show the message "You need to show at least one 939 | ; plug-in window before using quick-toggle"). This way if you pick Hide All by mistake, you can 940 | ; immediately undo it with quick-toggle. 941 | if(cmdID != PWH_CMDID_HIDE_ALL) 942 | pwhPluginWindowsRestore[pwhDawMainProcess] := PWHClone(pwhPluginWindows) 943 | 944 | if(isToggle) 945 | { 946 | ; Re-activate the GUI window, because otherwise the WinWaitNotActive at the end of PWHOpenMenu 947 | ; will get triggered and the GUI will close - not what we want! 948 | WinActivate, ahk_id %pwhGuiHwnd% 949 | } 950 | else 951 | { 952 | PWHDestroy() 953 | PWHReactivateDawWindow() 954 | } 955 | } 956 | 957 | PWHOnHighlightTimer() 958 | { 959 | ; We highlight a menu item on mouse hover by positioning a colored block behind the highlighted 960 | ; item. The simplest way to update the menu highlight is to use a timer, because that way we can 961 | ; detect when the mouse moves outside the GUI window and clear the highlight. (Getting mouse 962 | ; messages from outside our window is possible but a lot more complicated.) Luckily, the CPU cost 963 | ; of doing this on a timer (even for large menus) is imperceptible... 964 | 965 | PWHOnThreadStart() 966 | 967 | themeColors := PWHGetThemeColors() 968 | 969 | ; Find out which control (if any) the mouse pointer is over. 970 | MouseGetPos, , , , mouseCtl, 3 971 | 972 | highlightedMenuItemCtl := 0 973 | 974 | ; For each menu item... 975 | for i, menuItemCtl in pwhMenuItemControls 976 | { 977 | ; This menu item is highlighted if the mouse is over its clickable control. 978 | isHighlight := (mouseCtl = menuItemCtl.clickableHwnd) 979 | 980 | ; We will need to know which item (if any) is highlighted later. 981 | if(isHighlight) 982 | highlightedMenuItemCtl := menuItemCtl 983 | 984 | ; Set the text color (highlighted/not) of each text control belonging to this menu item. 985 | for j, textHwnd in menuItemCtl.textHwnds 986 | GuiControl, % "+c" (isHighlight ? themeColors.highlightTextColor : themeColors.textColor), % textHwnd 987 | } 988 | 989 | if(highlightedMenuItemCtl) 990 | { 991 | ; Get the width of the GUI window and the y-position and height of this menu item (by way of 992 | ; its clickable control). 993 | WinGetPos, , , guiW, , ahk_id %pwhGuiHwnd% 994 | ControlGetPos, , clickableY, , clickableH, , % "ahk_id " highlightedMenuItemCtl.clickableHwnd 995 | 996 | ; Calculate the new position for the color block used to highlight the menu item. 997 | x := pwhBorderWidth 998 | y := clickableY 999 | w := guiW - (pwhBorderWidth * 2) 1000 | h := clickableH 1001 | 1002 | ; Get the current position of the color block. 1003 | ControlGetPos, highlightX, highlightY, highlightW, highlightH, , % "ahk_id " pwhHighlightHwnd 1004 | 1005 | ; We don't want to do anything if the highlight color block is already visible and in the 1006 | ; right position, because this would cause flickering. 1007 | if(!PWHIsControlVisible(pwhHighlightHwnd) or (highlightX != x) or (highlightY != y) or (highlightW != w) or (highlightH != h)) 1008 | { 1009 | ; Show and position the color block, sending it to the bottom of the z-order (it has to be 1010 | ; behind all of the text controls that make up the menu item). 1011 | DllCall("SetWindowPos", "UInt", pwhHighlightHwnd, "Int", HWND_BOTTOM, "Int", x, "Int", y, "Int", w, "Int", h, "UInt", SWP_SHOWWINDOW) 1012 | 1013 | ; Force a repaint of the section of the GUI window now occupied by the color block. When we 1014 | ; move the color block, windows automatically repaints the former position but not the new 1015 | ; position - weird. 1016 | PWHInvalidateRect(pwhGuiHwnd, x, y, w, h) 1017 | } 1018 | } 1019 | else 1020 | { 1021 | ; No item is highlighted, so we can just hide the color block. 1022 | GuiControl, Hide, %pwhHighlightHwnd% 1023 | } 1024 | } 1025 | 1026 | PWHUpdateCheckMarks() 1027 | { 1028 | ; Update the text of each menu item's check mark control to either contain a check mark (glyph 1029 | ; 0x61 in Webdings) or blank, matching the plug-in window's isVisible flag. Note that not every 1030 | ; menu item has a check mark control (Hide All, Show All). 1031 | for i, menuItemCtl in pwhMenuItemControls 1032 | { 1033 | if(menuItemCtl.checkMarkHwnd) 1034 | GuiControl, , % menuItemCtl.checkMarkHwnd, % pwhPluginWindows[menuItemCtl.cmdID].isVisible ? Chr(0x61) : "" 1035 | } 1036 | } 1037 | 1038 | PWHGetThemeColors() 1039 | { 1040 | ; Note that Bitwig currently does not allow customization of its UI colors. 1041 | if(pwhIsBitwig) 1042 | return { backColor: "353535", textColor: "ffffff", highlightBackColor: "c4c4c4", highlightTextColor: "353535", borderColor: "272727", separatorColor: "8c8c8c" } 1043 | 1044 | switch pwhAbletonTheme 1045 | { 1046 | case "Light": return { backColor: "ffffff", textColor: "000000", highlightBackColor: "79c3ec", highlightTextColor: "000000", borderColor: "000000", separatorColor: "000000" } 1047 | case "Mid Light", default: return { backColor: "dcdcdc", textColor: "000000", highlightBackColor: "bfe9ff", highlightTextColor: "000000", borderColor: "000000", separatorColor: "000000" } 1048 | case "Mid Dark": return { backColor: "333333", textColor: "e0e0e0", highlightBackColor: "8ccad8", highlightTextColor: "141414", borderColor: "e0e0e0", separatorColor: "e0e0e0" } 1049 | case "Dark": return { backColor: "1e1e1e", textColor: "dcdcdc", highlightBackColor: "8ccad8", highlightTextColor: "000000", borderColor: "dcdcdc", separatorColor: "dcdcdc" } 1050 | } 1051 | } 1052 | 1053 | PWHReactivateDawWindow() 1054 | { 1055 | if(PWHIsWinVisible(pwhOrigActiveWindow)) 1056 | { 1057 | ; Our preference is to reactivate the window that was originally active before we displayed 1058 | ; our GUI menu. 1059 | WinActivate, % "ahk_id " pwhOrigActiveWindow 1060 | } 1061 | else 1062 | { 1063 | ; However, if the user just hid the window that was active, we have to settle for the next 1064 | ; best thing. First, find the frontmost visible plug-in window and activate that. Note that the 1065 | ; array returned by PWHGetPluginWindows is in front-to-back order... 1066 | 1067 | pluginWindows := PWHGetPluginWindows() 1068 | 1069 | anyVisible := false 1070 | for i, window in pluginWindows 1071 | { 1072 | if(window.isVisible) 1073 | { 1074 | WinActivate, % "ahk_id " window.hwnd 1075 | anyVisible := true 1076 | break 1077 | } 1078 | } 1079 | 1080 | ; If there is no visible plug-in window, fall back to activating the frontmost window belonging 1081 | ; to the DAW. 1082 | if(!anyVisible) 1083 | WinActivate, ahk_group pwhDawGroup 1084 | } 1085 | } 1086 | 1087 | PWHShowHidePluginWindow(hwnd, ownedHwnds, showIt) 1088 | { 1089 | changedVisible := false 1090 | 1091 | isVisible := PWHIsWinVisible(hwnd) 1092 | 1093 | if(showIt and !isVisible) 1094 | { 1095 | WinShow, ahk_id %hwnd% 1096 | changedVisible := true 1097 | 1098 | ; Whenever we activate a plug-in window, it becomes the window that should be reactivated when 1099 | ; our GUI menu is dismissed (overriding any previously-stored window). However, we only want 1100 | ; to do this for plug-in windows, not their owned popup windows. The ownedHwnds parameter is 1101 | ; used to make this distinction (see the recursive call to PWHShowHidePluginWindow below). 1102 | if(ownedHwnds) 1103 | pwhOrigActiveWindow := hwnd 1104 | } 1105 | else if(!showIt and isVisible) 1106 | { 1107 | WinHide, ahk_id %hwnd% 1108 | changedVisible := true 1109 | } 1110 | 1111 | ; If this plug-in window owns any popup windows, show/hide them to match the visibility of the 1112 | ; plug-in window. Note that we only go one window deep, and we don't update the changedVisible 1113 | ; flag (it only applies to the plug-in window itself). 1114 | for i, ownedHwnd in ownedHwnds 1115 | PWHShowHidePluginWindow(ownedHwnd, 0, showIt) 1116 | 1117 | ; The caller needs to know whether we actually changed the visibility of the window. 1118 | return changedVisible 1119 | } 1120 | 1121 | PWHOnWindowShowEvent(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) 1122 | { 1123 | ; Bitwig hides all plug-in windows when the user invokes a modal UI such as the pop-up browser, 1124 | ; and then shows all of them again when the user closes the modal UI. This is a problem if the 1125 | ; user has used PWH to hide some of the plug-in windows - they will reappear every time a modal UI 1126 | ; is closed! We work around this by registering a windows event hook for the "object show" event, 1127 | ; so we will be notified every time any window gets shown. We can then see if the shown window is 1128 | ; one that we want hidden, and immediately re-hide it. This is fast enough that you usually don't 1129 | ; see the window at all, while sometimes you see a momentary flicker, depending on the number of 1130 | ; windows that need to be hidden. 1131 | 1132 | ; (This whole thing can be removed if Bitwig ever fixes their logic so that they don't show a 1133 | ; window unless they hid it in the first place.) 1134 | 1135 | ; To ensure that we don't miss any events, this event handler must execute in as short a time as 1136 | ; possible. Therefore, we just add the window handle to a collection and set a one-shot timer to 1137 | ; process it later. (If there is already a pending timer, this has no effect.) The critical 1138 | ; section prevents the event handler and timer function from modifying the global variable at the 1139 | ; same time. Note that the collection is keyed by window handle so we can do fast lookups... 1140 | 1141 | Critical, On 1142 | pwhWindowShowEvents[hwnd] := true 1143 | Critical, Off 1144 | 1145 | SetTimer, PWHOnWindowShowTimer, -1 1146 | } 1147 | 1148 | PWHOnWindowShowTimer() 1149 | { 1150 | PWHOnThreadStart() 1151 | 1152 | ; To ensure that each window recorded by PWHOnWindowShowEvent is processed exactly once, we clone 1153 | ; and clear the global collection. The critical section prevents the event handler and timer 1154 | ; function from modifying the global variable at the same time. 1155 | Critical, On 1156 | shownHwnds := pwhWindowShowEvents.Clone() 1157 | pwhWindowShowEvents := {} 1158 | Critical, Off 1159 | 1160 | ; We get notified any time ANY window is shown, across the entire system. We don't want to waste 1161 | ; any CPU time processing show events when Bitwig is not the active application. 1162 | WinGet, pname, ProcessName, A 1163 | if(RegExMatch(pname, "^Bitwig") = 0) 1164 | return 1165 | 1166 | for dawMainWindow, pluginWindows in pwhPluginWindowsLastState 1167 | { 1168 | for i, window in pluginWindows 1169 | { 1170 | ; This window should be hidden but just got shown - hide it again. 1171 | if(!window.isVisible and shownHwnds[window.hwnd]) 1172 | PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible) 1173 | } 1174 | } 1175 | } 1176 | 1177 | PWHIsWinVisible(hwnd) 1178 | { 1179 | WinGet, style, Style, ahk_id %hwnd% 1180 | return style and (style & WS_VISIBLE) != 0 1181 | } 1182 | 1183 | PWHIsControlVisible(hwnd) 1184 | { 1185 | ControlGet, style, Style, , , ahk_id %hwnd% 1186 | return style and (style & WS_VISIBLE) != 0 1187 | } 1188 | 1189 | PWHInvalidateRect(hwnd, x, y, w, h, erase := 1) 1190 | { 1191 | VarSetCapacity(rect, 16) 1192 | NumPut(x, rect, 0) 1193 | NumPut(y, rect, 4) 1194 | NumPut(x + w, rect, 8) 1195 | NumPut(y + h, rect, 12) 1196 | DllCall("InvalidateRect", "Ptr", hwnd, "Ptr", &rect, "Int", erase) 1197 | } 1198 | 1199 | PWHGetMouseMonitor() 1200 | { 1201 | CoordMode, Mouse, Screen 1202 | MouseGetPos, mouseX, mouseY 1203 | 1204 | return DllCall("MonitorFromPoint", "Int", mouseX, "Int", mouseY, "UInt", MONITOR_DEFAULTTONEAREST) 1205 | } 1206 | 1207 | PWHGetMonitorInfo(hMonitor) 1208 | { 1209 | ; This code is based on MDMF_GetInfo by "just me". 1210 | ; https://www.autohotkey.com/boards/viewtopic.php?t=4606 1211 | 1212 | NumPut(VarSetCapacity(mi, 40), mi, 0, "UInt") 1213 | DllCall("GetMonitorInfo", "Ptr", hMonitor, "Ptr", &mi) 1214 | 1215 | return { left: NumGet(mi, 4, "Int") 1216 | , top: NumGet(mi, 8, "Int") 1217 | , right: NumGet(mi, 12, "Int") 1218 | , bottom: NumGet(mi, 16, "Int") 1219 | , workLeft: NumGet(mi, 20, "Int") 1220 | , workTop: NumGet(mi, 24, "Int") 1221 | , workRight: NumGet(mi, 28, "Int") 1222 | , workBottom: NumGet(mi, 32, "Int") 1223 | , isPrimary: NumGet(mi, 36, "UInt") } 1224 | } 1225 | 1226 | PWHClone(obj) 1227 | { 1228 | ; This code is based on ObjFullyClone by "SpeedMaster". 1229 | ; https://www.autohotkey.com/boards/viewtopic.php?p=283227 1230 | 1231 | copy := obj.Clone() 1232 | 1233 | for k, v in copy 1234 | { 1235 | if(IsObject(v)) 1236 | copy[k] := PWHClone(v) 1237 | } 1238 | 1239 | return copy 1240 | } 1241 | 1242 | PWHSort(arr, compareFn := 0) 1243 | { 1244 | ; Insertion sort is simple to implement and its performance characteristics make it a good choice 1245 | ; for this use case. See https://en.wikipedia.org/wiki/Insertion_sort#Algorithm 1246 | 1247 | ; compareFn must be a reference to a function that returns true if a < b. 1248 | ; If no compareFn is passed in, the array entries are compared via the built-in < operator. 1249 | 1250 | i := 2 1251 | while i <= arr.Length() 1252 | { 1253 | x := arr[i] 1254 | j := i - 1 1255 | while j >= 1 and (compareFn ? %compareFn%(x, arr[j]) : x < arr[j]) 1256 | { 1257 | arr[j + 1] := arr[j] 1258 | j-- 1259 | } 1260 | arr[j + 1] := x 1261 | i++ 1262 | } 1263 | } 1264 | 1265 | PWHCompareSortKey(a, b) 1266 | { 1267 | ; Ascending sort by sortKey, using hwnd as a tie-breaker. (We only need a tie-breaker in the rare 1268 | ; case where two tracks have the same name and plug-in, and in that case there is no "right" 1269 | ; order; we only need their order to be stable.) We use StrCmpLogicalW so that track names that 1270 | ; start with a number (as in Ableton) will sort numerically. 1271 | strCmp := DllCall("shlwapi\StrCmpLogicalW", "Str", a.sortKey, "Str", b.sortKey, "Int") 1272 | return (strCmp = 0) ? (a.hwnd < b.hwnd) : strCmp < 0 1273 | } 1274 | 1275 | PWHCompareZOrderDesc(a, b) 1276 | { 1277 | ; Descending sort by z-order. 1278 | return a.zOrder >= b.zOrder 1279 | } 1280 | -------------------------------------------------------------------------------- /readme/menu-multi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RandScullard/plugin-window-helper/0e94652bb9c42db500960f5e8861618f320f64e0/readme/menu-multi.gif -------------------------------------------------------------------------------- /readme/menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RandScullard/plugin-window-helper/0e94652bb9c42db500960f5e8861618f320f64e0/readme/menu.gif -------------------------------------------------------------------------------- /readme/mouse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 41 | 43 | 52 | 56 | 60 | 64 | 68 | 72 | 73 | 82 | 86 | 90 | 94 | 98 | 102 | 103 | 111 | 116 | 122 | 127 | 132 | 138 | 139 | 140 | 145 | 154 | 159 | Menu 170 | 175 | Quick-Toggle 186 | 187 | 188 | --------------------------------------------------------------------------------