; plugin-window-helper.ahk
; version 1.0.0

; Copyright (c) 2021 Rand Scullard

; Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
; associated documentation files (the "Software"), to deal in the Software without restriction,
; including without limitation the rights to use, copy, modify, merge, publish, distribute,
; sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:

; The above copyright notice and this permission notice shall be included in all copies or
; substantial portions of the Software.

; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
; NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
; DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
; OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#Requires AutoHotkey v1.1.33+


; -----------------------------------------------------------------------------------------------
; See https://github.com/AfterLemon/Class_Console
; Uncomment the following to create a log window to use for debugging:

; #Include Class_Console\Class_Console.ahk
; global pwhConsole
; Class_Console("pwhConsole",0,0,800,1100)
; pwhConsole.show()
; -----------------------------------------------------------------------------------------------


; Windows constants:
global EVENT_OBJECT_SHOW := 0x8002
global GW_OWNER := 4
global HWND_BOTTOM := 1
global MONITOR_DEFAULTTONEAREST := 2
global SS_NOPREFIX := 0x80
global SWP_SHOWWINDOW := 0x40
global WINEVENT_OUTOFCONTEXT := 0x0000
global WINEVENT_SKIPOWNPROCESS := 0x0002
global WM_LBUTTONUP := 0x0202
global WM_MBUTTONUP := 0x0208
global WM_RBUTTONUP := 0x0205
global WS_EX_STATICEDGE := 0x20000
global WS_EX_TRANSPARENT := 0x20
global WS_VISIBLE := 0x10000000

; Menu command IDs besides the ones associated with plug-in windows. Note that they are negative to
; avoid conflicting with the positive command IDs for plug-in windows:
global PWH_CMDID_HIDE_ALL := -1
global PWH_CMDID_SHOW_ALL := -2

global pwhAbletonTheme
global pwhOpenMenuTickCount
global pwhIsQuickToggleRunning
global pwhIsBuildingGui
global pwhGuiHwnd
global pwhDawMainProcess
global pwhDawProcesses
global pwhIsBitwig
global pwhOrigActiveWindow
global pwhPluginWindows := []
global pwhPluginWindowsLastState := {}
global pwhPluginWindowsRestore := {}
global pwhWindowShowEvents := {}
global pwhHighlightHwnd
global pwhMenuItemControls
global pwhBorderWidth


; -----------------------------------------------------------------------------------------------
; If this script is running standalone, we call PWHInit here in the auto-execute section.
; When this script is included in another script, THAT script is responsible for calling PWHInit.
if(!A_IsCompiled and A_LineFile = A_ScriptFullPath)
   PWHInit()
; -----------------------------------------------------------------------------------------------


PWHInit(abletonTheme := "Mid Light", menuHotkeys := 0, quickToggleHotkeys := 0)
{
   ; Win+Ctrl+Shift+F22 is generated by Windows when you do a three-finger tap on the touchpad, and
   ; Win+Ctrl+Shift+F24 represents a four-finger tap. Since AutoHotkey doesn't allow arrays for
   ; optional parameter default values, we have to assign the default hotkeys here.
   if(!menuHotkeys)
      menuHotkeys := ["#^+F22", "*XButton1"]
   if(!quickToggleHotkeys)
      quickToggleHotkeys := ["#^+F24", "*XButton2", "Pause"]

   pwhAbletonTheme := abletonTheme

   ; This group helps us find the main DAW window as well as various popups created by the DAW, such
   ; as the settings window.
   GroupAdd, pwhDawGroup, ahk_class Ableton Live Window Class
   GroupAdd, pwhDawGroup, ahk_class bitwig

   ; This group is used to find all of the DAW's plug-in windows.
   GroupAdd, pwhPluginGroup, ahk_class AbletonVstPlugClass
   GroupAdd, pwhPluginGroup, ahk_class Vst3PlugWindow
   GroupAdd, pwhPluginGroup, ahk_class vst3window

   ; When we look for popup windows owned by plug-in windows, we need to ignore any IME windows
   ; created by the OS and linked to the active plug-in window without the DAW's knowledge.
   GroupAdd, pwhIgnoreGroup, ahk_class IME

   ; AutoHotkey's built-in GUI control click handling fires on button-down, but we want our menu
   ; items to trigger on button-up, so we have to handle the button-up messages ourselves.
   fnOnGuiButtonUp := Func("PWHOnGuiButtonUp")
   OnMessage(WM_LBUTTONUP, fnOnGuiButtonUp)
   OnMessage(WM_MBUTTONUP, fnOnGuiButtonUp)
   OnMessage(WM_RBUTTONUP, fnOnGuiButtonUp)

   ; See PWHOnWindowShowEvent.
   cb := RegisterCallback("PWHOnWindowShowEvent", "Fast")
   pwhWinEventHook := DllCall("SetWinEventHook"
      , "UInt", EVENT_OBJECT_SHOW
      , "UInt", EVENT_OBJECT_SHOW
      , "Ptr", 0
      , "Ptr", cb
      , "UInt", 0
      , "UInt", 0
      , "UInt", WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS)

   ; Register our hotkeys. We do this using the Hotkey command rather than "traditional" hotkeys
   ; because (a) this allows us to make the hotkeys configurable via parameters to PWHInit, and (b)
   ; we don't want to terminate the auto-execute section when this script is included into another
   ; AutoHotkey script...

   fnIsDawWinActive := Func("PWHIsDawWinActive")
   fnIsGuiActive := Func("PWHIsGuiActive")
   fnIsDawWinOrGuiActive := Func("PWHIsDawWinOrGuiActive")

   Hotkey, If, % fnIsDawWinActive

      for i, hk in menuHotkeys
         Hotkey, %hk%, PWHOpenMenu

   Hotkey, If, % fnIsGuiActive

      for i, hk in menuHotkeys
         Hotkey, %hk% Up, PWHOnGuiActiveMenuHotkeyUp

   Hotkey, If, % fnIsDawWinOrGuiActive

      for i, hk in quickToggleHotkeys
         Hotkey, %hk%, PWHQuickToggle

   Hotkey, If

   ; Register a function to run when this script exits.
   OnExit("PWHOnExit")
}

PWHOnExit()
{
   ; This function runs when this script exits...

   ; See PWHOnWindowShowEvent.
   DllCall("UnhookWinEvent", "Ptr", pwhWinEventHook)
}

PWHOnThreadStart()
{
   ; Every newly launched thread starts off with certain default settings that this script needs to
   ; override. Normally we would override the defaults in the auto-execute section, but this script
   ; can be included into another script, so we don't want to change any global settings. There's a
   ; call to this function at every thread entry point in this script.

   ; Make this script run as fast as possible by disabling all AutoHotkey delays.
   SetBatchLines, -1
   SetWinDelay, -1
   SetControlDelay, -1

   ; This script's main feature is working with hidden windows.
   DetectHiddenWindows, On
}

PWHOpenMenu()
{
   PWHOnThreadStart()

   ; If the user clicks the buttons for PWHOpenMenu and PWHQuickToggle at around the same time, the
   ; menu can open while the quick-toggle process is still running. This would cause the menu to be
   ; out of sync with the actual state of the plug-in windows, so we prevent it.
   if(pwhIsQuickToggleRunning)
      return

   ; Remember the time the menu was opened - see PWHOnGuiActiveMenuHotkeyUp.
   pwhOpenMenuTickCount := A_TickCount

   ; Get the process identifier(s) of whichever DAW instance happens to be active at the moment.
   PWHGetDawProcesses()

   ; Remember the active window so we can reactivate it when the menu is dismissed.
   WinGet, pwhOrigActiveWindow, ID, A

   ; Get the list of open plug-in windows owned by this DAW. We have to store this in a global so we
   ; can access it again from event handlers, such as when the user clicks a menu item.
   pwhPluginWindows := PWHGetPluginWindows()

   ; If there are no plug-in windows open, there's not much we can do.
   if(pwhPluginWindows.Length() = 0)
   {
      Msgbox, 0, Plug-In Window Helper, You need to open at least one plug-in window before using this feature.
      PWHReactivateDawWindow()
      return
   }

   ; Sort the list of plug-in windows by track name + plug-in name.
   PWHSort(pwhPluginWindows, Func("PWHCompareSortKey"))

   ; Build the menu GUI, display it, and activate it.
   PWHBuildGui()

   ; When the user does anything to activate another window besides our GUI, close the GUI. This
   ; happens, for example, when the user clicks in another application window, or hits the Start
   ; menu, or hits Alt+Tab, etc. In this scenario, we just need to destroy the GUI and not
   ; reactivate the DAW window, since the user's action has already made another window active and
   ; we don't want to mess with that.
   WinWaitNotActive, % "ahk_id " pwhGuiHwnd
   if(PWHIsGuiOpen())
      PWHDestroy()
}

PWHQuickToggle()
{
   PWHOnThreadStart()

   ; If the user clicks the buttons for PWHOpenMenu and PWHQuickToggle at around the same time,
   ; quick-toggle can be invoked while the menu GUI is still under construction. This would cause
   ; the menu to be out of sync with the actual state of the plug-in windows, so we prevent it.
   if(pwhIsBuildingGui)
      return

   pwhIsQuickToggleRunning := true

   ; We allow the user to invoke quick-toggle while the menu GUI is open (sometimes the user hits
   ; the menu hotkey when they meant to quick-toggle). In this case we just close the GUI and
   ; proceed as usual.
   if(PWHIsGuiOpen())
      PWHDestroy()

   ; Get the process identifier(s) of whichever DAW instance happens to be active at the moment.
   PWHGetDawProcesses()

   ; Clear out any active window we may have remembered from the last menu invocation. We don't want
   ; to try and reactivate it in the context of quick-toggle.
   pwhOrigActiveWindow := 0

   ; Get the list of open plug-in windows owned by this DAW and see if any is visible. This
   ; determines whether we are hiding all the visible windows or re-showing them again.
   pluginWindows := PWHGetPluginWindows()
   anyVisible := false
   for i, window in pluginWindows
   {
      if(window.isVisible)
      {
         anyVisible := true
         break
      }
   }

   if(anyVisible)
   {
      ; There is at least one plug-in window visible, so our job is to remember which ones are
      ; visible and then hide them all...

      ; Store a copy of the plug-in window state for this DAW instance, so we can re-show the windows
      ; later. Since we support multiple DAW instances, we have to be able to store state for each
      ; DAW instance separately. Note that this state never gets cleaned up (until the script exits)
      ; but it's a small amount of data, and the user is probably not opening and closing the DAW
      ; all that frequently.
      pwhPluginWindowsRestore[pwhDawMainProcess] := PWHClone(pluginWindows)

      ; Hide all the visible windows and update the state to reflect that they are hidden.
      for i, window in pluginWindows
      {
         window.isVisible := false
         PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible)
      }

      ; Store the plug-in window state to reflect that all windows are hidden. Then if the DAW tries
      ; to auto-show any of these windows, we will be able to quickly hide them again.
      pwhPluginWindowsLastState[pwhDawMainProcess] := pluginWindows
   }
   else
   {
      ; There are no plug-in windows visible, so we need to find out which ones were last visible
      ; and, if possible, re-show them...

      ; If we have any stored state to restore
      if(pwhPluginWindowsRestore[pwhDawMainProcess].Length() > 0)
      {
         ; Find at least one stored plug-in window that was formerly visible AND still exists (the
         ; user could have closed the window since we stored the state).
         for i, window in pwhPluginWindowsRestore[pwhDawMainProcess]
         {
            if(window.isVisible and WinExist("ahk_id " window.hwnd))
            {
               anyVisible := true
               break
            }
         }

         ; If we found one, we can safely use the stored state. (If we don't use the stored state,
         ; we will use the "live" pluginWindows state that we retrieved above, and that we already
         ; know does not contain any visible windows.)
         if(anyVisible)
            pluginWindows := pwhPluginWindowsRestore[pwhDawMainProcess]
      }

      ; If we get to this point and we have not found a formerly-visible window to re-show, then we
      ; want to show ALL of the hidden plug-in windows. (There isn't anything else we can reasonably
      ; do in this situation, and at least this shows SOMETHING.) Note that if there just aren't any
      ; windows at all, pluginWindows will be empty; see below where we display a message box.
      if(!anyVisible)
      {
         for i, window in pluginWindows
            window.isVisible := true
      }

      anyVisible := false

      if(pluginWindows.Length() > 0)
      {
         ; Before we re-show the windows, we have to update the stored state so that the code in
         ; PWHOnWindowShowTimer won't immediately re-hide them!
         pwhPluginWindowsLastState[pwhDawMainProcess] := pluginWindows

         ; Depending what part of the code last stored the list, it may be sorted by name. We need
         ; it sorted by z-order, back-to-front. This way when we re-show the windows, they will be
         ; in the same z-order as when they were hidden.
         PWHSort(pluginWindows, Func("PWHCompareZOrderDesc"))

         ; Now re-show the windows that were formerly visible and still exist.
         for i, window in pluginWindows
         {
            if(window.isVisible and WinExist("ahk_id " window.hwnd))
            {
               PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible)
               anyVisible := true
            }
         }
      }

      ; We didn't find any plug-in windows to show, so we throw up our hands.
      if(!anyVisible)
         Msgbox, 0, Plug-In Window Helper, You need to open at least one plug-in window before using this feature.
   }

   ; Activate the DAW main window or its frontmost plug-in window.
   PWHReactivateDawWindow()

   pwhIsQuickToggleRunning := false
}

PWHOnGuiActiveMenuHotkeyUp()
{
   PWHOnThreadStart()

   ; We open the menu when the user presses the menu hotkey, and we close it when they release the
   ; hotkey. If they move the mouse slightly while clicking, or if the menu opens so that the mouse
   ; is positioned over a menu command, the menu could disappear instantly. To avoid this, we
   ; remember the time the menu was opened and ignore any button release that happens very shortly
   ; afterwards.
   elapsed := A_TickCount - pwhOpenMenuTickCount
   if(elapsed < 500)
      return

   if(PWHIsMouseOverGui())
   {
      SendEvent, {Blind}{LButton Up}
   }
   else
   {
      PWHDestroy()
      PWHReactivateDawWindow()
   }
}

PWHDestroy()
{
   ; AutoHotkey freaks out if we destroy the GUI while we're still in the process of assembling it,
   ; so ignore any requests to destroy it while it's being built. (This can happen if several
   ; hotkeys are triggered at around the same time.)
   if(!pwhIsBuildingGui)
   {
      Gui, % pwhGuiHwnd ":Destroy"
      SetTimer, PWHOnHighlightTimer, Delete
   }
}

PWHIsGuiOpen()
{
   return WinExist("ahk_id " pwhGuiHwnd)
}

PWHIsGuiActive()
{
   return WinActive("ahk_id " pwhGuiHwnd)
}

PWHIsMouseOverGui()
{
   MouseGetPos, , , mouseWin
   return (mouseWin = pwhGuiHwnd)
}

PWHIsDawWinActive()
{
   ; Note that this function returns true if any of the windows belonging to the DAW process is
   ; active, including the main window, any popup window, or any plug-in window.
   WinGet, pname, ProcessName, A
   return RegExMatch(pname, "^(Ableton Live|Bitwig)") > 0
}

PWHIsDawWinOrGuiActive()
{
   return PWHIsDawWinActive() or PWHIsGuiActive()
}

PWHGetDawProcesses()
{
   ; Note that this function is always called from contexts where we already know that the DAW is
   ; the active application.

   ; Get the frontmost window that is either the DAW main window or one of its popups (but not
   ; a plug-in window - these may run in child processes).
   WinGet, dawMainWindow, ID, ahk_group pwhDawGroup

   ; Get the process ID from the window. This is the main process of the DAW, meaning it is not one
   ; of the plug-in-hosting child processes.
   WinGet, pwhDawMainProcess, PID, ahk_id %dawMainWindow%

   ; Remember which DAW this is - this affects the look and feel of our GUI.
   WinGet, pname, ProcessName, ahk_id %dawMainWindow%
   pwhIsBitwig := RegExMatch(pname, "^Bitwig") > 0

   ; Get the PID and parent PID of every running process belonging to a DAW.
   procs := []
   for proc in ComObjGet("winmgmts:").ExecQuery("select Handle, ParentProcessId from Win32_Process where Name like 'Ableton Live%' or Name like 'Bitwig%'")
      procs.Push({ pid: proc.Handle, parentPid: proc.ParentProcessId })

   ; Build a collection of PIDs containing the main DAW process and all of its descendant processes.
   ; This is keyed by PID so PWHGetPluginWindows can quickly decide if a window belongs to this DAW.
   pwhDawProcesses := { (pwhDawMainProcess): true }
   for i, pid in PWHGetDescendantProcesses(pwhDawMainProcess, procs)
      pwhDawProcesses[pid] := true
}

PWHGetDescendantProcesses(parentPid, procs)
{
   pids := []
   for i, proc in procs
   {
      if(proc.parentPid = parentPid)
      {
         pids.Push(proc.pid)
         pids.Push(PWHGetDescendantProcesses(proc.pid, procs)*)
      }
   }

   return pids
}

PWHGetPluginWindows()
{
   pluginWindows := []
   windowsByHwnd := {}

   ; Get every window in the system that is categorized as a DAW plug-in window (see pwhPluginGroup).
   WinGet, winList, List, ahk_group pwhPluginGroup
   Loop, %winList%
   {
      hwnd := winList%A_Index%

      ; We are only interested in popup windows belonging to the active DAW's process(es).
      WinGet, pid, PID, ahk_id %hwnd%
      if(!pwhDawProcesses[pid])
         continue

      WinGetTitle, title, ahk_id %hwnd%
      WinGetClass, className, ahk_id %hwnd%

      ; The plug-in window title contains two important pieces of information: The track name and the
      ; plug-in name, separated by a forward slash. Since the slash is our only clue, we can handle
      ; slashes in the track name, but a slash in the plug-in name would mess us up. (So far we have
      ; not encountered a plug-in with a slash in its name.)

      titleParts := StrSplit(title, "/")

      ; In Ableton the plug-in name comes first, and in Bitwig it comes last. Once we have the plug-in
      ; name, we remove it so we can concatenate the remaining parts into the track name.
      pluginNameIdx := pwhIsBitwig ? titleParts.Length() : 1
      pluginName := Trim(titleParts[pluginNameIdx])
      titleParts.RemoveAt(pluginNameIdx)

      ; Now whatever parts are left get concatenated into the track name. (If the track name
      ; contains no slashes, there will only be one part left.)
      trackName := ""
      for i, titlePart in titleParts
      {
         if(i > 1)
            trackName .= "/"
         trackName .= titlePart
      }
      trackName := Trim(trackName)

      ; Now we know all we need to about this plug-in window. (Note that we fill in ownedHwnds in the
      ; next step.) Add it to the array...

      window := { hwnd:       hwnd
                , ownedHwnds: []
                , trackName:  trackName
                , pluginName: pluginName
                , className:  className
                , zOrder:     A_Index
                , sortKey:    trackName "`t" pluginName
                , isVisible:  PWHIsWinVisible(hwnd) }

      pluginWindows.Push(window)

      ; We use this map of hwnd to window to improve the performance of the next step.
      windowsByHwnd[hwnd] := window
   }

   ; Now fill in the ownedHwnds array for each plug-in window. Note that we only look one window
   ; deep; we have not yet encountered a plug-in that opens a popup that in turn opens another
   ; popup...

   ; Get every window in the system.
   WinGet, winList, List
   Loop, %winList%
   {
      hwnd := winList%A_Index%

      ; We are only interested in owned popup windows.
      ownerHwnd := DllCall("GetWindow", "Ptr", hwnd, "UInt", GW_OWNER, "Ptr")
      if(ownerHwnd)
      {
         ; See if this popup window's owner is one of the plug-in windows we retrieved in the
         ; previous step.
         window := windowsByHwnd[ownerHwnd]

         ; We need to ignore any IME windows created by the OS and linked to the active plug-in
         ; window without the DAW's knowledge. Only add this popup window to the plug-in window's
         ; ownedHwnds if it is not in the "ignore" group.
         if(window and !WinExist("ahk_id " hwnd " ahk_group pwhIgnoreGroup"))
            window.ownedHwnds.Push(hwnd)
      }
   }

   ; This array is ordered by z-order, front-to-back.
   return pluginWindows
}

PWHBuildGui()
{
   ; We need to keep track of when we're building the GUI so we don't try to do anything else at the
   ; same time.
   pwhIsBuildingGui := true

   ; Bitwig uses different colors (see PWHGetThemeColors) as well as a different font and spacing.
   ; All of the differences are captured in themeColors and the following constants...

   themeColors := PWHGetThemeColors()

   if(pwhIsBitwig)
   {
      FONT_NAME := "Segoe UI"
      FONT_WEIGHT := "normal"
      FONT_SIZE := 10
      CHECK_MARK_FONT_SIZE := 13
      ITEM_SPACE_X := 14
      ITEM_SPACE_Y := 6
      SEP_SPACE_X := 6
      SEP_SPACE_Y := 7
      MARGIN_L := 10
      MARGIN_R := 20
      MARGIN_T := 6
      MARGIN_B := 5
      CHECK_MARK_X := 9
      CHECK_MARK_OFFSET_Y := 0
      ITEM_LABEL_X := 30
      SCALE_BORDERS := false
   }
   else
   {
      FONT_NAME := "Arial"
      FONT_WEIGHT := "bold"
      FONT_SIZE := 8
      CHECK_MARK_FONT_SIZE := 11
      ITEM_SPACE_X := 10
      ITEM_SPACE_Y := 4
      SEP_SPACE_X := 0
      SEP_SPACE_Y := 6
      MARGIN_L := 8
      MARGIN_R := 20
      MARGIN_T := 6
      MARGIN_B := 5
      CHECK_MARK_X := 6
      CHECK_MARK_OFFSET_Y := -2
      ITEM_LABEL_X := 22
      SCALE_BORDERS := true
   }

   ; We need to use this special combination of options to get controls that will paint properly to
   ; show the controls behind them. This is what enables the menu highlight on mouse hover.
   TRANSPARENT := "BackgroundTrans E" WS_EX_TRANSPARENT

   ; We don't want our menu to have a caption or be listed in the taskbar, and it must appear on top
   ; of all the DAW windows. The Label option establishes a naming convention for the GUI's callback
   ; functions.
   Gui, New, +HwndpwhGuiHwnd -Caption  +ToolWindow +AlwaysOnTop +LabelPWHOnGui

   ; We determine our own left, right, and top margins, but we let AutoHotkey create a bottom margin
   ; after the last menu item.
   Gui, Margin, 0, %MARGIN_B%

   ; Set the font and background colors for the GUI.
   Gui, Font, % FONT_WEIGHT " s" FONT_SIZE " c" themeColors.textColor, %FONT_NAME%
   Gui, Color, % themeColors.backColor, % themeColors.backColor

   ; Add the caption for the list of plug-in windows.
   Gui, Add, Text, hwndhwnd x%MARGIN_L% y%MARGIN_T%, Plug-In Windows:

   ; We need to take the caption's width into account when we figure out how wide the menu should
   ; be. (We account for the rest of the menu items below.)
   GuiControlGet, textPos, Pos, %hwnd%
   menuWidth := textPosX + textPosW

   menuItems := []
   maxX := 0

   ; Add a menu item for each plug-in window. Each menu item has three Text controls: Check mark,
   ; track name, and plug-in name. We need to use three separate Text controls to get the three
   ; columns aligned properly.
   for i, window in pwhPluginWindows
   {
      textHwnds := []

      ; Check mark: Note that we use a glyph from the Webdings font. Apply a y-offset to get it to
      ; line up properly with the text.
      Gui, Font, % "s" CHECK_MARK_FONT_SIZE, Webdings
      Gui, Add, Text, % "hwndcheckMarkHwnd " TRANSPARENT " x" CHECK_MARK_X " y+" (ITEM_SPACE_Y + CHECK_MARK_OFFSET_Y)
      Gui, Font, % "s" FONT_SIZE, %FONT_NAME%
      textHwnds.Push(checkMarkHwnd)

      ; Track name: Note that we have to undo the check mark's y-offset. SS_NOPREFIX turns off
      ; special handling of & characters in the track name.
      Gui, Add, Text, % "hwndhwnd x" ITEM_LABEL_X " yp+" -CHECK_MARK_OFFSET_Y " " TRANSPARENT " " SS_NOPREFIX, % window.trackName
      textHwnds.Push(hwnd)

      ; Plug-in name: Leave a bit of horizontal space between the track name and plug-in name, but at
      ; this stage, the plug-in names do not yet line up vertically.
      Gui, Add, Text, % "hwndhwnd x+" ITEM_SPACE_X " yp " TRANSPARENT " " SS_NOPREFIX, % window.pluginName
      textHwnds.Push(hwnd)

      ; Accumulate the x-position of the rightmost plug-in name control. We will line up all of the
      ; plug-in names at this position.
      GuiControlGet, textPos, Pos, %hwnd%
      maxX := Max(maxX, textPosX)

      ; Keep some information about each menu item for use in the next steps. Note that the command
      ; ID of a plug-in window menu item is just its ordinal.
      menuItems.Push({ cmdID: i, checkMarkHwnd: checkMarkHwnd, textHwnds: textHwnds })
   }

   ; Now walk through the menu items and line up the plug-in names so they appear in a column.
   ; The plug-in name is in the last Text control in each menu item's textHwnds array.
   for i, menuItem in menuItems
      GuiControl, Move, % menuItem.textHwnds[menuItem.textHwnds.Length()], % "x" maxX

   ; Calculate the menu width as the right edge of the widest menu item, plus a margin. Note that we
   ; don't need to account for the Hide All and Show All items because they are narrower than the
   ; "Plug-In Windows:" caption.
   for i, menuItem in menuItems
   {
      GuiControlGet, textPos, Pos, % menuItem.textHwnds[menuItem.textHwnds.Length()]
      menuWidth := Max(menuWidth, textPosX + textPosW)
   }
   menuWidth += MARGIN_R

   ; Add a horizontal separator line.
   PWHAddGuiColorBlock(SEP_SPACE_X, "+" SEP_SPACE_Y, menuWidth - (SEP_SPACE_X * 2), 1, themeColors.separatorColor)

   ; Add the Hide All and Show All items. Note that they have command IDs that will not conflict
   ; with any of the plug-in window menu items. They also do not have check marks...

   Gui, Add, Text, hwndhwnd x%ITEM_LABEL_X% y+%SEP_SPACE_Y% %TRANSPARENT%, Hide All
   menuItems.Push({ cmdID: PWH_CMDID_HIDE_ALL, textHwnds: [hwnd] })

   Gui, Add, Text, hwndhwnd x%ITEM_LABEL_X% y+%ITEM_SPACE_Y% %TRANSPARENT%, Show All
   menuItems.Push({ cmdID: PWH_CMDID_SHOW_ALL, textHwnds: [hwnd] })

   pwhMenuItemControls := []

   ; Now that we've added all the menu items, create a clickable control for each one. We can't just
   ; make the existing text controls clickable because there is space around and between the
   ; controls, and the user can click in the spaces.
   for i, menuItem in menuItems
   {
      ; We want to position the clickable control directly on top of the menu item, so we need the
      ; y-position and height of the item.
      GuiControlGet, textPos, Pos, % menuItem.textHwnds[menuItem.textHwnds.Length()]
      y := textPosY - (ITEM_SPACE_Y / 2)
      h := textPosH + ITEM_SPACE_Y

      ; Add the clickable control so it spans the entire width of the menu. Note that it must have a
      ; click handler (even one that does nothing) so it will get detected when we call MouseGetPos.
      Gui, Add, Text, % "hwndclickableHwnd gPWHOnMenuItemClick x0 y" y " w" menuWidth " h" h " " TRANSPARENT

      ; We keep a global array of menu item controls so we can use this info when handling clicks
      ; and highlighting menu items on mouse hover.
      pwhMenuItemControls.Push({ cmdID: menuItem.cmdID, clickableHwnd: clickableHwnd, checkMarkHwnd: menuItem.checkMarkHwnd, textHwnds: menuItem.textHwnds })
   }

   ; We highlight a menu item on mouse hover by positioning a colored block behind the highlighted
   ; item. For now, just create the colored block and hide it. It gets shown and positioned in
   ; PWHOnHighlightTimer.
   pwhHighlightHwnd := PWHAddGuiColorBlock(0, 0, 1, 1, themeColors.highlightBackColor)
   GuiControl, Hide, %pwhHighlightHwnd%

   ; Show the GUI offscreen, so we can measure it and position it properly - we don't want it to
   ; visibly "jump". PWHPositionGuiOnScreen will move it back on screen to its final position.
   Gui, Show, x-999999 y-999999

   ; Update the check marks to reflect the visible/hidden status of each plug-in window.
   PWHUpdateCheckMarks()

   ; Add borders around the edges of the menu.
   PWHAddGuiBorders(themeColors, SCALE_BORDERS)

   ; Now that everything is finally set, we can move the menu to a position on-screen relative to
   ; the mouse pointer.
   PWHPositionGuiOnScreen()

   ; The simplest way to update the mouse hover menu highlight is to use a timer, because that way
   ; we can detect when the mouse moves outside the GUI window and clear the highlight. (Getting
   ; mouse messages from outside our window is possible but a lot more complicated.) Luckily, the
   ; CPU cost of doing this on a timer (even for large menus) is imperceptible.
   SetTimer, PWHOnHighlightTimer, 50

   pwhIsBuildingGui := false
}

PWHOnGuiEscape()
{
   ; Close the menu when the user hits Escape.
   PWHOnThreadStart()
   PWHDestroy()
   PWHReactivateDawWindow()
}

PWHOnMenuItemClick()
{
   ; We need this do-nothing click handler so the control will get detected when we call
   ; MouseGetPos. The actual click handling is done in PWHOnGuiButtonUp.
}

PWHAddGuiBorders(themeColors, scaleBorders)
{
   ; Because of DPI scaling, we can't position the borders with AutoHotkey's Gui commands; depending
   ; on the scale factor there would be rounding errors and the borders wouldn't line up exactly. We
   ; have to use WinGetPos and ControlMove, which operate on actual, unscaled pixels...

   ; First add four color blocks to the GUI, one for each border (top, bottom, left, right). They
   ; are initially 1x1 in scaled units.
   borderHwnds := []
   Loop, 4
      borderHwnds.Push(PWHAddGuiColorBlock(0, 0, 1, 1, themeColors.borderColor))

   ; Get the size of the GUI window in pixels.
   WinGetPos, , , guiWidth, guiHeight, ahk_id %pwhGuiHwnd%

   ; We want borders that either scale in thickness according to the DPI scale factor (Ableton), or
   ; are always exactly one pixel thick (Bitwig). If we want them to scale, we measure the width and
   ; height (in pixels) of one of the 1x1 color blocks we created above.
   if(scaleBorders)
      ControlGetPos, , , borderWidth, borderHeight, , % "ahk_id " borderHwnds[1]
   else
      borderWidth := borderHeight := 1

   ; Now we can position each border in unscaled pixel units.
   ControlMove, , 0, 0, guiWidth, borderHeight, % "ahk_id " borderHwnds[1] ; top
   ControlMove, , 0, 0, borderWidth, guiHeight, % "ahk_id " borderHwnds[2] ; left
   ControlMove, , 0, guiHeight - borderHeight, guiWidth, borderHeight, % "ahk_id " borderHwnds[3] ; bottom
   ControlMove, , guiWidth - borderWidth, 0, borderWidth, guiHeight, % "ahk_id " borderHwnds[4] ; right

   ; Remember the border width; we will need it when we position the menu item highlight.
   pwhBorderWidth := borderWidth
}

PWHAddGuiColorBlock(x, y, w, h, color)
{
   ; In the AutoHotkey community, the generally accepted method for creating a solid block of an
   ; arbitrary color is to use a Progress control with the desired background color. Note that we
   ; have to turn off the WS_EX_STATICEDGE style to get rid of the border.
   Gui, Add, Progress, % "hwndhwnd x" x " y" y " w" w " h" h " Background" color
   Control, ExStyle, % -WS_EX_STATICEDGE, , % "ahk_id " hwnd
   return hwnd
}

PWHPositionGuiOnScreen()
{
   ; Note that before this function is called, we have the GUI positioned so it is "hidden" offscreen.

   ; Get the mouse position in screen coords.
   CoordMode, Mouse, Screen
   MouseGetPos, x, y

   ; Get the working area (excluding the taskbar) of the monitor the mouse is on.
   hMonitor := PWHGetMouseMonitor()
   monInfo := PWHGetMonitorInfo(hMonitor)

   ; Get the dimensions of the GUI window.
   WinGetPos, , , width, height, ahk_id %pwhGuiHwnd%

   ; Move the GUI to its final position: Aligned to the mouse pointer, but not extending outside the
   ; screen bounds.
   x := PWHConstrainCoord(x, width, monInfo.workLeft, monInfo.workRight)
   y := PWHConstrainCoord(y, height, monInfo.workTop, monInfo.workBottom)
   Gui, Show, x%x% y%y%
}

PWHConstrainCoord(coord, extent, screenMin, screenMax)
{
   ; Normally we display the GUI so its top left is at the mouse position. However, depending on the
   ; mouse location and GUI size, the GUI could extend off the edge of the screen. To avoid this, we
   ; first try to flip the GUI to the opposite side of the mouse pointer, and if it won't fit there
   ; either, we shift it the minimum distance needed to get it entirely on-screen. Note that we do
   ; NOT handle the case where the GUI is actually wider or taller than the screen - to handle this
   ; would require a scrollable GUI.

   if(coord + extent > screenMax)
   {
      if(coord - extent >= screenMin)
         coord -= extent
      else
         coord -= (coord + extent) - screenMax
   }

   return coord
}

PWHOnGuiButtonUp(wParam, lParam, msg, hwnd)
{
   ; AutoHotkey's built-in GUI control click handling fires on button-down, but we want our menu
   ; items to trigger on button-up, so we have to handle the button-up messages ourselves...

   PWHOnThreadStart()

   ; Get the command ID for the menu item the mouse pointer was over when the button was released.
   cmdID := 0
   for i, menuItemCtl in pwhMenuItemControls
   {
      if(menuItemCtl.clickableHwnd = hwnd)
      {
         cmdID := menuItemCtl.cmdID
         break
      }
   }

   ; A real cmdID is never zero, so this means the mouse was not over any menu item; we're done.
   if(cmdID = 0)
      return

   ; When the user clicks with the right mouse button or control-clicks with any button, we toggle
   ; the visibility of the selected plug-in window, instead of showing that plug-in window and hiding
   ; all others. We also keep the menu open to allow the user to toggle more windows. Note that
   ; toggle mode doesn't change the behavior of Hide All and Show All; it just keeps the menu open.
   isToggle := (msg = WM_RBUTTONUP) or GetKeyState("Control", "P")

   ; First, update the isVisible flag for each entry in the pwhPluginWindows array...

   if(cmdID = PWH_CMDID_HIDE_ALL or cmdID = PWH_CMDID_SHOW_ALL)
   {
      for i, window in pwhPluginWindows
         window.isVisible := (cmdID = PWH_CMDID_SHOW_ALL)
   }
   else if(isToggle)
   {
      window := pwhPluginWindows[cmdID]
      window.isVisible := !window.isVisible
   }
   else
   {
      for i, window in pwhPluginWindows
         window.isVisible := (i = cmdID)
   }

   ; If we're in toggle mode, the menu will stay open, so we need to update the check mark for each
   ; plug-in window menu item.
   if(isToggle)
      PWHUpdateCheckMarks()

   ; Update the stored plug-in window state with a copy of the new visible/hidden state. Then if the
   ; DAW tries to auto-show any of the hidden windows later, we will be able to quickly hide them again.
   pwhPluginWindowsLastState[pwhDawMainProcess] := PWHClone(pwhPluginWindows)

   ; When we show a plug-in window, we also bring it to the front of the z-order and activate it.
   ; Sort the array by z-order, back to front, so the windows that we are about to show will
   ; preserve their existing z-order. This is most noticeable when you do Hide All followed by Show
   ; All; the windows get re-shown in the same z-order they were in before. (Note that hiding a
   ; window does not affect the z-order.)
   PWHSort(pwhPluginWindows, Func("PWHCompareZOrderDesc"))

   ; Now actually show or hide each window according to its isVisible flag...

   for i, window in pwhPluginWindows
   {
      ; Note that this function does nothing if the window's visibility already matches the
      ; isVisible flag. changedVisible tells us whether anything actually happened.
      changedVisible := PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible)

      ; There's a weird behavior where, if we're in toggle mode (i.e. the menu stays open) and we
      ; show a hidden plug-in window, then when the user next dismisses the GUI, one of the visible
      ; plug-in windows further back in the z-order will swap places with another plug-in window.
      ; Activating the GUI window at this point makes this weird behavior go away. Unfortunately it
      ; also slows down the Show All command a bit in toggle mode. (We really shouldn't need to do
      ; this because we are about to activate the GUI window at the end of this function, but go
      ; figure.)
      if(isToggle and window.isVisible and changedVisible)
         WinActivate, ahk_id %pwhGuiHwnd%
   }

   ; Refresh the array of plug-in windows that drives the GUI. Since we've probably just messed with
   ; the z-order, we want the z-order stored in the array to be correct. Note that the array needs
   ; to be sorted to match the menu order.
   pwhPluginWindows := PWHGetPluginWindows()
   PWHSort(pwhPluginWindows, Func("PWHCompareSortKey"))

   ; If we just ran any command EXCEPT Hide All, update the plug-in window state that supports the
   ; quick-toggle feature. We don't update it on Hide All because then quick-toggle would be useless
   ; immediately after Hide All (it would always show the message "You need to show at least one
   ; plug-in window before using quick-toggle"). This way if you pick Hide All by mistake, you can
   ; immediately undo it with quick-toggle.
   if(cmdID != PWH_CMDID_HIDE_ALL)
      pwhPluginWindowsRestore[pwhDawMainProcess] := PWHClone(pwhPluginWindows)

   if(isToggle)
   {
      ; Re-activate the GUI window, because otherwise the WinWaitNotActive at the end of PWHOpenMenu
      ; will get triggered and the GUI will close - not what we want!
      WinActivate, ahk_id %pwhGuiHwnd%
   }
   else
   {
      PWHDestroy()
      PWHReactivateDawWindow()
   }
}

PWHOnHighlightTimer()
{
   ; We highlight a menu item on mouse hover by positioning a colored block behind the highlighted
   ; item. The simplest way to update the menu highlight is to use a timer, because that way we can
   ; detect when the mouse moves outside the GUI window and clear the highlight. (Getting mouse
   ; messages from outside our window is possible but a lot more complicated.) Luckily, the CPU cost
   ; of doing this on a timer (even for large menus) is imperceptible...

   PWHOnThreadStart()

   themeColors := PWHGetThemeColors()

   ; Find out which control (if any) the mouse pointer is over.
   MouseGetPos, , , , mouseCtl, 3

   highlightedMenuItemCtl := 0

   ; For each menu item...
   for i, menuItemCtl in pwhMenuItemControls
   {
      ; This menu item is highlighted if the mouse is over its clickable control.
      isHighlight := (mouseCtl = menuItemCtl.clickableHwnd)

      ; We will need to know which item (if any) is highlighted later.
      if(isHighlight)
         highlightedMenuItemCtl := menuItemCtl

      ; Set the text color (highlighted/not) of each text control belonging to this menu item.
      for j, textHwnd in menuItemCtl.textHwnds
         GuiControl, % "+c" (isHighlight ? themeColors.highlightTextColor : themeColors.textColor), % textHwnd
   }

   if(highlightedMenuItemCtl)
   {
      ; Get the width of the GUI window and the y-position and height of this menu item (by way of
      ; its clickable control).
      WinGetPos, , , guiW, , ahk_id %pwhGuiHwnd%
      ControlGetPos, , clickableY, , clickableH, , % "ahk_id " highlightedMenuItemCtl.clickableHwnd

      ; Calculate the new position for the color block used to highlight the menu item.
      x := pwhBorderWidth
      y := clickableY
      w := guiW - (pwhBorderWidth * 2)
      h := clickableH

      ; Get the current position of the color block.
      ControlGetPos, highlightX, highlightY, highlightW, highlightH, , % "ahk_id " pwhHighlightHwnd

      ; We don't want to do anything if the highlight color block is already visible and in the
      ; right position, because this would cause flickering.
      if(!PWHIsControlVisible(pwhHighlightHwnd) or (highlightX != x) or (highlightY != y) or (highlightW != w) or (highlightH != h))
      {
         ; Show and position the color block, sending it to the bottom of the z-order (it has to be
         ; behind all of the text controls that make up the menu item).
         DllCall("SetWindowPos", "UInt", pwhHighlightHwnd, "Int", HWND_BOTTOM, "Int", x, "Int", y, "Int", w, "Int", h, "UInt", SWP_SHOWWINDOW)

         ; Force a repaint of the section of the GUI window now occupied by the color block. When we
         ; move the color block, windows automatically repaints the former position but not the new
         ; position - weird.
         PWHInvalidateRect(pwhGuiHwnd, x, y, w, h)
      }
   }
   else
   {
      ; No item is highlighted, so we can just hide the color block.
      GuiControl, Hide, %pwhHighlightHwnd%
   }
}

PWHUpdateCheckMarks()
{
   ; Update the text of each menu item's check mark control to either contain a check mark (glyph
   ; 0x61 in Webdings) or blank, matching the plug-in window's isVisible flag. Note that not every
   ; menu item has a check mark control (Hide All, Show All).
   for i, menuItemCtl in pwhMenuItemControls
   {
      if(menuItemCtl.checkMarkHwnd)
         GuiControl, , % menuItemCtl.checkMarkHwnd, % pwhPluginWindows[menuItemCtl.cmdID].isVisible ? Chr(0x61) : ""
   }
}

PWHGetThemeColors()
{
   ; Note that Bitwig currently does not allow customization of its UI colors.
   if(pwhIsBitwig)
      return { backColor: "353535", textColor: "ffffff", highlightBackColor: "c4c4c4", highlightTextColor: "353535", borderColor: "272727", separatorColor: "8c8c8c" }

   switch pwhAbletonTheme
   {
      case "Light":              return { backColor: "ffffff", textColor: "000000", highlightBackColor: "79c3ec", highlightTextColor: "000000", borderColor: "000000", separatorColor: "000000" }
      case "Mid Light", default: return { backColor: "dcdcdc", textColor: "000000", highlightBackColor: "bfe9ff", highlightTextColor: "000000", borderColor: "000000", separatorColor: "000000" }
      case "Mid Dark":           return { backColor: "333333", textColor: "e0e0e0", highlightBackColor: "8ccad8", highlightTextColor: "141414", borderColor: "e0e0e0", separatorColor: "e0e0e0" }
      case "Dark":               return { backColor: "1e1e1e", textColor: "dcdcdc", highlightBackColor: "8ccad8", highlightTextColor: "000000", borderColor: "dcdcdc", separatorColor: "dcdcdc" }
   }
}

PWHReactivateDawWindow()
{
   if(PWHIsWinVisible(pwhOrigActiveWindow))
   {
      ; Our preference is to reactivate the window that was originally active before we displayed
      ; our GUI menu.
      WinActivate, % "ahk_id " pwhOrigActiveWindow
   }
   else
   {
      ; However, if the user just hid the window that was active, we have to settle for the next
      ; best thing. First, find the frontmost visible plug-in window and activate that. Note that the
      ; array returned by PWHGetPluginWindows is in front-to-back order...

      pluginWindows := PWHGetPluginWindows()

      anyVisible := false
      for i, window in pluginWindows
      {
         if(window.isVisible)
         {
            WinActivate, % "ahk_id " window.hwnd
            anyVisible := true
            break
         }
      }

      ; If there is no visible plug-in window, fall back to activating the frontmost window belonging
      ; to the DAW.
      if(!anyVisible)
         WinActivate, ahk_group pwhDawGroup
   }
}

PWHShowHidePluginWindow(hwnd, ownedHwnds, showIt)
{
   changedVisible := false

   isVisible := PWHIsWinVisible(hwnd)

   if(showIt and !isVisible)
   {
      WinShow, ahk_id %hwnd%
      changedVisible := true

      ; Whenever we activate a plug-in window, it becomes the window that should be reactivated when
      ; our GUI menu is dismissed (overriding any previously-stored window). However, we only want
      ; to do this for plug-in windows, not their owned popup windows. The ownedHwnds parameter is
      ; used to make this distinction (see the recursive call to PWHShowHidePluginWindow below).
      if(ownedHwnds)
         pwhOrigActiveWindow := hwnd
   }
   else if(!showIt and isVisible)
   {
      WinHide, ahk_id %hwnd%
      changedVisible := true
   }

   ; If this plug-in window owns any popup windows, show/hide them to match the visibility of the
   ; plug-in window. Note that we only go one window deep, and we don't update the changedVisible
   ; flag (it only applies to the plug-in window itself).
   for i, ownedHwnd in ownedHwnds
      PWHShowHidePluginWindow(ownedHwnd, 0, showIt)

   ; The caller needs to know whether we actually changed the visibility of the window.
   return changedVisible
}

PWHOnWindowShowEvent(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime)
{
   ; Bitwig hides all plug-in windows when the user invokes a modal UI such as the pop-up browser,
   ; and then shows all of them again when the user closes the modal UI. This is a problem if the
   ; user has used PWH to hide some of the plug-in windows - they will reappear every time a modal UI
   ; is closed! We work around this by registering a windows event hook for the "object show" event,
   ; so we will be notified every time any window gets shown. We can then see if the shown window is
   ; one that we want hidden, and immediately re-hide it. This is fast enough that you usually don't
   ; see the window at all, while sometimes you see a momentary flicker, depending on the number of
   ; windows that need to be hidden.

   ; (This whole thing can be removed if Bitwig ever fixes their logic so that they don't show a
   ; window unless they hid it in the first place.)

   ; To ensure that we don't miss any events, this event handler must execute in as short a time as
   ; possible. Therefore, we just add the window handle to a collection and set a one-shot timer to
   ; process it later. (If there is already a pending timer, this has no effect.) The critical
   ; section prevents the event handler and timer function from modifying the global variable at the
   ; same time. Note that the collection is keyed by window handle so we can do fast lookups...

   Critical, On
   pwhWindowShowEvents[hwnd] := true
   Critical, Off

   SetTimer, PWHOnWindowShowTimer, -1
}

PWHOnWindowShowTimer()
{
   PWHOnThreadStart()

   ; To ensure that each window recorded by PWHOnWindowShowEvent is processed exactly once, we clone
   ; and clear the global collection. The critical section prevents the event handler and timer
   ; function from modifying the global variable at the same time.
   Critical, On
   shownHwnds := pwhWindowShowEvents.Clone()
   pwhWindowShowEvents := {}
   Critical, Off

   ; We get notified any time ANY window is shown, across the entire system. We don't want to waste
   ; any CPU time processing show events when Bitwig is not the active application.
   WinGet, pname, ProcessName, A
   if(RegExMatch(pname, "^Bitwig") = 0)
      return

   for dawMainWindow, pluginWindows in pwhPluginWindowsLastState
   {
      for i, window in pluginWindows
      {
         ; This window should be hidden but just got shown - hide it again.
         if(!window.isVisible and shownHwnds[window.hwnd])
            PWHShowHidePluginWindow(window.hwnd, window.ownedHwnds, window.isVisible)
      }
   }
}

PWHIsWinVisible(hwnd)
{
   WinGet, style, Style, ahk_id %hwnd%
   return style and (style & WS_VISIBLE) != 0
}

PWHIsControlVisible(hwnd)
{
   ControlGet, style, Style, , , ahk_id %hwnd%
   return style and (style & WS_VISIBLE) != 0
}

PWHInvalidateRect(hwnd, x, y, w, h, erase := 1)
{
   VarSetCapacity(rect, 16)
   NumPut(x, rect, 0)
   NumPut(y, rect, 4)
   NumPut(x + w, rect, 8)
   NumPut(y + h, rect, 12)
   DllCall("InvalidateRect", "Ptr", hwnd, "Ptr", &rect, "Int", erase)
}

PWHGetMouseMonitor()
{
   CoordMode, Mouse, Screen
   MouseGetPos, mouseX, mouseY

   return DllCall("MonitorFromPoint", "Int", mouseX, "Int", mouseY, "UInt", MONITOR_DEFAULTTONEAREST)
}

PWHGetMonitorInfo(hMonitor)
{
   ; This code is based on MDMF_GetInfo by "just me".
   ; https://www.autohotkey.com/boards/viewtopic.php?t=4606

   NumPut(VarSetCapacity(mi, 40), mi, 0, "UInt")
   DllCall("GetMonitorInfo", "Ptr", hMonitor, "Ptr", &mi)

   return { left:       NumGet(mi,  4, "Int")
          , top:        NumGet(mi,  8, "Int")
          , right:      NumGet(mi, 12, "Int")
          , bottom:     NumGet(mi, 16, "Int")
          , workLeft:   NumGet(mi, 20, "Int")
          , workTop:    NumGet(mi, 24, "Int")
          , workRight:  NumGet(mi, 28, "Int")
          , workBottom: NumGet(mi, 32, "Int")
          , isPrimary:  NumGet(mi, 36, "UInt") }
}

PWHClone(obj)
{
   ; This code is based on ObjFullyClone by "SpeedMaster".
   ; https://www.autohotkey.com/boards/viewtopic.php?p=283227

   copy := obj.Clone()

   for k, v in copy
   {
      if(IsObject(v))
         copy[k] := PWHClone(v)
   }

   return copy
}

PWHSort(arr, compareFn := 0)
{
   ; Insertion sort is simple to implement and its performance characteristics make it a good choice
   ; for this use case. See https://en.wikipedia.org/wiki/Insertion_sort#Algorithm

   ; compareFn must be a reference to a function that returns true if a < b.
   ; If no compareFn is passed in, the array entries are compared via the built-in < operator.

   i := 2
   while i <= arr.Length()
   {
      x := arr[i]
      j := i - 1
      while j >= 1 and (compareFn ? %compareFn%(x, arr[j]) : x < arr[j])
      {
         arr[j + 1] := arr[j]
         j--
      }
      arr[j + 1] := x
      i++
   }
}

PWHCompareSortKey(a, b)
{
   ; Ascending sort by sortKey, using hwnd as a tie-breaker. (We only need a tie-breaker in the rare
   ; case where two tracks have the same name and plug-in, and in that case there is no "right"
   ; order; we only need their order to be stable.) We use StrCmpLogicalW so that track names that
   ; start with a number (as in Ableton) will sort numerically.
   strCmp := DllCall("shlwapi\StrCmpLogicalW", "Str", a.sortKey, "Str", b.sortKey, "Int")
   return (strCmp = 0) ? (a.hwnd < b.hwnd) : strCmp < 0
}

PWHCompareZOrderDesc(a, b)
{
   ; Descending sort by z-order.
   return a.zOrder >= b.zOrder
}