diff --git a/AutoHotkey64.exe b/AutoHotkey64.exe index d38aecc..50822a4 100644 Binary files a/AutoHotkey64.exe and b/AutoHotkey64.exe differ diff --git a/UX/Templates/Minimal for v2.ahk b/UX/Templates/Minimal for v2.ahk index c59bfef..93ca0cf 100644 --- a/UX/Templates/Minimal for v2.ahk +++ b/UX/Templates/Minimal for v2.ahk @@ -1,6 +1,6 @@ -/* -[NewScriptTemplate] -Description = Just #Requires v2.0 -*/ -#Requires AutoHotkey v2.0 - +/* +[NewScriptTemplate] +Description = Just #Requires v2.0 +*/ +#Requires AutoHotkey v2.0 + diff --git a/UX/WindowSpy.ahk b/UX/WindowSpy.ahk index c3923ce..9b918e3 100644 --- a/UX/WindowSpy.ahk +++ b/UX/WindowSpy.ahk @@ -1,244 +1,244 @@ -; -; Window Spy for AHKv2 -; - -#Requires AutoHotkey v2.0 - -#NoTrayIcon -#SingleInstance Ignore -SetWorkingDir A_ScriptDir -CoordMode "Pixel", "Screen" - -Global oGui - -WinSpyGui() - -WinSpyGui() { - Global oGui - - try TraySetIcon "inc\spy.ico" - DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy") - - oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale","Window Spy for AHKv2") - oGui.OnEvent("Close",WinSpyClose) - oGui.OnEvent("Size",WinSpySize) - - oGui.Add("Text",,"Window Title, Class and Process:") - oGui.Add("Checkbox","yp xp+200 w120 Right vCtrl_FollowMouse","Follow Mouse").Value := 1 - oGui.Add("Edit","xm w320 r5 ReadOnly -Wrap vCtrl_Title") - oGui.Add("Text",,"Mouse Position") - oGui.Add("Edit","w320 r4 ReadOnly vCtrl_MousePos") - oGui.Add("Text","w320 vCtrl_CtrlLabel",(txtFocusCtrl := "Focused Control") ":") - oGui.Add("Edit","w320 r4 ReadOnly vCtrl_Ctrl") - oGui.Add("Text",,"Active Window Postition:") - oGui.Add("Edit","w320 r2 ReadOnly vCtrl_Pos") - oGui.Add("Text",,"Status Bar Text:") - oGui.Add("Edit","w320 r2 ReadOnly vCtrl_SBText") - oGui.Add("Checkbox","vCtrl_IsSlow","Slow TitleMatchMode") - oGui.Add("Text",,"Visible Text:") - oGui.Add("Edit","w320 r2 ReadOnly vCtrl_VisText") - oGui.Add("Text",,"All Text:") - oGui.Add("Edit","w320 r2 ReadOnly vCtrl_AllText") - oGui.Add("Text","w320 r1 vCtrl_Freeze",(txtNotFrozen := "(Hold Ctrl or Shift to suspend updates)")) - - oGui.Show("NoActivate") - WinGetClientPos(&x_temp, &y_temp2,,,"ahk_id " oGui.hwnd) - - ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX - - oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use - oGui.txtFrozen := "(Updates suspended)" - oGui.txtMouseCtrl := "Control Under Mouse Position" - oGui.txtFocusCtrl := txtFocusCtrl - - SetTimer Update, 250 -} - -WinSpySize(GuiObj, MinMax, Width, Height) { - Global oGui - - If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is - return - - SetTimer Update, (MinMax=0)?250:0 ; suspend updates on minimize - - ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin - list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze" - Loop Parse list, "," - oGui["Ctrl_" A_LoopField].Move(,,ctrlW) -} - -WinSpyClose(GuiObj) { - ExitApp -} - -Update() { ; timer, no params - Try TryUpdate() ; Try -} - -TryUpdate() { - Global oGui - - If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is - return - - Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value - CoordMode "Mouse", "Screen" - MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow - actWin := WinExist("A") - - if (Ctrl_FollowMouse) { - curWin := msWin, curCtrl := msCtrl - WinExist("ahk_id " curWin) ; updating LastWindowFound? - } else { - curWin := actWin - curCtrl := ControlGetFocus() ; get focused control hwnd from active win - } - curCtrlClassNN := "" - Try curCtrlClassNN := ControlGetClassNN(curCtrl) - - t1 := WinGetTitle(), t2 := WinGetClass() - if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab - UpdateText("Ctrl_Freeze", oGui.txtFrozen) - return - } - - UpdateText("Ctrl_Freeze", oGui.txtNotFrozen) - t3 := WinGetProcessName(), t4 := WinGetPID() - - WinDataText := t1 "`n" ; ZZZ - . "ahk_class " t2 "`n" - . "ahk_exe " t3 "`n" - . "ahk_pid " t4 "`n" - . "ahk_id " curWin - - UpdateText("Ctrl_Title", WinDataText) - CoordMode "Mouse", "Window" - MouseGetPos &mrX, &mrY - CoordMode "Mouse", "Client" - MouseGetPos &mcX, &mcY - mClr := PixelGetColor(msX,msY,"RGB") - mClr := SubStr(mClr, 3) - - mpText := "Screen:`t" msX ", " msY "`n" - . "Window:`t" mrX ", " mrY "`n" - . "Client:`t" mcX ", " mcY " (default)`n" - . "Color:`t" mClr " (Red=" SubStr(mClr, 1, 2) " Green=" SubStr(mClr, 3, 2) " Blue=" SubStr(mClr, 5) ")" - - UpdateText("Ctrl_MousePos", mpText) - - UpdateText("Ctrl_CtrlLabel", (Ctrl_FollowMouse ? oGui.txtMouseCtrl : oGui.txtFocusCtrl) ":") - - if (curCtrl) { - ctrlTxt := ControlGetText(curCtrl) - WinGetClientPos(&sX, &sY, &sW, &sH, curCtrl) - ControlGetPos &cX, &cY, &cW, &cH, curCtrl - - cText := "ClassNN:`t" curCtrlClassNN "`n" - . "Text:`t" textMangle(ctrlTxt) "`n" - . "Screen:`tx: " sX "`ty: " sY "`tw: " sW "`th: " sH "`n" - . "Client`tx: " cX "`ty: " cY "`tw: " cW "`th: " cH - } else - cText := "" - - UpdateText("Ctrl_Ctrl", cText) - wX := "", wY := "", wW := "", wH := "" - WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin - WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin) - - wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n" - . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH - - UpdateText("Ctrl_Pos", wText) - sbTxt := "" - - Loop { - ovi := "" - Try ovi := StatusBarGetText(A_Index) - if (ovi = "") - break - sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n" - } - - sbTxt := SubStr(sbTxt,1,-1) ; StringTrimRight, sbTxt, sbTxt, 1 - UpdateText("Ctrl_SBText", sbTxt) - bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow - - if (bSlow) { - DetectHiddenText False - ovVisText := WinGetText() ; WinGetText, ovVisText - DetectHiddenText True - ovAllText := WinGetText() ; WinGetText, ovAllText - } else { - ovVisText := WinGetTextFast(false) - ovAllText := WinGetTextFast(true) - } - - UpdateText("Ctrl_VisText", ovVisText) - UpdateText("Ctrl_AllText", ovAllText) -} - -; =========================================================================================== -; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects -; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used -; to retrieve the text of each control. -; =========================================================================================== -WinGetTextFast(detect_hidden) { - controls := WinGetControlsHwnd() - - static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source. - - buf := Buffer(WINDOW_TEXT_SIZE * 2,0) - - text := "" - - Loop controls.Length { - hCtl := controls[A_Index] - if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl) - continue - if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE) - continue - - text .= StrGet(buf) "`r`n" ; text .= buf "`r`n" - } - return text -} - -; =========================================================================================== -; Unlike using a pure GuiControl, this function causes the text of the -; controls to be updated only when the text has changed, preventing periodic -; flickering (especially on older systems). -; =========================================================================================== -UpdateText(vCtl, NewText) { - Global oGui - static OldText := {} - ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd) - - if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) { - ctl.Value := NewText - OldText.%hCtl% := NewText - } -} - -textMangle(x) { - elli := false - if (pos := InStr(x, "`n")) - x := SubStr(x, 1, pos-1), elli := true - else if (StrLen(x) > 40) - x := SubStr(x,1,40), elli := true - if elli - x .= " (...)" - return x -} - -suspend_timer() { - Global oGui - SetTimer Update, 0 - UpdateText("Ctrl_Freeze", oGui.txtFrozen) -} - -~*Shift:: -~*Ctrl::suspend_timer() - -~*Ctrl up:: -~*Shift up::SetTimer Update, 250 +; +; Window Spy for AHKv2 +; + +#Requires AutoHotkey v2.0 + +#NoTrayIcon +#SingleInstance Ignore +SetWorkingDir A_ScriptDir +CoordMode "Pixel", "Screen" + +Global oGui + +WinSpyGui() + +WinSpyGui() { + Global oGui + + try TraySetIcon "inc\spy.ico" + DllCall("shell32\SetCurrentProcessExplicitAppUserModelID", "wstr", "AutoHotkey.WindowSpy") + + oGui := Gui("AlwaysOnTop Resize MinSize +DPIScale","Window Spy for AHKv2") + oGui.OnEvent("Close",WinSpyClose) + oGui.OnEvent("Size",WinSpySize) + + oGui.Add("Text",,"Window Title, Class and Process:") + oGui.Add("Checkbox","yp xp+200 w120 Right vCtrl_FollowMouse","Follow Mouse").Value := 1 + oGui.Add("Edit","xm w320 r5 ReadOnly -Wrap vCtrl_Title") + oGui.Add("Text",,"Mouse Position") + oGui.Add("Edit","w320 r4 ReadOnly vCtrl_MousePos") + oGui.Add("Text","w320 vCtrl_CtrlLabel",(txtFocusCtrl := "Focused Control") ":") + oGui.Add("Edit","w320 r4 ReadOnly vCtrl_Ctrl") + oGui.Add("Text",,"Active Window Postition:") + oGui.Add("Edit","w320 r2 ReadOnly vCtrl_Pos") + oGui.Add("Text",,"Status Bar Text:") + oGui.Add("Edit","w320 r2 ReadOnly vCtrl_SBText") + oGui.Add("Checkbox","vCtrl_IsSlow","Slow TitleMatchMode") + oGui.Add("Text",,"Visible Text:") + oGui.Add("Edit","w320 r2 ReadOnly vCtrl_VisText") + oGui.Add("Text",,"All Text:") + oGui.Add("Edit","w320 r2 ReadOnly vCtrl_AllText") + oGui.Add("Text","w320 r1 vCtrl_Freeze",(txtNotFrozen := "(Hold Ctrl or Shift to suspend updates)")) + + oGui.Show("NoActivate") + WinGetClientPos(&x_temp, &y_temp2,,,"ahk_id " oGui.hwnd) + + ; oGui.horzMargin := x_temp*96//A_ScreenDPI - 320 ; now using oGui.MarginX + + oGui.txtNotFrozen := txtNotFrozen ; create properties for futur use + oGui.txtFrozen := "(Updates suspended)" + oGui.txtMouseCtrl := "Control Under Mouse Position" + oGui.txtFocusCtrl := txtFocusCtrl + + SetTimer Update, 250 +} + +WinSpySize(GuiObj, MinMax, Width, Height) { + Global oGui + + If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is + return + + SetTimer Update, (MinMax=0)?250:0 ; suspend updates on minimize + + ctrlW := Width - (oGui.MarginX * 2) ; ctrlW := Width - horzMargin + list := "Title,MousePos,Ctrl,Pos,SBText,VisText,AllText,Freeze" + Loop Parse list, "," + oGui["Ctrl_" A_LoopField].Move(,,ctrlW) +} + +WinSpyClose(GuiObj) { + ExitApp +} + +Update() { ; timer, no params + Try TryUpdate() ; Try +} + +TryUpdate() { + Global oGui + + If !oGui.HasProp("txtNotFrozen") ; WinSpyGui() not done yet, return until it is + return + + Ctrl_FollowMouse := oGui["Ctrl_FollowMouse"].Value + CoordMode "Mouse", "Screen" + MouseGetPos &msX, &msY, &msWin, &msCtrl, 2 ; get ClassNN and hWindow + actWin := WinExist("A") + + if (Ctrl_FollowMouse) { + curWin := msWin, curCtrl := msCtrl + WinExist("ahk_id " curWin) ; updating LastWindowFound? + } else { + curWin := actWin + curCtrl := ControlGetFocus() ; get focused control hwnd from active win + } + curCtrlClassNN := "" + Try curCtrlClassNN := ControlGetClassNN(curCtrl) + + t1 := WinGetTitle(), t2 := WinGetClass() + if (curWin = oGui.hwnd || t2 = "MultitaskingViewFrame") { ; Our Gui || Alt-tab + UpdateText("Ctrl_Freeze", oGui.txtFrozen) + return + } + + UpdateText("Ctrl_Freeze", oGui.txtNotFrozen) + t3 := WinGetProcessName(), t4 := WinGetPID() + + WinDataText := t1 "`n" ; ZZZ + . "ahk_class " t2 "`n" + . "ahk_exe " t3 "`n" + . "ahk_pid " t4 "`n" + . "ahk_id " curWin + + UpdateText("Ctrl_Title", WinDataText) + CoordMode "Mouse", "Window" + MouseGetPos &mrX, &mrY + CoordMode "Mouse", "Client" + MouseGetPos &mcX, &mcY + mClr := PixelGetColor(msX,msY,"RGB") + mClr := SubStr(mClr, 3) + + mpText := "Screen:`t" msX ", " msY "`n" + . "Window:`t" mrX ", " mrY "`n" + . "Client:`t" mcX ", " mcY " (default)`n" + . "Color:`t" mClr " (Red=" SubStr(mClr, 1, 2) " Green=" SubStr(mClr, 3, 2) " Blue=" SubStr(mClr, 5) ")" + + UpdateText("Ctrl_MousePos", mpText) + + UpdateText("Ctrl_CtrlLabel", (Ctrl_FollowMouse ? oGui.txtMouseCtrl : oGui.txtFocusCtrl) ":") + + if (curCtrl) { + ctrlTxt := ControlGetText(curCtrl) + WinGetClientPos(&sX, &sY, &sW, &sH, curCtrl) + ControlGetPos &cX, &cY, &cW, &cH, curCtrl + + cText := "ClassNN:`t" curCtrlClassNN "`n" + . "Text:`t" textMangle(ctrlTxt) "`n" + . "Screen:`tx: " sX "`ty: " sY "`tw: " sW "`th: " sH "`n" + . "Client`tx: " cX "`ty: " cY "`tw: " cW "`th: " cH + } else + cText := "" + + UpdateText("Ctrl_Ctrl", cText) + wX := "", wY := "", wW := "", wH := "" + WinGetPos &wX, &wY, &wW, &wH, "ahk_id " curWin + WinGetClientPos(&wcX, &wcY, &wcW, &wcH, "ahk_id " curWin) + + wText := "Screen:`tx: " wX "`ty: " wY "`tw: " wW "`th: " wH "`n" + . "Client:`tx: " wcX "`ty: " wcY "`tw: " wcW "`th: " wcH + + UpdateText("Ctrl_Pos", wText) + sbTxt := "" + + Loop { + ovi := "" + Try ovi := StatusBarGetText(A_Index) + if (ovi = "") + break + sbTxt .= "(" A_Index "):`t" textMangle(ovi) "`n" + } + + sbTxt := SubStr(sbTxt,1,-1) ; StringTrimRight, sbTxt, sbTxt, 1 + UpdateText("Ctrl_SBText", sbTxt) + bSlow := oGui["Ctrl_IsSlow"].Value ; GuiControlGet, bSlow,, Ctrl_IsSlow + + if (bSlow) { + DetectHiddenText False + ovVisText := WinGetText() ; WinGetText, ovVisText + DetectHiddenText True + ovAllText := WinGetText() ; WinGetText, ovAllText + } else { + ovVisText := WinGetTextFast(false) + ovAllText := WinGetTextFast(true) + } + + UpdateText("Ctrl_VisText", ovVisText) + UpdateText("Ctrl_AllText", ovAllText) +} + +; =========================================================================================== +; WinGetText ALWAYS uses the "slow" mode - TitleMatchMode only affects +; WinText/ExcludeText parameters. In "fast" mode, GetWindowText() is used +; to retrieve the text of each control. +; =========================================================================================== +WinGetTextFast(detect_hidden) { + controls := WinGetControlsHwnd() + + static WINDOW_TEXT_SIZE := 32767 ; Defined in AutoHotkey source. + + buf := Buffer(WINDOW_TEXT_SIZE * 2,0) + + text := "" + + Loop controls.Length { + hCtl := controls[A_Index] + if !detect_hidden && !DllCall("IsWindowVisible", "ptr", hCtl) + continue + if !DllCall("GetWindowText", "ptr", hCtl, "Ptr", buf.ptr, "int", WINDOW_TEXT_SIZE) + continue + + text .= StrGet(buf) "`r`n" ; text .= buf "`r`n" + } + return text +} + +; =========================================================================================== +; Unlike using a pure GuiControl, this function causes the text of the +; controls to be updated only when the text has changed, preventing periodic +; flickering (especially on older systems). +; =========================================================================================== +UpdateText(vCtl, NewText) { + Global oGui + static OldText := {} + ctl := oGui[vCtl], hCtl := Integer(ctl.hwnd) + + if (!oldText.HasProp(hCtl) Or OldText.%hCtl% != NewText) { + ctl.Value := NewText + OldText.%hCtl% := NewText + } +} + +textMangle(x) { + elli := false + if (pos := InStr(x, "`n")) + x := SubStr(x, 1, pos-1), elli := true + else if (StrLen(x) > 40) + x := SubStr(x,1,40), elli := true + if elli + x .= " (...)" + return x +} + +suspend_timer() { + Global oGui + SetTimer Update, 0 + UpdateText("Ctrl_Freeze", oGui.txtFrozen) +} + +~*Shift:: +~*Ctrl::suspend_timer() + +~*Ctrl up:: +~*Shift up::SetTimer Update, 250 diff --git a/UX/inc/CommandLineToArgs.ahk b/UX/inc/CommandLineToArgs.ahk index 1083f27..209f961 100644 --- a/UX/inc/CommandLineToArgs.ahk +++ b/UX/inc/CommandLineToArgs.ahk @@ -1,12 +1,12 @@ - -CommandLineToArgs(cmd) { - argv := DllCall("shell32\CommandLineToArgvW", "wstr", cmd, 'int*', &narg:=0, "ptr") - try { - args := [] - Loop args.Capacity := narg - args.Push(StrGet(NumGet(argv, (A_Index-1)*A_PtrSize, "ptr"), "UTF-16")) - } - finally - DllCall("LocalFree", "ptr", argv) - return args -} + +CommandLineToArgs(cmd) { + argv := DllCall("shell32\CommandLineToArgvW", "wstr", cmd, 'int*', &narg:=0, "ptr") + try { + args := [] + Loop args.Capacity := narg + args.Push(StrGet(NumGet(argv, (A_Index-1)*A_PtrSize, "ptr"), "UTF-16")) + } + finally + DllCall("LocalFree", "ptr", argv) + return args +} diff --git a/UX/inc/CreateAppShortcut.ahk b/UX/inc/CreateAppShortcut.ahk index 9d0656f..1492a94 100644 --- a/UX/inc/CreateAppShortcut.ahk +++ b/UX/inc/CreateAppShortcut.ahk @@ -1,37 +1,37 @@ -CreateAppShortcut(linkFile, p) { - ;target, args, description, aumid, uninst? - lnk := ComObject('{00021401-0000-0000-C000-000000000046}' ; CLSID_ShellLink - ,'{000214F9-0000-0000-C000-000000000046}') ; IID_IShellLink - - ComCall(20, lnk, 'wstr', p.target) - ComCall(11, lnk, 'wstr', p.HasProp('args') ? p.args : "") - ComCall(7, lnk, 'wstr', p.desc) - if p.HasProp('icon') - ComCall(17, lnk, 'wstr', p.icon, 'int', p.HasProp('iconIndex') ? p.iconIndex : 0) - - ; Set the System.AppUserModel.ID property via IPropertyStore - props := ComObjQuery(lnk, '{886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99}') - static PKEY_AppUserModel_ID := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 5) - static PKEY_AppUserModel_UninstallCommand := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 37) - setProp PKEY_AppUserModel_ID, p.aumid - if p.HasProp('uninst') - setProp PKEY_AppUserModel_UninstallCommand, p.uninst - - ; Save via IPersistFile - pf := ComObjQuery(lnk, '{0000010B-0000-0000-C000-000000000046}') - ComCall(6, pf, 'wstr', linkFile, 'int', true) - - setProp(key, value) { - propvar := Buffer(24, 0), propref := ComValue(0x400C, propvar.ptr) - propref[] := String(value) - ComCall(6, props, 'ptr', key, 'ptr', propvar) - propref[] := 0 - } - - PKEY(sguid, propID) { - pk := Buffer(20) - DllCall('ole32\IIDFromString', 'wstr', sguid, 'ptr', pk, 'hresult') - NumPut('int', propID, pk, 16) - return pk - } -} +CreateAppShortcut(linkFile, p) { + ;target, args, description, aumid, uninst? + lnk := ComObject('{00021401-0000-0000-C000-000000000046}' ; CLSID_ShellLink + ,'{000214F9-0000-0000-C000-000000000046}') ; IID_IShellLink + + ComCall(20, lnk, 'wstr', p.target) + ComCall(11, lnk, 'wstr', p.HasProp('args') ? p.args : "") + ComCall(7, lnk, 'wstr', p.desc) + if p.HasProp('icon') + ComCall(17, lnk, 'wstr', p.icon, 'int', p.HasProp('iconIndex') ? p.iconIndex : 0) + + ; Set the System.AppUserModel.ID property via IPropertyStore + props := ComObjQuery(lnk, '{886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99}') + static PKEY_AppUserModel_ID := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 5) + static PKEY_AppUserModel_UninstallCommand := PKEY('{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}', 37) + setProp PKEY_AppUserModel_ID, p.aumid + if p.HasProp('uninst') + setProp PKEY_AppUserModel_UninstallCommand, p.uninst + + ; Save via IPersistFile + pf := ComObjQuery(lnk, '{0000010B-0000-0000-C000-000000000046}') + ComCall(6, pf, 'wstr', linkFile, 'int', true) + + setProp(key, value) { + propvar := Buffer(24, 0), propref := ComValue(0x400C, propvar.ptr) + propref[] := String(value) + ComCall(6, props, 'ptr', key, 'ptr', propvar) + propref[] := 0 + } + + PKEY(sguid, propID) { + pk := Buffer(20) + DllCall('ole32\IIDFromString', 'wstr', sguid, 'ptr', pk, 'hresult') + NumPut('int', propID, pk, 16) + return pk + } +} diff --git a/UX/inc/EnableUIAccess.ahk b/UX/inc/EnableUIAccess.ahk index 8c6d2c7..79e6617 100644 --- a/UX/inc/EnableUIAccess.ahk +++ b/UX/inc/EnableUIAccess.ahk @@ -1,191 +1,212 @@ -EnableUIAccess(ExePath) { - static CertName := "AutoHotkey" - hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W - , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE - , "wstr", "Root", "ptr") - if !hStore - throw OSError() - store := CertStore(hStore) - ; Find or create certificate for signing. - cert := CertContext() - while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore - , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING - , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR - , "wstr", CertName, "ptr", cert.ptr, "ptr")) - && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey" - , "ptr", cert, "uint", 1 ; CRYPT_ACQUIRE_CACHE_FLAG - , "ptr", 0, "ptr*", 0, "uint*", &keySpec:=0, "ptr", 0) - && (keySpec & 2)) { ; AT_SIGNATURE - ; Keep looking for a certificate with a private key. - } - if !cert.ptr - cert := EnableUIAccess_CreateCert(CertName, hStore) - ; Set uiAccess attribute in manifest. - EnableUIAccess_SetManifest(ExePath) - ; Sign the file (otherwise uiAccess attribute is ignored). - EnableUIAccess_SignFile(ExePath, cert, CertName) -} - -EnableUIAccess_SetManifest(ExePath) { - xml := ComObject("Msxml2.DOMDocument") - xml.async := false - xml.setProperty("SelectionLanguage", "XPath") - xml.setProperty("SelectionNamespaces" - , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' " - . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'") - if !xml.load("res://" ExePath "/#24/#1") ; Load current manifest - throw Error("File or manifest not found",, ExePath) - - node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security" - . "/v3:requestedPrivileges/v3:requestedExecutionLevel") - if !node ; Not AutoHotkey? - throw Error("Manifest is missing required elements") - - node.setAttribute("uiAccess", "true") - xml := RTrim(xml.xml, "`r`n") - - data := Buffer(StrPut(xml, "utf-8") - 1) - StrPut(xml, data, "utf-8") - - if !(hupd := DllCall("BeginUpdateResource", "str", ExePath, "int", false)) - throw OSError() - r := DllCall("UpdateResource", "ptr", hupd, "ptr", 24, "ptr", 1 - , "ushort", 1033, "ptr", data, "uint", data.size) - - ; Retry loop to work around file locks (especially by antivirus) - for delay in [0, 100, 500, 1000, 3500] { - Sleep delay - if DllCall("EndUpdateResource", "ptr", hupd, "int", !r) || !r - return - if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED - break - } - throw OSError(A_LastError, "EndUpdateResource") -} - -EnableUIAccess_CreateCert(Name, hStore) { - ; Here Name is used as the key container name. - prov := CryptContext() - if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov - , "str", Name, "ptr", 0, "uint", 1, "uint", 0) { ; PROV_RSA_FULL=1, open existing=0 - if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov - , "str", Name, "ptr", 0, "uint", 1, "uint", 8) ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8 - throw OSError() - if !DllCall("Advapi32\CryptGenKey", "ptr", prov - , "uint", 2, "uint", 0x4000001, "ptr*", CryptKey()) ; AT_SIGNATURE=2, EXPORTABLE=..01 - throw OSError() - } - - ; Here Name is used as the certificate subject and name. - Loop 2 { - if A_Index = 1 - pbName := cbName := 0 - else - bName := Buffer(cbName), pbName := bName.ptr - if !DllCall("Crypt32\CertStrToName", "uint", 1, "str", "CN=" Name - , "uint", 3, "ptr", 0, "ptr", pbName, "uint*", &cbName, "ptr", 0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3 - throw OSError() - } - cnb := Buffer(2*A_PtrSize), NumPut("ptr", cbName, "ptr", pbName, cnb) - - endTime := Buffer(16) - DllCall("GetSystemTime", "ptr", endTime) - NumPut("ushort", NumGet(endTime, "ushort") + 10, endTime) ; += 10 years - - if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate" - , "ptr", prov, "ptr", cnb, "uint", 0, "ptr", 0 - , "ptr", 0, "ptr", 0, "ptr", endTime, "ptr", 0, "ptr") - throw OSError() - cert := CertContext(hCert) - - if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr", hStore - , "ptr", hCert, "uint", 1, "ptr", 0) ; STORE_ADD_NEW=1 - throw OSError() - - return cert -} - -EnableUIAccess_DeleteCertAndKey(Name) { - ; This first call "acquires" the key container but also deletes it. - DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str", Name - , "ptr", 0, "uint", 1, "uint", 16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16 - if !hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W - , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE - , "wstr", "Root", "ptr") - throw OSError() - store := CertStore(hStore) - deleted := 0 - ; Multiple certificates might be created over time as keys become inaccessible. - while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore - , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING - , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR - , "wstr", Name, "ptr", 0, "ptr") { - if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr", p) - throw OSError() - deleted++ - } - return deleted -} - -class CryptPtrBase { - __new(p:=0) => this.ptr := p - __delete() => this.ptr && this.Dispose() -} -class CryptContext extends CryptPtrBase { - Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr", this, "uint", 0) -} -class CertContext extends CryptPtrBase { - Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr", this) -} -class CertStore extends CryptPtrBase { - Dispose() => DllCall("Crypt32\CertCloseStore", "ptr", this, "uint", 0) -} -class CryptKey extends CryptPtrBase { - Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr", this) -} - -EnableUIAccess_SignFile(ExePath, CertCtx, Name) { - file_info := struct( ; SIGNER_FILE_INFO - "ptr", A_PtrSize*3, "ptr", StrPtr(ExePath)) - dwIndex := Buffer(4, 0) ; DWORD - subject_info := struct( ; SIGNER_SUBJECT_INFO - "ptr", A_PtrSize*4, "ptr", dwIndex.ptr, "ptr", SIGNER_SUBJECT_FILE:=1, - "ptr", file_info.ptr) - cert_store_info := struct( ; SIGNER_CERT_STORE_INFO - "ptr", A_PtrSize*4, "ptr", CertCtx.ptr, "ptr", SIGNER_CERT_POLICY_CHAIN:=2) - cert_info := struct( ; SIGNER_CERT - "uint", 8+A_PtrSize*2, "uint", SIGNER_CERT_STORE:=2, - "ptr", cert_store_info.ptr) - authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE - "uint", 8+A_PtrSize*3, "int", false, "ptr", true, "ptr", StrPtr(Name)) - sig_info := struct( ; SIGNER_SIGNATURE_INFO - "uint", 8+A_PtrSize*4, "uint", CALG_SHA1:=0x8004, - "ptr", SIGNER_AUTHCODE_ATTR:=1, "ptr", authcode_attr.ptr) - - DllCall("MSSign32\SignerSign" - , "ptr", subject_info, "ptr", cert_info, "ptr", sig_info - , "ptr", 0, "ptr", 0, "ptr", 0, "ptr", 0, "hresult") - - struct(args*) => ( - args.Push(b := Buffer(args[2], 0)), - NumPut(args*), - b - ) -} - -; Verifies a signed executable file. Returns 0 on success, or a standard OS error number. -EnableUIAccess_Verify(ExePath) { - wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO - NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi) - - ; WINTRUST_ACTION_GENERIC_VERIFY_V2 - NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16)) - - wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA - NumPut( - 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0, - 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1, - 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd - ) - return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int') -} +EnableUIAccess(ExePath) { + static CertName := "AutoHotkey" + hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W + , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE + , "wstr", "Root", "ptr") + if !hStore + throw OSError() + store := CertStore(hStore) + ; Find or create certificate for signing. + cert := CertContext() + while (cert.ptr := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore + , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING + , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR + , "wstr", CertName, "ptr", cert.ptr, "ptr")) + && !(DllCall("Crypt32\CryptAcquireCertificatePrivateKey" + , "ptr", cert, "uint", 5 ; CRYPT_ACQUIRE_CACHE_FLAG|CRYPT_ACQUIRE_COMPARE_KEY_FLAG + , "ptr", 0, "ptr*", 0, "uint*", &keySpec:=0, "ptr", 0) + && (keySpec & 2)) { ; AT_SIGNATURE + ; Keep looking for a certificate with a private key. + } + if !cert.ptr + cert := EnableUIAccess_CreateCert(CertName, hStore) + ; Set uiAccess attribute in manifest. + EnableUIAccess_SetManifest(ExePath) + ; Sign the file (otherwise uiAccess attribute is ignored). + EnableUIAccess_SignFile(ExePath, cert, CertName) +} + +EnableUIAccess_SetManifest(ExePath) { + xml := ComObject("Msxml2.DOMDocument") + xml.async := false + xml.setProperty("SelectionLanguage", "XPath") + xml.setProperty("SelectionNamespaces" + , "xmlns:v1='urn:schemas-microsoft-com:asm.v1' " + . "xmlns:v3='urn:schemas-microsoft-com:asm.v3'") + try + if !xml.loadXML(EnableUIAccess_ReadManifest(ExePath)) + throw Error("Invalid manifest") + catch as e + throw Error("Error loading manifest from " ExePath,, e.Message "`n @ " e.File ":" e.Line) + + + node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security" + . "/v3:requestedPrivileges/v3:requestedExecutionLevel") + if !node ; Not AutoHotkey? + throw Error("Manifest is missing required elements") + + node.setAttribute("uiAccess", "true") + xml := RTrim(xml.xml, "`r`n") + + data := Buffer(StrPut(xml, "utf-8") - 1) + StrPut(xml, data, "utf-8") + + if !(hupd := DllCall("BeginUpdateResource", "str", ExePath, "int", false)) + throw OSError() + r := DllCall("UpdateResource", "ptr", hupd, "ptr", 24, "ptr", 1 + , "ushort", 1033, "ptr", data, "uint", data.size) + + ; Retry loop to work around file locks (especially by antivirus) + for delay in [0, 100, 500, 1000, 3500] { + Sleep delay + if DllCall("EndUpdateResource", "ptr", hupd, "int", !r) || !r + return + if !(A_LastError = 5 || A_LastError = 110) ; ERROR_ACCESS_DENIED || ERROR_OPEN_FAILED + break + } + throw OSError(A_LastError, "EndUpdateResource") +} + +EnableUIAccess_ReadManifest(ExePath) { + if !(hmod := DllCall("LoadLibraryEx", "str", ExePath, "ptr", 0, "uint", 2, "ptr")) + throw OSError() + try { + if !(hres := DllCall("FindResource", "ptr", hmod, "ptr", 1, "ptr", 24, "ptr")) + throw OSError() + size := DllCall("SizeofResource", "ptr", hmod, "ptr", hres, "uint") + if !(hglb := DllCall("LoadResource", "ptr", hmod, "ptr", hres, "ptr")) + throw OSError() + if !(pres := DllCall("LockResource", "ptr", hglb, "ptr")) + throw OSError() + return StrGet(pres, size, "utf-8") + } + finally + DllCall("FreeLibrary", "ptr", hmod) +} + +EnableUIAccess_CreateCert(Name, hStore) { + ; Here Name is used as the key container name. + prov := CryptContext() + if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov + , "str", Name, "ptr", 0, "uint", 1, "uint", 0) { ; PROV_RSA_FULL=1, open existing=0 + if !DllCall("Advapi32\CryptAcquireContext", "ptr*", prov + , "str", Name, "ptr", 0, "uint", 1, "uint", 8) ; PROV_RSA_FULL=1, CRYPT_NEWKEYSET=8 + throw OSError() + if !DllCall("Advapi32\CryptGenKey", "ptr", prov + , "uint", 2, "uint", 0x4000001, "ptr*", CryptKey()) ; AT_SIGNATURE=2, EXPORTABLE=..01 + throw OSError() + } + + ; Here Name is used as the certificate subject and name. + Loop 2 { + if A_Index = 1 + pbName := cbName := 0 + else + bName := Buffer(cbName), pbName := bName.ptr + if !DllCall("Crypt32\CertStrToName", "uint", 1, "str", "CN=" Name + , "uint", 3, "ptr", 0, "ptr", pbName, "uint*", &cbName, "ptr", 0) ; X509_ASN_ENCODING=1, CERT_X500_NAME_STR=3 + throw OSError() + } + cnb := Buffer(2*A_PtrSize), NumPut("ptr", cbName, "ptr", pbName, cnb) + + endTime := Buffer(16) + DllCall("GetSystemTime", "ptr", endTime) + NumPut("ushort", NumGet(endTime, "ushort") + 10, endTime) ; += 10 years + + if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate" + , "ptr", prov, "ptr", cnb, "uint", 0, "ptr", 0 + , "ptr", 0, "ptr", 0, "ptr", endTime, "ptr", 0, "ptr") + throw OSError() + cert := CertContext(hCert) + + if !DllCall("Crypt32\CertAddCertificateContextToStore", "ptr", hStore + , "ptr", hCert, "uint", 1, "ptr", 0) ; STORE_ADD_NEW=1 + throw OSError() + + return cert +} + +EnableUIAccess_DeleteCertAndKey(Name) { + ; This first call "acquires" the key container but also deletes it. + DllCall("Advapi32\CryptAcquireContext", "ptr*", 0, "str", Name + , "ptr", 0, "uint", 1, "uint", 16) ; PROV_RSA_FULL=1, CRYPT_DELETEKEYSET=16 + if !hStore := DllCall("Crypt32\CertOpenStore", "ptr", 10 ; STORE_PROV_SYSTEM_W + , "uint", 0, "ptr", 0, "uint", 0x20000 ; SYSTEM_STORE_LOCAL_MACHINE + , "wstr", "Root", "ptr") + throw OSError() + store := CertStore(hStore) + deleted := 0 + ; Multiple certificates might be created over time as keys become inaccessible. + while p := DllCall("Crypt32\CertFindCertificateInStore", "ptr", hStore + , "uint", 0x10001 ; X509_ASN_ENCODING|PKCS_7_ASN_ENCODING + , "uint", 0, "uint", 0x80007 ; FIND_SUBJECT_STR + , "wstr", Name, "ptr", 0, "ptr") { + if !DllCall("Crypt32\CertDeleteCertificateFromStore", "ptr", p) + throw OSError() + deleted++ + } + return deleted +} + +class CryptPtrBase { + __new(p:=0) => this.ptr := p + __delete() => this.ptr && this.Dispose() +} +class CryptContext extends CryptPtrBase { + Dispose() => DllCall("Advapi32\CryptReleaseContext", "ptr", this, "uint", 0) +} +class CertContext extends CryptPtrBase { + Dispose() => DllCall("Crypt32\CertFreeCertificateContext", "ptr", this) +} +class CertStore extends CryptPtrBase { + Dispose() => DllCall("Crypt32\CertCloseStore", "ptr", this, "uint", 0) +} +class CryptKey extends CryptPtrBase { + Dispose() => DllCall("Advapi32\CryptDestroyKey", "ptr", this) +} + +EnableUIAccess_SignFile(ExePath, CertCtx, Name) { + file_info := struct( ; SIGNER_FILE_INFO + "ptr", A_PtrSize*3, "ptr", StrPtr(ExePath)) + dwIndex := Buffer(4, 0) ; DWORD + subject_info := struct( ; SIGNER_SUBJECT_INFO + "ptr", A_PtrSize*4, "ptr", dwIndex.ptr, "ptr", SIGNER_SUBJECT_FILE:=1, + "ptr", file_info.ptr) + cert_store_info := struct( ; SIGNER_CERT_STORE_INFO + "ptr", A_PtrSize*4, "ptr", CertCtx.ptr, "ptr", SIGNER_CERT_POLICY_CHAIN:=2) + cert_info := struct( ; SIGNER_CERT + "uint", 8+A_PtrSize*2, "uint", SIGNER_CERT_STORE:=2, + "ptr", cert_store_info.ptr) + authcode_attr := struct( ; SIGNER_ATTR_AUTHCODE + "uint", 8+A_PtrSize*3, "int", false, "ptr", true, "ptr", StrPtr(Name)) + sig_info := struct( ; SIGNER_SIGNATURE_INFO + "uint", 8+A_PtrSize*4, "uint", CALG_SHA1:=0x8004, + "ptr", SIGNER_AUTHCODE_ATTR:=1, "ptr", authcode_attr.ptr) + + DllCall("MSSign32\SignerSign" + , "ptr", subject_info, "ptr", cert_info, "ptr", sig_info + , "ptr", 0, "ptr", 0, "ptr", 0, "ptr", 0, "hresult") + + struct(args*) => ( + args.Push(b := Buffer(args[2], 0)), + NumPut(args*), + b + ) +} + +; Verifies a signed executable file. Returns 0 on success, or a standard OS error number. +EnableUIAccess_Verify(ExePath) { + wfi := Buffer(4*A_PtrSize) ; WINTRUST_FILE_INFO + NumPut('ptr', wfi.size, 'ptr', StrPtr(ExePath), 'ptr', 0, 'ptr', 0, wfi) + + ; WINTRUST_ACTION_GENERIC_VERIFY_V2 + NumPut('int64', 0x11d0cd4400aac56b, 'int64', 0xee95c24fc000c28c, actionID := Buffer(16)) + + wtd := Buffer(9*A_PtrSize+16) ; WINTRUST_DATA + NumPut( + 'ptr', wtd.Size, 'ptr', 0, 'ptr', 0, 'int', WTD_UI_NONE:=2, 'int', WTD_REVOKE_NONE:=0, + 'ptr', WTD_CHOICE_FILE:=1, 'ptr', wfi.ptr, 'ptr', WTD_STATEACTION_VERIFY:=1, + 'ptr', 0, 'ptr', 0, 'int', 0, 'int', 0, 'ptr', 0, wtd + ) + return DllCall('wintrust\WinVerifyTrust', 'ptr', 0, 'ptr', actionID, 'ptr', wtd, 'int') +} diff --git a/UX/inc/GetGitHubReleaseAssetURL.ahk b/UX/inc/GetGitHubReleaseAssetURL.ahk index 0f70228..739a009 100644 --- a/UX/inc/GetGitHubReleaseAssetURL.ahk +++ b/UX/inc/GetGitHubReleaseAssetURL.ahk @@ -1,26 +1,26 @@ -GetGitHubReleaseAssetURL(repo, ext:='.zip', release:='latest') { - req := ComObject('Msxml2.XMLHTTP') - req.open('GET', 'https://api.github.com/repos/' repo '/releases/' release, false) - req.send() - if req.status != 200 - throw Error(req.status ' - ' req.statusText, -1) - - res := JSON_parse(req.responseText) - try - assets := res.assets - catch PropertyError - throw Error(res.message, -1) - - loop assets.length { - asset := assets.%A_Index-1% - if SubStr(asset.name, -StrLen(ext)) = ext { - return asset.browser_download_url - } - } - - JSON_parse(str) { - htmlfile := ComObject('htmlfile') - htmlfile.write('') - return htmlfile.parentWindow.JSON.parse(str) - } +GetGitHubReleaseAssetURL(repo, ext:='.zip', release:='latest') { + req := ComObject('Msxml2.XMLHTTP') + req.open('GET', 'https://api.github.com/repos/' repo '/releases/' release, false) + req.send() + if req.status != 200 + throw Error(req.status ' - ' req.statusText, -1) + + res := JSON_parse(req.responseText) + try + assets := res.assets + catch PropertyError + throw Error(res.message, -1) + + loop assets.length { + asset := assets.%A_Index-1% + if SubStr(asset.name, -StrLen(ext)) = ext { + return asset.browser_download_url + } + } + + JSON_parse(str) { + htmlfile := ComObject('htmlfile') + htmlfile.write('') + return htmlfile.parentWindow.JSON.parse(str) + } } \ No newline at end of file diff --git a/UX/inc/HashFile.ahk b/UX/inc/HashFile.ahk index 615bc9a..a0a0944 100644 --- a/UX/inc/HashFile.ahk +++ b/UX/inc/HashFile.ahk @@ -1,96 +1,96 @@ -; HashFile by Deo -; https://autohotkey.com/board/topic/66139-ahk-l-calculating-md5sha-checksum-from-file/ -; Modified for AutoHotkey v2 by lexikos. - -#Requires AutoHotkey v2.0-beta - -/* -HASH types: -1 - MD2 -2 - MD5 -3 - SHA -4 - SHA256 -5 - SHA384 -6 - SHA512 -*/ -HashFile(filePath, hashType:=2) -{ - static PROV_RSA_AES := 24 - static CRYPT_VERIFYCONTEXT := 0xF0000000 - static BUFF_SIZE := 1024 * 1024 ; 1 MB - static HP_HASHVAL := 0x0002 - static HP_HASHSIZE := 0x0004 - - switch hashType { - case 1: hash_alg := (CALG_MD2 := 32769) - case 2: hash_alg := (CALG_MD5 := 32771) - case 3: hash_alg := (CALG_SHA := 32772) - case 4: hash_alg := (CALG_SHA_256 := 32780) - case 5: hash_alg := (CALG_SHA_384 := 32781) - case 6: hash_alg := (CALG_SHA_512 := 32782) - default: throw ValueError('Invalid hashType', -1, hashType) - } - - f := FileOpen(filePath, "r") - f.Pos := 0 ; Rewind in case of BOM. - - HCRYPTPROV() => { - ptr: 0, - __delete: this => this.ptr && DllCall("Advapi32\CryptReleaseContext", "Ptr", this, "UInt", 0) - } - - if !DllCall("Advapi32\CryptAcquireContextW" - , "Ptr*", hProv := HCRYPTPROV() - , "Uint", 0 - , "Uint", 0 - , "Uint", PROV_RSA_AES - , "UInt", CRYPT_VERIFYCONTEXT) - throw OSError() - - HCRYPTHASH() => { - ptr: 0, - __delete: this => this.ptr && DllCall("Advapi32\CryptDestroyHash", "Ptr", this) - } - - if !DllCall("Advapi32\CryptCreateHash" - , "Ptr", hProv - , "Uint", hash_alg - , "Uint", 0 - , "Uint", 0 - , "Ptr*", hHash := HCRYPTHASH()) - throw OSError() - - read_buf := Buffer(BUFF_SIZE, 0) - - While (cbCount := f.RawRead(read_buf, BUFF_SIZE)) - { - if !DllCall("Advapi32\CryptHashData" - , "Ptr", hHash - , "Ptr", read_buf - , "Uint", cbCount - , "Uint", 0) - throw OSError() - } - - if !DllCall("Advapi32\CryptGetHashParam" - , "Ptr", hHash - , "Uint", HP_HASHSIZE - , "Uint*", &HashLen := 0 - , "Uint*", &HashLenSize := 4 - , "UInt", 0) - throw OSError() - - bHash := Buffer(HashLen, 0) - if !DllCall("Advapi32\CryptGetHashParam" - , "Ptr", hHash - , "Uint", HP_HASHVAL - , "Ptr", bHash - , "Uint*", &HashLen - , "UInt", 0 ) - throw OSError() - - loop HashLen - HashVal .= Format('{:02x}', (NumGet(bHash, A_Index-1, "UChar")) & 0xff) - - return HashVal -} +; HashFile by Deo +; https://autohotkey.com/board/topic/66139-ahk-l-calculating-md5sha-checksum-from-file/ +; Modified for AutoHotkey v2 by lexikos. + +#Requires AutoHotkey v2.0-beta + +/* +HASH types: +1 - MD2 +2 - MD5 +3 - SHA +4 - SHA256 +5 - SHA384 +6 - SHA512 +*/ +HashFile(filePath, hashType:=2) +{ + static PROV_RSA_AES := 24 + static CRYPT_VERIFYCONTEXT := 0xF0000000 + static BUFF_SIZE := 1024 * 1024 ; 1 MB + static HP_HASHVAL := 0x0002 + static HP_HASHSIZE := 0x0004 + + switch hashType { + case 1: hash_alg := (CALG_MD2 := 32769) + case 2: hash_alg := (CALG_MD5 := 32771) + case 3: hash_alg := (CALG_SHA := 32772) + case 4: hash_alg := (CALG_SHA_256 := 32780) + case 5: hash_alg := (CALG_SHA_384 := 32781) + case 6: hash_alg := (CALG_SHA_512 := 32782) + default: throw ValueError('Invalid hashType', -1, hashType) + } + + f := FileOpen(filePath, "r") + f.Pos := 0 ; Rewind in case of BOM. + + HCRYPTPROV() => { + ptr: 0, + __delete: this => this.ptr && DllCall("Advapi32\CryptReleaseContext", "Ptr", this, "UInt", 0) + } + + if !DllCall("Advapi32\CryptAcquireContextW" + , "Ptr*", hProv := HCRYPTPROV() + , "Uint", 0 + , "Uint", 0 + , "Uint", PROV_RSA_AES + , "UInt", CRYPT_VERIFYCONTEXT) + throw OSError() + + HCRYPTHASH() => { + ptr: 0, + __delete: this => this.ptr && DllCall("Advapi32\CryptDestroyHash", "Ptr", this) + } + + if !DllCall("Advapi32\CryptCreateHash" + , "Ptr", hProv + , "Uint", hash_alg + , "Uint", 0 + , "Uint", 0 + , "Ptr*", hHash := HCRYPTHASH()) + throw OSError() + + read_buf := Buffer(BUFF_SIZE, 0) + + While (cbCount := f.RawRead(read_buf, BUFF_SIZE)) + { + if !DllCall("Advapi32\CryptHashData" + , "Ptr", hHash + , "Ptr", read_buf + , "Uint", cbCount + , "Uint", 0) + throw OSError() + } + + if !DllCall("Advapi32\CryptGetHashParam" + , "Ptr", hHash + , "Uint", HP_HASHSIZE + , "Uint*", &HashLen := 0 + , "Uint*", &HashLenSize := 4 + , "UInt", 0) + throw OSError() + + bHash := Buffer(HashLen, 0) + if !DllCall("Advapi32\CryptGetHashParam" + , "Ptr", hHash + , "Uint", HP_HASHVAL + , "Ptr", bHash + , "Uint*", &HashLen + , "UInt", 0 ) + throw OSError() + + loop HashLen + HashVal .= Format('{:02x}', (NumGet(bHash, A_Index-1, "UChar")) & 0xff) + + return HashVal +} diff --git a/UX/inc/README.txt b/UX/inc/README.txt index a7f020c..2251389 100644 --- a/UX/inc/README.txt +++ b/UX/inc/README.txt @@ -1,3 +1,3 @@ -Scripts in this directory may be copied and used freely, but -may be removed or modified without notice by any future release. +Scripts in this directory may be copied and used freely, but +may be removed or modified without notice by any future release. Do not #include them directly; instead, create a copy. \ No newline at end of file diff --git a/UX/inc/ShellRun.ahk b/UX/inc/ShellRun.ahk index b91bf06..b57c033 100644 --- a/UX/inc/ShellRun.ahk +++ b/UX/inc/ShellRun.ahk @@ -1,7 +1,7 @@ -; For documentation about the parameters, refer to: -; https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute -ShellRun(filePath, arguments?, directory?, operation?, show?) { - static VT_UI4 := 0x13, SWC_DESKTOP := ComValue(VT_UI4, 0x8) - ComObject("Shell.Application").Windows.Item(SWC_DESKTOP).Document.Application - .ShellExecute(filePath, arguments?, directory?, operation?, show?) +; For documentation about the parameters, refer to: +; https://learn.microsoft.com/en-us/windows/win32/shell/shell-shellexecute +ShellRun(filePath, arguments?, directory?, operation?, show?) { + static VT_UI4 := 0x13, SWC_DESKTOP := ComValue(VT_UI4, 0x8) + ComObject("Shell.Application").Windows.Item(SWC_DESKTOP).Document.Application + .ShellExecute(filePath, arguments?, directory?, operation?, show?) } \ No newline at end of file diff --git a/UX/inc/bounce-v1.ahk b/UX/inc/bounce-v1.ahk index 44cf087..fd01ed9 100644 --- a/UX/inc/bounce-v1.ahk +++ b/UX/inc/bounce-v1.ahk @@ -1,3 +1,3 @@ -; v1: includes the file from the script's directory. -; v2: does nothing because the path is relative to this file. +; v1: includes the file from the script's directory. +; v2: does nothing because the path is relative to this file. #include *i reload-v1.ahk \ No newline at end of file diff --git a/UX/inc/common.ahk b/UX/inc/common.ahk index 0c93902..0a8939d 100644 --- a/UX/inc/common.ahk +++ b/UX/inc/common.ahk @@ -1,21 +1,21 @@ -A_AllowMainWindow := true -if A_AhkPath != A_ScriptDir '\AutoHotkeyUX.exe' { - ; Standalone, compiled or test mode: locate InstallDir via registry - DirExist(ROOT_DIR := RegRead('HKCU\SOFTWARE\AutoHotkey', 'InstallDir', "")) - || (ROOT_DIR := RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "")) -} -if (ROOT_DIR ?? "") = "" || !DirExist(ROOT_DIR) - Loop Files A_ScriptDir '\..', 'D' - ROOT_DIR := A_LoopFileFullPath - -if !trace.Enabled := RegRead('HKCU\Software\AutoHotkey', 'Trace', false) - trace.DefineProp 'call', {call: (*) => ''} - -#include config.ahk - -trace(s) { - try - FileAppend s "`n", "*" - catch - OutputDebug s "`n" -} +A_AllowMainWindow := true +if A_AhkPath != A_ScriptDir '\AutoHotkeyUX.exe' { + ; Standalone, compiled or test mode: locate InstallDir via registry + DirExist(ROOT_DIR := RegRead('HKCU\SOFTWARE\AutoHotkey', 'InstallDir', "")) + || (ROOT_DIR := RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "")) +} +if (ROOT_DIR ?? "") = "" || !DirExist(ROOT_DIR) + Loop Files A_ScriptDir '\..', 'D' + ROOT_DIR := A_LoopFileFullPath + +if !trace.Enabled := RegRead('HKCU\Software\AutoHotkey', 'Trace', false) + trace.DefineProp 'call', {call: (*) => ''} + +#include config.ahk + +trace(s) { + try + FileAppend s "`n", "*" + catch + OutputDebug s "`n" +} diff --git a/UX/inc/config.ahk b/UX/inc/config.ahk index 97d2c46..26b5507 100644 --- a/UX/inc/config.ahk +++ b/UX/inc/config.ahk @@ -1,13 +1,13 @@ - -; CONFIG_FILE_PATH := A_MyDocuments "\AutoHotkey\AutoHotkey.ini" -CONFIG_KEY := 'HKCU\Software\AutoHotkey' - -ConfigRead(section, key, default) { - ; return IniRead(CONFIG_FILE_PATH, section, key, default) - return RegRead(CONFIG_KEY '\' section, key, default) -} - -ConfigWrite(value, section, key) { - ; IniWrite(value, CONFIG_FILE_PATH, section, key) - RegWrite(value, 'REG_SZ', CONFIG_KEY '\' section, key) -} + +; CONFIG_FILE_PATH := A_MyDocuments "\AutoHotkey\AutoHotkey.ini" +CONFIG_KEY := 'HKCU\Software\AutoHotkey' + +ConfigRead(section, key, default) { + ; return IniRead(CONFIG_FILE_PATH, section, key, default) + return RegRead(CONFIG_KEY '\' section, key, default) +} + +ConfigWrite(value, section, key) { + ; IniWrite(value, CONFIG_FILE_PATH, section, key) + RegWrite(value, 'REG_SZ', CONFIG_KEY '\' section, key) +} diff --git a/UX/inc/identify.ahk b/UX/inc/identify.ahk index ab7cd61..4d6cee7 100644 --- a/UX/inc/identify.ahk +++ b/UX/inc/identify.ahk @@ -1,27 +1,27 @@ -#include identify_regex.ahk - -IdentifyBySyntax(code) { - static identify_regex := get_identify_regex() - p := 1, count_1 := count_2 := 0, version := marks := '' - while (p := RegExMatch(code, identify_regex, &m, p)) { - p += m.Len() - if SubStr(m.mark,1,1) = 'v' { - switch SubStr(m.mark,2,1) { - case '1': count_1++ - case '2': count_2++ - } - if !InStr(marks, m.mark) - marks .= m.mark ' ' - } - } - if !(count_1 || count_2) - return {v: 0, r: "no tell-tale matches"} - ; Use a simple, cautious approach for now: select a version only if there were - ; matches for exactly one version. - if count_1 && count_2 - return {v: 0, r: Format( - count_1 > count_2 ? "v1 {1}:{2} - {3}" : count_2 > count_1 ? "v2 {2}:{1} - {3}" : "? {1}:{2} - {3}", - count_1, count_2, Trim(marks) - )} - return {v: count_1 ? 1 : 2, r: Trim(marks)} -} +#include identify_regex.ahk + +IdentifyBySyntax(code) { + static identify_regex := get_identify_regex() + p := 1, count_1 := count_2 := 0, version := marks := '' + while (p := RegExMatch(code, identify_regex, &m, p)) { + p += m.Len() + if SubStr(m.mark,1,1) = 'v' { + switch SubStr(m.mark,2,1) { + case '1': count_1++ + case '2': count_2++ + } + if !InStr(marks, m.mark) + marks .= m.mark ' ' + } + } + if !(count_1 || count_2) + return {v: 0, r: "no tell-tale matches"} + ; Use a simple, cautious approach for now: select a version only if there were + ; matches for exactly one version. + if count_1 && count_2 + return {v: 0, r: Format( + count_1 > count_2 ? "v1 {1}:{2} - {3}" : count_2 > count_1 ? "v2 {2}:{1} - {3}" : "? {1}:{2} - {3}", + count_1, count_2, Trim(marks) + )} + return {v: count_1 ? 1 : 2, r: Trim(marks)} +} diff --git a/UX/inc/identify_regex.ahk b/UX/inc/identify_regex.ahk index 78500b6..952ed37 100644 --- a/UX/inc/identify_regex.ahk +++ b/UX/inc/identify_regex.ahk @@ -1,4 +1,4 @@ -get_identify_regex() => ' -( -(?(DEFINE)(?(?(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z)))(?(?=[ `t]*+(?&line_comment)?(?m:$)))(?(?:(?&eol).*\R|(?&block_comment))++)(?(?:[^ `t`r`n]++|[ `t]*+(?!(?&eol)))*+)(?[ `t]*+\((?i:Join[^ `t`r`n]*+|(?&line_comment)|[^ `t`r`n()]++|[ `t]++)*+\R(?:[ `t]*+(?!\)).*\R)*+[ `t]*+\))(?[ `t]*+(?:,(?!::| +& )|[<>=/|^,?:\.+\-*&!~](?![^"'`r`n]*?(?:".*?::(?!.*?")|'.*?::(?!.*?')|::))|(?i:AND|OR)(?=[ `t])))(?(?&eol)(?:(?(?<=:=)|(?<=[:,]))|(?<=[<>=/|^,?:\.+\-*&!~](?(?&tosol)(?:(?&solcont)(?&subexp)|[ `t]*+,[ `t]*+(?=%)(?&pct)|(?&contsec)(?&ambig)))(?(?:.*+(?&v1_cont))*.*+)(?(?:(?&exp)|(?&v1_cont)|.*+)++(*:~))(?(?=%[ `t])(?:(?&subexp)(?&exp)|(?&v1_fin)(*:v1-pct)))(?(*:exp)(?&exp))(?(?&toeol)(?:(?&tosol)(?:(?&solcont)|(?&contsec))(?&v1_lines))?)(?(?=/|^,?:\.*&!~])(?\R(?:(?&contsec)|(?!(?&solcont))(*:v2-cbe)|))(?(?:[, `t]++|(?&enclf)|(?&subexp)|(?&line_comment))*+)(?%(?:[^,`r`n;\[\]{}()"%']*+|,(?![ `t]*+%)|(?&subexp))*+%(*:v2-pct)|=>(*:v2-fat))(?(?:(?!(?&otb))(?&eolcont)?[ `t]*+(?:[^ `t;,`r`n=\[\]{}()"%']++|\((?&encex)\)|\[(?&encex)\]|\{(?&encex)\}|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq))|'(?&tosol)(?&contsec)'(*:v2-sq)|(?(?:(?&subexp)|[ `t]*+,|(?&eol))++(?&otb)?))(?:[ `t]*+(?&line_comment)(*SKIP)(?!)|(?m:^)[ `t{}]*(?:(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z))(*SKIP)(?!)|(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])|~?(?>\w+|[^ `t`r`n]) & ~?(?>\w+|[^ `t`r`n]))(?i:[ `t]+up)?::(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])(?&eol)(*:remap?)|(?&eol)(?!(?&tosol)[ `t]*+(?:[\{#]|.*?::|[\w[:^ascii:]]++\())(*:v1-hk)|(*:hotkey))|(?(?=:[^\:`r`n]*[xX]):[[:alnum:]\?\*\- ]*:.*(?=/|^,?:\.+\-*&!~ `t()\[\]{}%]++|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq))|['"].*)*+(?=[ `t]*+(?:(?[\w[:^ascii:]#@$]++|%[\w[:^ascii:]#@$]++%)++(?:[ `t]++(?i:not[ `t]++)?(?i:in|contains|between)[ `t]++(?&v1_fin)(*:v1-if)|[ `t]*+(?:[<>]=?|!?=)(?&ambig))|(?&expm))|[\w[:^ascii:]]++(?:[ `t]*+=(?:>(?&ambig)|.*?\?.+?:.*(?&ambig)|(?&v1_fin)(*:v1-ass))|[\(\[](?=.*[\)\]][ `t`r`n]*\{)(?:[ `t]*+[\w[:^ascii:]]++[ `t]*+(?::=(?&subexp))?[ `t]*+,)*+[ `t]*+(?:(?i:ByRef)[ `t]++[\w[:^ascii:]](*:v1-ref)|&(*:v2-ref)|[\w[:^ascii:]]++[ `t]*+=(*:v1-def)|\*[ `t]*+[\)\]](*:v2-vfn)).*|(?=[\(\[\.\?]|[ `t]*+(?>[\:\+\-\*/\.\|&\^]|<<|>>|//)=)(?&expm)|,(?&v1_fin)(*:v1-cmd)|(?&eol)(?&ambig)|[ `t]++(?:[ `t]*+(?:\^|(?:(?!\{)[\w[:^ascii:]<>=/|^,?:\.+\-*&!~ `t()\[\]{}%]|(?>"(?>[^"``\r\n]|``.)*"|'(?>[^'``\r\n]|``.)*'(*:v2-sq)))*+\{[ `t]*+(?:\w+|.)(?:[ `t]++\w+)?[ `t]*+\})(?&v1_fin)(*:v1-send)|(?:[^`r`n,\[\]{}()"%']*+,[ `t]*+)*+(?&pct)|(?&ambig)(*:cmd?)))|(?:\+\+|--)(?&expm)|.(?&ambig)(*:!!))) -)' +get_identify_regex() => ' +( +(?(DEFINE)(?(?(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z)))(?(?=[ `t]*+(?&line_comment)?(?m:$)))(?(?:(?&eol).*\R|(?&block_comment))++)(?(?:[^ `t`r`n]++|[ `t]*+(?!(?&eol)))*+)(?[ `t]*+\((?i:Join[^ `t`r`n]*+|(?&line_comment)|[^ `t`r`n()]++|[ `t]++)*+\R(?:[ `t]*+(?!\)).*\R)*+[ `t]*+\))(?[ `t]*+(?:,(?!::| +& )|[<>=/|^,?:\.+\-*&!~](?![^"'`r`n]*?(?:".*?::(?!.*?")|'.*?::(?!.*?')|::))|(?i:AND|OR)(?=[ `t])))(?(?&eol)(?:(?(?<=:=)|(?<=[:,]))|(?<=[<>=/|^,?:\.+\-*&!~](?(?&tosol)(?:(?&solcont)(?&subexp)|[ `t]*+,[ `t]*+(?=%)(?&pct)|(?&contsec)(?&ambig)))(?(?:.*+(?&v1_cont))*.*+)(?(?:(?&exp)|(?&v1_cont)|.*+)++(*:~))(?(?=%[ `t])(?:(?&subexp)(?&exp)|(?&v1_fin)(*:v1-pct)))(?(*:exp)(?&exp))(?(?&toeol)(?:(?&tosol)(?:(?&solcont)|(?&contsec))(?&v1_lines))?)(?(?=/|^,?:\.*&!~])(?\R(?:(?&contsec)|(?!(?&solcont))(*:v2-cbe)|))(?(?:[, `t]++|(?&enclf)|(?&subexp)|(?&line_comment))*+)(?%(?:[^,`r`n;\[\]{}()"%']*+|,(?![ `t]*+%)|(?&subexp))*+%(*:v2-pct)|=>(*:v2-fat))(?(?:(?!(?&otb))(?&eolcont)?[ `t]*+(?:[^ `t;,`r`n=\[\]{}()"%']++|\((?&encex)\)|\[(?&encex)\]|\{(?&encex)\}|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq))|'(?&tosol)(?&contsec)'(*:v2-sq)|(?(?:(?&subexp)|[ `t]*+,|(?&eol))++(?&otb)?))(?:[ `t]*+(?&line_comment)(*SKIP)(?!)|(?m:^)[ `t{}]*(?:(?m:^[ `t]*/\*(?:.*\R?)+?(?:[ `t]*\*/|.*\Z))(*SKIP)(?!)|(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])|~?(?>\w+|[^ `t`r`n]) & ~?(?>\w+|[^ `t`r`n]))(?i:[ `t]+up)?::(?:[<>*~$!^+#]*(?>\w+|[^ `t`r`n])(?&eol)(*:remap?)|(?&eol)(?!(?&tosol)[ `t]*+(?:[\{#]|.*?::|[\w[:^ascii:]]++\())(*:v1-hk)|(*:hotkey))|(?(?=:[^\:`r`n]*[xX]):[[:alnum:]\?\*\- ]*:.*(?=/|^,?:\.+\-*&!~ `t()\[\]{}%]++|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq))|['"].*)*+(?=[ `t]*+(?:(?[\w[:^ascii:]#@$]++|%[\w[:^ascii:]#@$]++%)++(?:[ `t]++(?i:not[ `t]++)?(?i:in|contains|between)[ `t]++(?&v1_fin)(*:v1-if)|[ `t]*+(?:[<>]=?|!?=)(?&ambig))|(?&expm))|[\w[:^ascii:]]++(?:[ `t]*+=(?:>(?&ambig)|.*?\?.+?:.*(?&ambig)|(?&v1_fin)(*:v1-ass))|[\(\[](?=.*[\)\]][ `t`r`n]*\{)(?:[ `t]*+[\w[:^ascii:]]++[ `t]*+(?::=(?&subexp))?[ `t]*+,)*+[ `t]*+(?:(?i:ByRef)[ `t]++[\w[:^ascii:]](*:v1-ref)|&(*:v2-ref)|[\w[:^ascii:]]++[ `t]*+=(*:v1-def)|\*[ `t]*+[\)\]](*:v2-vfn)).*|(?=[\(\[\.\?]|[ `t]*+(?>[\:\+\-\*/\.\|&\^]|<<|>>|//)=)(?&expm)|,(?&v1_fin)(*:v1-cmd)|(?&eol)(?&ambig)|[ `t]++(?:[ `t]*+(?:\^|(?:(?!\{)[\w[:^ascii:]<>=/|^,?:\.+\-*&!~ `t()\[\]{}%]|(?>"(?>[^"``\r\n]|``["'``])*+"|'(?>[^'``\r\n]|``["'``])*+'(*:v2-sq)))*+\{[ `t]*+(?:\w+|.)(?:[ `t]++\w+)?[ `t]*+\})(?&v1_fin)(*:v1-send)|(?:[^`r`n,\[\]{}()"%']*+,[ `t]*+)*+(?&pct)|(?&ambig)(*:cmd?)))|(?:\+\+|--)(?&expm)|.(?&ambig)(*:!!))) +)' diff --git a/UX/inc/launcher-common.ahk b/UX/inc/launcher-common.ahk index 039a500..2e76083 100644 --- a/UX/inc/launcher-common.ahk +++ b/UX/inc/launcher-common.ahk @@ -1,78 +1,78 @@ - -#include common.ahk - -GetExeInfo(exe) { - if !(verSize := DllCall("version\GetFileVersionInfoSize", "str", exe, "uint*", 0, "uint")) - || !DllCall("version\GetFileVersionInfo", "str", exe, "uint", 0, "uint", verSize, "ptr", verInfo := Buffer(verSize)) - throw OSError() - prop := {Path: exe} - static Properties := { - Version: 'FileVersion', - Description: 'FileDescription', - ProductName: 'ProductName' - } - for propName, infoName in Properties.OwnProps() - if DllCall("version\VerQueryValue", "ptr", verInfo, "str", "\StringFileInfo\040904b0\" infoName, "ptr*", &p:=0, "uint*", &len:=0) - prop.%propName% := StrGet(p, len) - else throw OSError() - if InStr(exe, '_UIA') - prop.Description .= ' UIA' - prop.Version := RegExReplace(prop.Version, 'i)[a-z]{2,}\K(?=\d)|, ', '.') ; Hack-fix for erroneous version numbers (AutoHotkey_H v2.0-beta3-H...) - return prop -} - -IsUsableAutoHotkey(exeinfo) { - return exeinfo.HasProp('Description') - && RegExMatch(exeinfo.Description, '^AutoHotkey.* (32|64)-bit', &m) - && (m.1 != '64' || A_Is64bitOS) - && !InStr(exeinfo.Path, '\AutoHotkeyUX.exe') -} - -GetMajor(v) { - Loop Parse, v, '.-+' - return Integer(A_LoopField) - throw ValueError('Invalid version number', -1, v) -} - -ReadHashes(path, filter?) { - filemap := Map(), filemap.CaseSense := 0 - if !FileExist(path) - return filemap - csvfile := FileOpen(path, 'r') - props := StrSplit(csvfile.ReadLine(), ',') - while !csvfile.AtEOF { - item := {} - Loop Parse csvfile.ReadLine(), 'CSV' - item.%props[A_Index]% := A_LoopField - if IsSet(filter) && !filter(item) - continue - filemap[item.Path] := item - } - return filemap -} - -GetUsableAutoHotkeyExes() { - static files - if IsSet(files) { - trace '![Launcher] returning hashes again' - return files - } - files := ReadHashes(ROOT_DIR '\UX\installed-files.csv', - item => IsUsableAutoHotkey(item) && ( - item.Path ~= '^(?!\w:|\\\\)' && item.Path := ROOT_DIR '\' item.Path, - true - )) - if files.Count { - trace '![Launcher] returning hashes from cache' - return files - } - Loop Files ROOT_DIR '\AutoHotkey*.exe', 'R' { - try { - item := GetExeInfo(A_LoopFilePath) - if IsUsableAutoHotkey(item) - files[item.Path] := item - } - } - trace '![Launcher] returning hashes from filesystem' - return files -} + +#include common.ahk + +GetExeInfo(exe) { + if !(verSize := DllCall("version\GetFileVersionInfoSize", "str", exe, "uint*", 0, "uint")) + || !DllCall("version\GetFileVersionInfo", "str", exe, "uint", 0, "uint", verSize, "ptr", verInfo := Buffer(verSize)) + throw OSError() + prop := {Path: exe} + static Properties := { + Version: 'FileVersion', + Description: 'FileDescription', + ProductName: 'ProductName' + } + for propName, infoName in Properties.OwnProps() + if DllCall("version\VerQueryValue", "ptr", verInfo, "str", "\StringFileInfo\040904b0\" infoName, "ptr*", &p:=0, "uint*", &len:=0) + prop.%propName% := StrGet(p, len) + else throw OSError() + if InStr(exe, '_UIA') + prop.Description .= ' UIA' + prop.Version := RegExReplace(prop.Version, 'i)[a-z]{2,}\K(?=\d)|, ', '.') ; Hack-fix for erroneous version numbers (AutoHotkey_H v2.0-beta3-H...) + return prop +} + +IsUsableAutoHotkey(exeinfo) { + return exeinfo.HasProp('Description') + && RegExMatch(exeinfo.Description, '^AutoHotkey.* (32|64)-bit', &m) + && (m.1 != '64' || A_Is64bitOS) + && !InStr(exeinfo.Path, '\AutoHotkeyUX.exe') +} + +GetMajor(v) { + Loop Parse, v, '.-+' + return Integer(A_LoopField) + throw ValueError('Invalid version number', -1, v) +} + +ReadHashes(path, filter?) { + filemap := Map(), filemap.CaseSense := 0 + if !FileExist(path) + return filemap + csvfile := FileOpen(path, 'r') + props := StrSplit(csvfile.ReadLine(), ',') + while !csvfile.AtEOF { + item := {} + Loop Parse csvfile.ReadLine(), 'CSV' + item.%props[A_Index]% := A_LoopField + if IsSet(filter) && !filter(item) + continue + filemap[item.Path] := item + } + return filemap +} + +GetUsableAutoHotkeyExes() { + static files + if IsSet(files) { + trace '![Launcher] returning hashes again' + return files + } + files := ReadHashes(ROOT_DIR '\UX\installed-files.csv', + item => IsUsableAutoHotkey(item) && ( + item.Path ~= '^(?!\w:|\\\\)' && item.Path := ROOT_DIR '\' item.Path, + true + )) + if files.Count { + trace '![Launcher] returning hashes from cache' + return files + } + Loop Files ROOT_DIR '\AutoHotkey*.exe', 'R' { + try { + item := GetExeInfo(A_LoopFilePath) + if IsUsableAutoHotkey(item) + files[item.Path] := item + } + } + trace '![Launcher] returning hashes from filesystem' + return files +} diff --git a/UX/install-ahk2exe.ahk b/UX/install-ahk2exe.ahk index 2037257..4329556 100644 --- a/UX/install-ahk2exe.ahk +++ b/UX/install-ahk2exe.ahk @@ -1,53 +1,53 @@ -; Run this script to launch or download and install Ahk2Exe into A_ScriptDir '\..\Compiler'. -#requires AutoHotkey v2.0 - -#include install.ahk -#include inc\GetGitHubReleaseAssetURL.ahk - -#SingleInstance Force -InstallAhk2Exe - -InstallAhk2Exe() { - inst := Installation() - inst.ResolveInstallDir() ; This sets inst.InstallDir and inst.UserInstall - - finalPath := inst.InstallDir '\Compiler\Ahk2Exe.exe' - if FileExist(finalPath) { - ShellRun finalPath - ExitApp - } - - if !A_Args.Length { - (inst.UserInstall) || SetTimer(() => ( - WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) && - SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C - ), -25) - if MsgBox("Ahk2Exe is not installed, but we can download and install it for you.", "AutoHotkey", 'OkCancel') = 'Cancel' - ExitApp - if !A_IsAdmin && !inst.UserInstall { - Run Format('*RunAs "{1}" /restart /script "{2}" /Y', A_AhkPath, A_ScriptFullPath) - ExitApp - } - } - - tempDir := A_ScriptDir '\.staging' ; Avoid A_Temp for security reasons - DirCreate tempDir - SetWorkingDir tempDir - - TrayTip "Downloading Ahk2Exe", "AutoHotkey" - url := GetGitHubReleaseAssetURL('AutoHotkey/Ahk2Exe') - Download url, 'Ahk2Exe.zip' - - TrayTip "Installing Ahk2Exe", "AutoHotkey" - DirCopy 'Ahk2Exe.zip', 'Compiler', true - FileDelete 'Ahk2Exe.zip' - - inst.AddCompiler(tempDir '\Compiler') - inst.Apply() - - ; Working dir may have been changed - DirDelete tempDir '\Compiler', true - DirDelete tempDir - - ShellRun finalPath -} +; Run this script to launch or download and install Ahk2Exe into A_ScriptDir '\..\Compiler'. +#requires AutoHotkey v2.0 + +#include install.ahk +#include inc\GetGitHubReleaseAssetURL.ahk + +#SingleInstance Force +InstallAhk2Exe + +InstallAhk2Exe() { + inst := Installation() + inst.ResolveInstallDir() ; This sets inst.InstallDir and inst.UserInstall + + finalPath := inst.InstallDir '\Compiler\Ahk2Exe.exe' + if FileExist(finalPath) { + ShellRun finalPath + ExitApp + } + + if !A_Args.Length { + (inst.UserInstall) || SetTimer(() => ( + WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) && + SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C + ), -25) + if MsgBox("Ahk2Exe is not installed, but we can download and install it for you.", "AutoHotkey", 'OkCancel') = 'Cancel' + ExitApp + if !A_IsAdmin && !inst.UserInstall { + Run Format('*RunAs "{1}" /restart /script "{2}" /Y', A_AhkPath, A_ScriptFullPath) + ExitApp + } + } + + tempDir := A_ScriptDir '\.staging' ; Avoid A_Temp for security reasons + DirCreate tempDir + SetWorkingDir tempDir + + TrayTip "Downloading Ahk2Exe", "AutoHotkey" + url := GetGitHubReleaseAssetURL('AutoHotkey/Ahk2Exe') + Download url, 'Ahk2Exe.zip' + + TrayTip "Installing Ahk2Exe", "AutoHotkey" + DirCopy 'Ahk2Exe.zip', 'Compiler', true + FileDelete 'Ahk2Exe.zip' + + inst.AddCompiler(tempDir '\Compiler') + inst.Apply() + + ; Working dir may have been changed + DirDelete tempDir '\Compiler', true + DirDelete tempDir + + ShellRun finalPath +} diff --git a/UX/install-version.ahk b/UX/install-version.ahk index 7707c4e..b470a5e 100644 --- a/UX/install-version.ahk +++ b/UX/install-version.ahk @@ -1,109 +1,109 @@ -; Run this script to download and install an additional AutoHotkey version. -; Specify the version as a single command line parameter. If omitted or -; incomplete like "1.1" or "2.0", the latest version will be downloaded. -#requires AutoHotkey v2.0 - -#include install.ahk - -A_ScriptName := "AutoHotkey" - -InstallAutoHotkey A_Args.Length ? A_Args[1] : '1.1' - -InstallAutoHotkey(version) { - abort(message, extra?) { - if IsSet(extra) - message .= "`n`nSpecifically: " SubStr(extra, 1, 100) - MsgBox message,, "Iconx" - ExitApp - } - - ; Determine base version, for download directory - baseVersion := RegExReplace(version, '^\d+(?:\.\d+)?\b\K.*') - if IsInteger(baseVersion) - baseVersion .= baseVersion = '1' ? '.1' : '.0' - else if !IsNumber(baseVersion) - abort "Invalid version.", version - - ; If version number is not exact, try to determine the latest compatible version - if IsNumber(version) { - url := Format('https://www.autohotkey.com/download/{}/version.txt', baseVersion) - req := ComObject('Msxml2.XMLHTTP') - req.open('GET', url, false) - req.send() - if req.status != 200 - throw Error(req.status ' - ' req.statusText, -1) - currentVersion := req.responseText - if VerCompare(currentVersion, baseVersion) < 0 || VerCompare(currentVersion, Round(baseVersion + 1)) >= 0 - abort "An error occurred while trying to identify the latest available version. The downloaded version.txt was invalid.", currentVersion - version := currentVersion - } - - ; Only versions for which a zip is available are supported by this script. - ; 1.1.00.00 - 1.1.22.06 have no downloads available at the time of writing. - ; 1.1.22.07 - 1.1.24.01 have one zip per exe. - if VerCompare(version, '1.1') >= 0 && VerCompare(version, '1.1.24.02') < 0 - abort "This version cannot be installed automatically.", version - - inst := Installation() - inst.ResolveInstallDir() - - if A_Args.Length < 2 && !A_IsAdmin && !inst.UserInstall { - ExitApp RunWait(Format('*RunAs "{1}" /force /script "{2}" {3} /Y', A_AhkPath, A_ScriptFullPath, version)) - } - - zipName := VerCompare(version, '1.1.24.02') < 0 - ? 'AutoHotkey' StrReplace(version, '.') '.zip' - : 'AutoHotkey_' version '.zip' - url := Format('https://www.autohotkey.com/download/{}/{}', baseVersion, zipName) - - try { - tempDir := inst.InstallDir '\.staging' ; Avoid A_Temp for security reasons - try { - DirCreate tempDir - SetWorkingDir tempDir - } - catch OSError as e - abort "An error occured while preparing the temporary directory.", e.Message - - TrayTip "Downloading AutoHotkey v" version, "AutoHotkey" - try - Download url, tempDir '\' zipName - catch - abort "Download failed.`n`nURL: " url - - TrayTip "Installing AutoHotkey v" version, "AutoHotkey" - inst.SourceDir := tempDir '\v' version - try - DirCopy tempDir '\' zipName, inst.SourceDir, true - catch - abort "Extraction failed." - finally - try - FileDelete zipName - catch - MsgBox 'Unable to delete temporary file "' tempDir '\' zipName '".',, "Icon!" - - try localUX := inst.Hashes['UX\install.ahk'] - catch - localUX := {Version: ''} - try { - if VerCompare(localUX.Version, version) < 0 - && FileExist(inst.SourceDir '\UX\install.ahk') - && FileExist(inst.SourceDir '\AutoHotkey32.exe') { - Run Format('"{1}\AutoHotkey32.exe" UX\install.ahk /to "{2}"', inst.SourceDir, inst.InstallDir), inst.SourceDir - ExitApp - } - else - inst.InstallExtraVersion - } - catch as e - abort "An error occurred during installation.", e.Message - } - finally { - try DirDelete tempDir '\v' version, true - try DirDelete tempDir - } - - TrayTip - MsgBox "AutoHotkey v" version " is now installed." -} +; Run this script to download and install an additional AutoHotkey version. +; Specify the version as a single command line parameter. If omitted or +; incomplete like "1.1" or "2.0", the latest version will be downloaded. +#requires AutoHotkey v2.0 + +#include install.ahk + +A_ScriptName := "AutoHotkey" + +InstallAutoHotkey A_Args.Length ? A_Args[1] : '1.1' + +InstallAutoHotkey(version) { + abort(message, extra?) { + if IsSet(extra) + message .= "`n`nSpecifically: " SubStr(extra, 1, 100) + MsgBox message,, "Iconx" + ExitApp + } + + ; Determine base version, for download directory + baseVersion := RegExReplace(version, '^\d+(?:\.\d+)?\b\K.*') + if IsInteger(baseVersion) + baseVersion .= baseVersion = '1' ? '.1' : '.0' + else if !IsNumber(baseVersion) + abort "Invalid version.", version + + ; If version number is not exact, try to determine the latest compatible version + if IsNumber(version) { + url := Format('https://www.autohotkey.com/download/{}/version.txt', baseVersion) + req := ComObject('Msxml2.XMLHTTP') + req.open('GET', url, false) + req.send() + if req.status != 200 + throw Error(req.status ' - ' req.statusText, -1) + currentVersion := req.responseText + if VerCompare(currentVersion, baseVersion) < 0 || VerCompare(currentVersion, Round(baseVersion + 1)) >= 0 + abort "An error occurred while trying to identify the latest available version. The downloaded version.txt was invalid.", currentVersion + version := currentVersion + } + + ; Only versions for which a zip is available are supported by this script. + ; 1.1.00.00 - 1.1.22.06 have no downloads available at the time of writing. + ; 1.1.22.07 - 1.1.24.01 have one zip per exe. + if VerCompare(version, '1.1') >= 0 && VerCompare(version, '1.1.24.02') < 0 + abort "This version cannot be installed automatically.", version + + inst := Installation() + inst.ResolveInstallDir() + + if A_Args.Length < 2 && !A_IsAdmin && !inst.UserInstall { + ExitApp RunWait(Format('*RunAs "{1}" /force /script "{2}" {3} /Y', A_AhkPath, A_ScriptFullPath, version)) + } + + zipName := VerCompare(version, '1.1.24.02') < 0 + ? 'AutoHotkey' StrReplace(version, '.') '.zip' + : 'AutoHotkey_' version '.zip' + url := Format('https://www.autohotkey.com/download/{}/{}', baseVersion, zipName) + + try { + tempDir := inst.InstallDir '\.staging' ; Avoid A_Temp for security reasons + try { + DirCreate tempDir + SetWorkingDir tempDir + } + catch OSError as e + abort "An error occured while preparing the temporary directory.", e.Message + + TrayTip "Downloading AutoHotkey v" version, "AutoHotkey" + try + Download url, tempDir '\' zipName + catch + abort "Download failed.`n`nURL: " url + + TrayTip "Installing AutoHotkey v" version, "AutoHotkey" + inst.SourceDir := tempDir '\v' version + try + DirCopy tempDir '\' zipName, inst.SourceDir, true + catch + abort "Extraction failed." + finally + try + FileDelete zipName + catch + MsgBox 'Unable to delete temporary file "' tempDir '\' zipName '".',, "Icon!" + + try localUX := inst.Hashes['UX\install.ahk'] + catch + localUX := {Version: ''} + try { + if VerCompare(localUX.Version, version) < 0 + && FileExist(inst.SourceDir '\UX\install.ahk') + && FileExist(inst.SourceDir '\AutoHotkey32.exe') { + Run Format('"{1}\AutoHotkey32.exe" UX\install.ahk /to "{2}"', inst.SourceDir, inst.InstallDir), inst.SourceDir + ExitApp + } + else + inst.InstallExtraVersion + } + catch as e + abort "An error occurred during installation.", e.Message + } + finally { + try DirDelete tempDir '\v' version, true + try DirDelete tempDir + } + + TrayTip + MsgBox "AutoHotkey v" version " is now installed." +} diff --git a/UX/install.ahk b/UX/install.ahk index 0b1a2f4..7be51a9 100644 --- a/UX/install.ahk +++ b/UX/install.ahk @@ -1,1013 +1,1024 @@ -; This script contains AutoHotkey (un)installation routines. -; See the AutoHotkey v2 documentation for usage. -#include inc\bounce-v1.ahk -/* v1 stops here */ -#requires AutoHotkey v2.0 - -#SingleInstance Off ; Needed for elevation with *runas. - -#include inc\launcher-common.ahk -#include inc\HashFile.ahk -#include inc\CreateAppShortcut.ahk -#include inc\EnableUIAccess.ahk -#include inc\ShellRun.ahk - -if A_LineFile = A_ScriptFullPath - Install_Main - -Install_Main() { - try { - inst := Installation() - method := 'InstallFull' - params := [] - while A_Index <= A_Args.Length { - switch A_Args[A_Index], 'off' { - case '/install': - method := 'InstallExtraVersion' - inst.SourceDir := A_Args[++A_Index] - case '/uninstall': - method := 'Uninstall' - if A_Index < A_Args.Length && SubStr(A_Args[A_Index+1],1,1) != '/' - params.Push(A_Args[++A_Index]) - case '/to', '/installto': - inst.InstallDir := A_Args[++A_Index] - case '/elevate': - inst.RequireAdmin := true - case '/user': - inst.UserInstall := true - case '/silent': - inst.Silent := true - default: - MsgBox 'Invalid arg "' A_Args[A_Index] '"', inst.DialogTitle, "Iconx" - ExitApp 1 - } - } - inst.%method%(params*) - } - catch as e { - DllCall(CallbackCreate(errBox.Bind(e))) - ExitApp 1 - } - errBox(e) { - throw e - } -} - -class Installation { - ProductName := "AutoHotkey" - ProductURL := "https://autohotkey.com" - Publisher := "AutoHotkey Foundation LLC" - Version := A_AhkVersion - AppUserModelID := 'AutoHotkey.AutoHotkey' - - UserInstall := !A_IsAdmin - Interpreter := A_AhkPath - Silent := false - - ScriptProgId := 'AutoHotkeyScript' - SoftwareSubKey := 'Software\AutoHotkey' - RootKey => this.UserInstall ? 'HKCU' : 'HKLM' - SoftwareKey => this.RootKey '\' this.SoftwareSubKey - ClassesKey => this.RootKey '\Software\Classes' - FileTypeKey => this.ClassesKey '\' this.ScriptProgId - UninstallKey => this.RootKey '\Software\Microsoft\Windows\CurrentVersion\Uninstall\AutoHotkey' - StartFolder => (this.UserInstall ? A_Programs : A_ProgramsCommon) - UninstallCmd => this.CmdStr('UX\ui-uninstall.ahk', ((A_IsAdmin && this.UserInstall) ? '/elevate' : '')) - QUninstallCmd => this.CmdStr('UX\install.ahk', '/uninstall /silent') - - DialogTitle => this.ProductName " Setup" - - FileItems := [] ; [{Source, Dest}] - RegItems := [] ; [{Key, ValueName, Value}] - PreCheck := [] ; [Callback(this)] - PreAction := [] ; [Callback(this)] - PostAction := [] ; [Callback(this)] - - ResolveInstallDir() { - if hadInstallDir := this.HasProp('InstallDir') - DirCreate installDir := this.InstallDir - else - installDir := A_ScriptDir '\..' - Loop Files installDir, 'D' - this.InstallDir := installDir := A_LoopFileFullPath - else - throw ValueError("Invalid target directory",, installDir) - SetRegView 64 - installDirs := [] - for rootKey in ['HKLM', 'HKCU'] - installDirs.Push RegRead(rootKey '\' this.SoftwareSubKey, 'InstallDir', '') - ; Override installation mode if already installed here - if installDirs[1] = installDir - this.UserInstall := false - else if installDirs[2] = installDir - this.UserInstall := true - ; If this.InstallDir wasn't set upon entry to this method... - else if !hadInstallDir { - ; Default to the location of an existing installation matching this.UserInstall - if installDirs[this.UserInstall?2:1] - this.InstallDir := installDirs[this.UserInstall?2:1] - ; Default to the location and mode of any other existing installation - else if installDirs[this.UserInstall?1:2] - this.InstallDir := installDirs[this.UserInstall?1:2], this.UserInstall := !this.UserInstall - } - } - - ResolveSourceDir() { - if !this.HasProp('SourceDir') { - if A_IsCompiled && IsSet(UnpackFiles) - this.SourceDir := UnpackFiles(this.InstallDir) - else - this.SourceDir := A_ScriptDir '\..' - } - Loop Files this.SourceDir, 'D' - this.SourceDir := A_LoopFileFullPath - else - throw ValueError("Invalid source directory",, this.SourceDir) - } - - HashesPath => this.InstallDir '\UX\installed-files.csv' - Hashes => ( - this.DefineProp('Hashes', {value: hashes := this.ReadHashes()}), - hashes - ) - - Apply() { - if !DirExist(this.InstallDir) - DirCreate this.InstallDir - SetWorkingDir this.InstallDir - - ; Execute pre-check actions - for item in this.PreCheck - item(this) - - ; Detect possible conflicts before taking action - this.PreApplyChecks() - - ; Execute pre-install actions - for item in this.PreAction - item(this) - - ; Install files - for item in this.FileItems { - SplitPath item.Dest,, &destDir - if destDir != '' - DirCreate destDir - try - FileCopy item.Source, item.Dest, true - catch - this.WarnBox 'Copy failed`nsource: ' item.Source '`ndest: ' item.Dest - else { - ; If source files were extracted from a zip, they may have a Zone.Identifier - ; stream identifying them as coming from the Internet. These must be deleted - ; to prevent warnings when the user runs the installed files. - try FileDelete item.Dest ":Zone.Identifier" - this.AddFileHash(item.Dest, this.Version) - } - } - - ; Install registry settings - for item in this.RegItems { - if item.HasProp('Value') { - RegWrite(item.Value, item.Value is Integer ? 'REG_DWORD' : 'REG_SZ' - , item.Key, item.ValueName) - } else { - try RegDelete(item.Key, item.ValueName) - } - } - - ; Execute post-install actions - for item in this.PostAction - item(this) - - ; Write file list to disk - if this.Hashes.Count - this.WriteHashes - } - - ElevationNeeded => !A_IsAdmin && (!this.UserInstall || this.HasProp('RequireAdmin')) - - ElevateIfNeeded() { - if this.ElevationNeeded { - try RunWait '*runas ' DllCall('GetCommandLine', 'str') - ExitApp - } - } - - ;{ Installation entry points - - InstallFull() { - SetRegView 64 - - this.ResolveInstallDir - this.ResolveSourceDir - - doFiles := this.InstallDir != this.SourceDir - - ; If a newer version is already installed, integrate with it - ux := doFiles && this.GetTargetUX() - if ux && VerCompare(ux.Version, this.Version) > 0 { - cmd := StrReplace(ux.InstallCommand, '%1', this.SourceDir,, &replaced) - if !replaced - cmd .= ' "' this.SourceDir '"' - if this.ElevationNeeded - cmd := '*runas ' cmd - try - exitCode := RunWait(cmd, this.InstallDir) - catch as e - if InStr(e.Message e.Extra, 'cancel') - exitCode := 1 - else - throw e - ExitApp exitCode - } - - this.ElevateIfNeeded - - ; If a legacy version is installed, upgrade it - wowKey(k) => StrReplace(k, '\Software\', '\Software\Wow6432Node\') - installedVersion := RegRead(key := wowKey(this.SoftwareKey), 'Version', '') - || RegRead(key := this.SoftwareKey, 'Version', '') - if SubStr(installedVersion, 1, 2) = '1.' { - this.SoftwareKeyV1 := key - this.UninstallKeyV1 := InStr(key, 'Wow64') ? wowKey(this.UninstallKey) : this.UninstallKey - this.AddPreCheck this.PrepareUpgradeV1.Bind(, installedVersion) - this.AddPreAction this.UpgradeV1.Bind(, installedVersion) - } - - if doFiles || FileExist(this.InstallDir '\UX\AutoHotkeyUX.exe') - this.Interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe' - - if doFiles { - if this.GetLinkAttrib(this.InstallDir '\v2') - this.AddPreAction this.DeleteLink.Bind(, 'v2') - this.AddCoreFiles 'v2' - ; Give UX its own AutoHotkey.exe for a few reasons: - ; 1. The Start menu shortcut needs a stable path, since pinning to taskbar creates - ; a copy that won't get updated (obsolete since using 'v2' and DisplaceFile now). - ; 2. LauncherConfigGui may store the path under HKCU, which mightn't get updated. - ; 3. It helps differentiate the launcher from other scripts in Task Manager. - ; 4. It makes the UX scripts independent from the installed interpreters. - srcExe := this.SourceDir '\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe' - dstExe := this.InstallDir '\UX\AutoHotkeyUX.exe' - if FileExist(srcExe) || (!FileExist(dstExe) && (srcExe := A_AhkPath)) - this.AddFileCopy srcExe, 'UX\AutoHotkeyUX.exe' ; Must be relative - this.AddPostAction this.UpdateV2Link - - this.AddUXFiles - this.AddMiscFiles - } - - this.AddPostAction this.CreateWindowSpyRedirect - - this.AddUninstallReg - this.AddSoftwareReg - this.AddFileTypeReg - - this.Apply - - if FileExist(this.InstallDir '\UX\reset-assoc.ahk') - RunWait this.CmdStr('UX\reset-assoc.ahk', '/check') - - if !this.Silent - ShellRun this.Interpreter, 'UX\ui-dash.ahk', this.InstallDir - } - - InstallExtraVersion() { - SetRegView 64 - - this.ResolveInstallDir - this.ResolveSourceDir - - Loop Files this.SourceDir '\AutoHotkey*.exe' { - exe := GetExeInfo(A_LoopFilePath) - break - } else - throw Error("AutoHotkey*.exe not found in source directory",, this.SourceDir) - - this.ElevateIfNeeded - - coreDir := 'v' (this.Version := exe.Version) - - if VerCompare(this.Version, '2.0-') < 0 { - try - exe := GetExeInfo(this.InstallDir '\AutoHotkeyU32.exe') - catch - {} - else if VerCompare(this.Version, exe.Version) > 0 - && !FileExist(this.InstallDir '\v' exe.Version) { - this.AddPreAction this.DisplaceV1.Bind(, exe.Version) - coreDir := '.' - } - } - - this.AddCoreFiles(coreDir) - - if FileExist(this.SourceDir '\Compiler\Ahk2Exe.exe') { - compilerVersion := GetExeInfo(this.SourceDir '\Compiler\Ahk2Exe.exe').Version - installedCompiler := this.Hashes.Get('Compiler\Ahk2Exe.exe', '') - if !installedCompiler || VerCompare(compilerVersion, installedCompiler.Version) > 0 - this.AddCompiler(this.SourceDir '\Compiler') - } - - this.Apply - } - - ;} - - ;{ Uninstallation - - UninstallRegistry() { - SetRegView 64 - delKey this.FileTypeKey - delKey this.ClassesKey '\.ahk' - delKey this.SoftwareKey - delKey this.UninstallKey - if this.RootKey = 'HKLM' { - delKey 'HKCU\' this.SoftwareSubKey - delKey 'HKCU\Software\Classes\' this.ScriptProgId - for k in ['AutoHotkey.exe', 'Ahk2Exe.exe'] ; made by v1 installer - delKey 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' k - } - - delKey(key) { - try - RegDeleteKey key - catch OSError as e - if e.number != 2 ; ERROR_FILE_NOT_FOUND - throw - } - - this.NotifyAssocChanged - } - - GetHashesForVersions(versions) { - versions := ',' versions ',' - files := Map(), files.CaseSense := "off" - for , cfiles in this.GetComponents(v => InStr(versions, ',' v ',')) - for fh in cfiles - files[fh.Path] := fh - return files - } - - Uninstall(versions:='') { - this.ResolveInstallDir - - this.ElevateIfNeeded - - files := versions = '' ? this.Hashes.Clone() : this.GetHashesForVersions(versions) - if !files.Count && versions = '' - this.GetConfirmation("Installation data missing. Files will not be deleted.", 'x') - - ; Close scripts and help files - this.PreUninstallChecks files - - ; Remove from registry only if being fully uninstalled - if versions = '' - this.UninstallRegistry - - ; Remove files - SetWorkingDir this.InstallDir - modified := "" - dirs := "" - for path, f in files { - if !FileExist(path) - continue - if HashFile(path) != f.Hash { - modified .= "`n" path - continue - } - if this.InstallDir '\' path = A_AhkPath { - postponed := A_AhkPath - this.Hashes.Delete(path) - continue - } - FileDelete path - this.Hashes.Delete(path) - SplitPath path,, &dir - if dir != "" - dirs .= dir "`n" - } - if modified != "" { - this.InfoBox("The following files were not deleted as they appear to have been modified:" - . modified) - } - - ; Update or remove hashes file - if this.Hashes.Count - this.WriteHashes - else - try FileDelete this.HashesPath - - ; Remove empty directories - for dir in StrSplit(Sort(RTrim(dirs, "`n"), 'UR'), "`n") { - this.DeleteLink dir '\AutoHotkey.exe' - try DirDelete dir, false - } - - if versions = '' ; Full uninstall - this.DeleteLink this.InstallDir '\v2' - - if IsSet(postponed) { - ; Try delete via cmd.exe after we exit - Run(A_ComSpec ' /c "' - 'ping -n 2 127.1 > NUL & ' - 'del "' postponed '" & ' - 'cd %TEMP% & ' - 'rmdir "' postponed '\.." & ' - 'rmdir "' A_WorkingDir '"' - '"',, 'Hide') - } - ExitApp - } - - ;} - - ;{ Conflict prevention - - PreApplyChecks() { - ; Check for files that might be overwritten - writeFiles := Map(), writeFiles.CaseSense := 'off' - hasChm := false - unknownFiles := '' - modifiedFiles := '' - hashes := this.Hashes - if (item := hashes.Get(this.InstallDir '\UX\AutoHotkeyUX.exe', false)) ; Erroneous entry by v2.0-beta.4 - hashes.Delete(item.Path), hashes[item.Path := 'UX\AutoHotkeyUX.exe'] := item ; Make it relative - for item in this.FileItems { - if !(attrib := FileExist(item.Dest)) - continue - if InStr(attrib, 'D') - this.FatalError("The following file cannot be installed because a directory by this name already exists:`n" - item.Dest "`n`nNo changes have been made.") - SplitPath item.Dest,, &destDir, &ext, &destName - if destDir = 'v2' && this.GetLinkAttrib(destDir) - continue ; Symlink should be deleted, so item.Dest won't exist. - if ext = 'exe' - writeFiles[this.InstallDir '\' item.Dest] := true - else if ext = 'chm' - hasChm := true - if !(curFile := hashes.Get(item.Dest, '')) - unknownFiles .= item.Dest "`n" - else if destDir = 'v2' && curFile.Version && curFile.Version != this.Version { - this.AddPreAction this.DisplaceFile.Bind(, item.Dest - , 'v' curFile.Version '\' destName '.' ext, curFile.Version) - if ext = 'exe' && FileExist('v2\' destName '_UIA.exe') - this.AddPreAction this.DisplaceFile.Bind(, 'v2\' destName '_UIA.exe' - , 'v' curFile.Version '\' destName '_UIA.exe', '') - } - else if curFile.Hash != HashFile(item.Dest) - modifiedFiles .= item.Dest "`n" - } - - ; Find any scripts being executed by files that will be overwritten - if writeFiles.Has(this.InstallDir '\AutoHotkeyU32.exe') ; Rough check for v1 upgrade - writeFiles[this.InstallDir '\AutoHotkey.exe'] := true - ours(exe) => writeFiles.Has(exe) || writeFiles.Has(StrReplace(exe, '_UIA')) - scripts := this.ScriptsUsingOurFiles(ours) - - ; Show confirmation prompt - message := "" - if scripts.Length { - message .= "The following scripts will be closed automatically:`n" - for w in scripts - message .= this.ScriptTitle(w) "`n" - message .= "`n" - } - if unknownFiles != '' { - message .= "The following files not created by setup will be overwritten:`n" - . unknownFiles - message .= "`n" - } - if modifiedFiles != '' { - message .= "The following files appear to contain modifications that will be lost:`n" - . modifiedFiles - message .= "`n" - } - if message != '' - this.GetConfirmation(message) - - this.CloseScriptsUsingOurFiles(scripts, ours) - } - - PreUninstallChecks(files) { - ours(exe) => files.Has(this.RelativePath(exe)) - scripts := this.ScriptsUsingOurFiles(ours) - this.CloseScriptsUsingOurFiles(scripts, ours) - } - - CloseScriptsUsingOurFiles(scripts, ours) { - ; Close scripts and help files - static WM_CLOSE := 0x10 - for w in WinGetList("AutoHotkey ahk_class HH Parent") - try PostMessage WM_CLOSE,,, w - for w in scripts - try PostMessage WM_CLOSE,,, w - ; Wait for windows/scripts to close - WinWaitClose "AutoHotkey ahk_class HH Parent" - loop { - Sleep 100 - ; Refresh the list in case scripts have started/stopped - scripts := this.ScriptsUsingOurFiles(ours) - ; Prompt again after around 3 seconds of waiting - if scripts.Length && Mod(A_Index, 30) = 0 { - message := "The following scripts must be closed manually before setup can continue:`n" - for w in scripts - message .= this.ScriptTitle(w) "`n" - this.GetConfirmation(message) - } - } until scripts.Length = 0 - } - - ScriptsUsingOurFiles(ours) { - scripts := [], dhw := A_DetectHiddenWindows - DetectHiddenWindows true - for w in WinGetList('ahk_class AutoHotkey') { - if w = A_ScriptHwnd - continue - if ours(WinGetProcessPath(w)) - scripts.Push(w) - } - DetectHiddenWindows dhw - return scripts - } - - ScriptTitle(wnd) { - try - return RegExReplace(WinGetTitle(wnd), ' - AutoHotkey v.*') - catch - return "(unable to retrieve title - already exited?)" - } - - ;} - - ;{ Components to install - - AddCoreFiles(destSubDir) { - this.AddFiles(this.SourceDir, destSubDir - , 'AutoHotkey*.exe', 'AutoHotkey.chm' - ) - this.AddFiles(this.SourceDir, destSubDir = '.' ? 'Compiler' : destSubDir - , 'Compiler\*.bin' ; Legacy base files for compiler - even if Ahk2Exe is not installed yet. - ) - - ; Queue creation of UIA executable files - if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) && VerCompare(this.Version, '1.1.19') >= 0 - Loop Files this.SourceDir '\AutoHotkey*.exe' - this.AddPostAction this.MakeUIA.Bind(, destSubDir '\' A_LoopFileName) - } - - AddMiscFiles() { - this.AddFiles(this.SourceDir, '.', 'license.txt') - } - - AddCompiler(compilerSourceDir) { - this.AddFiles(compilerSourceDir, 'Compiler', 'Ahk2Exe.exe') - this.AddVerb('Compile', 'Compiler\Ahk2Exe.exe', '/in "%l" %*', "Compile script") - this.AddVerb('Compile-Gui', 'Compiler\Ahk2Exe.exe', '/gui /in "%l" %*', "Compile script (GUI)...") - this.AddPostAction this.CreateCompilerShortcut - } - - AddUXFiles() { - this.AddFiles(this.SourceDir '\UX', 'UX', '*.ahk') - this.AddFiles(this.SourceDir '\UX', 'UX\inc', 'inc\*') - this.AddFiles(this.SourceDir '\UX\Templates', 'UX\Templates', '*.ahk') - this.AddPostAction this.CreateStartShortcut - } - - AddSoftwareReg() { - this.AddRegValues(this.SoftwareKey, [ - {ValueName: 'InstallDir', Value: this.InstallDir}, - {ValueName: 'InstallCommand', Value: this.CmdStr('UX\install.ahk', '/install "%1"')}, - {ValueName: 'Version', Value: this.Version}, - ]) - } - - AddUninstallReg() { - this.AddRegValues(this.UninstallKey, [ - {ValueName: 'DisplayName', Value: this.ProductName (this.RootKey = 'HKCU' ? " (user)" : "")}, - {ValueName: 'UninstallString', Value: this.UninstallCmd}, - {ValueName: 'QuietUninstallString', Value: this.QUninstallCmd}, - {ValueName: 'NoModify', Value: 1}, - {ValueName: 'DisplayIcon', Value: this.Interpreter}, - {ValueName: 'DisplayVersion', Value: this.Version}, - {ValueName: 'URLInfoAbout', Value: this.ProductURL}, - {ValueName: 'Publisher', Value: this.Publisher}, - {ValueName: 'InstallLocation', Value: this.InstallDir}, - ]) - - } - - AddFileTypeReg() { - this.AddRegValues(this.ClassesKey, [ - {Key: '.ahk', Value: this.ScriptProgId}, - {Key: '.ahk\ShellNew', ValueName: 'Command', Value: this.CmdStr('UX\ui-newscript.ahk', '"%1"')}, - {Key: '.ahk\ShellNew', ValueName: 'FileName'}, - {Key: '.ahk\PersistentHandler', Value: '{5e941d80-bf96-11cd-b579-08002b30bfeb}'} - ]) - this.AddRegValues(this.FileTypeKey, [ - {Value: "AutoHotkey Script"}, - {ValueName: 'AppUserModelID', Value: this.AppUserModelID}, ; Testing produced inconsistent results, but it seems sometimes this must be here, sometimes under the verb. - {Key: 'DefaultIcon', Value: this.Interpreter ",1"}, - {Key: 'Shell', Value: 'Open runas UIAccess Edit'}, ; Including 'runas' in lower-case fixes the shield icon not appearing on Windows 11. - {Key: 'Shell\Open', ValueName: 'FriendlyAppName', Value: 'AutoHotkey Launcher'}, - ]) - this.AddRunVerbs() - this.AddEditVerbIfUnset() - this.AddPostAction this.NotifyAssocChanged - } - - AddRunVerbs() { - aumid := {ValueName: 'AppUserModelID', Value: this.AppUserModelID} - this.AddVerb('Open', 'UX\launcher.ahk', '"%1" %*', "Run script", - aumid - ) - this.AddVerb('RunAs', 'UX\launcher.ahk', '"%1" %*', - aumid, {ValueName: 'HasLUAShield', Value: ""} - ) - if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) { - this.AddVerb('UIAccess', 'UX\launcher.ahk', '/runwith UIA "%1" %*', - "Run with UI access", aumid) - } - ; Add *Launch as a hidden verb, accessible via Run('*Launch ' file). - this.AddVerb('Launch', 'UX\launcher.ahk', '/Launch "%1" %*', "Launch", - aumid, {ValueName: 'ProgrammaticAccessOnly', Value: ""} - ) - } - - AddEditVerbIfUnset() { - static v1_edit_cmd := 'notepad.exe %1' - ; Add edit verb only if it is undefined or has its default v1 value. - if RegRead(this.FileTypeKey '\Shell\Edit\Command',, v1_edit_cmd) = v1_edit_cmd - this.AddVerb('Edit', 'UX\ui-editor.ahk', '"%1"', "Edit script") - } - - ;} - - ;{ Utility functions - - RelativePath(p) => ( - i := this.InstallDir '\', - SubStr(p, 1, StrLen(i)) = i ? SubStr(p, StrLen(i) + 1) : p - ) - - CmdStr(script, args:='') - => RTrim(Format((InStr(script, '.ahk') ? '"{1}" ' : '') '"{2}\{3}" {4}' - , this.Interpreter, this.InstallDir, script, args)) - - AddRegValues(key, values) { - for v in values { - i := {} - i.Key := key (v.HasProp('Key') ? '\' v.Key : '') - i.ValueName := v.HasProp('ValueName') ? v.ValueName : '' - (v is Primitive) ? i.Value := v : - (v.HasProp('Value')) ? i.Value := v.Value : 0 - this.RegItems.Push(i) - } - } - - AddVerb(name, script, args, values*) { - this.AddRegValues(this.FileTypeKey '\Shell\' name, [ - {Key: 'Command', Value: this.CmdStr(script, args)}, - values* - ]) - } - - AddFileCopy(sourcePath, destPath) { - this.FileItems.Push {Source: sourcePath, Dest: destPath} - } - - AddFiles(sourceDir, destSubDir, patterns*) { - destSubDir := (destSubDir != '.' ? destSubDir '\' : '') - for p in patterns { - Loop Files sourceDir '\' p { - this.AddFileCopy A_LoopFileFullPath, destSubDir . A_LoopFileName - } - } - } - - AddPreCheck(f) => this.PreCheck.Push(f) - AddPreAction(f) => this.PreAction.Push(f) - AddPostAction(f) => this.PostAction.Push(f) - - ReadHashes() { - ; For maintainability, don't assume the caller has set the working dir. - wd := A_WorkingDir, A_WorkingDir := this.InstallDir - hashes := ReadHashes(this.HashesPath, item => FileExist(item.Path)) - A_WorkingDir := wd - return hashes - } - - AddFileHash(f, v) { - this.Hashes[f] := {Path: f, Hash: HashFile(f), Version: v} - } - - WriteHashes() { - s := "Hash,Version,Path,Description`r`n" - for ,item in this.Hashes { - if !item.HasProp('Description') { - try ; Cache the file description for the launcher - exe := GetExeInfo(item.Path) - catch - item.Description := "" - else { - item.Description := exe.Description - if InStr(item.Description, 'AutoHotkey') - item.Version := exe.Version ; Ensure accuracy for the launcher - } - } - s .= Format('{},{},"{}","{}"`r`n', item.Hash, item.Version, item.Path, item.Description) - } - FileOpen(this.HashesPath, 'w').Write(s) - } - - GetComponents(versionFilter?) { - callerwd := A_WorkingDir - SetWorkingDir this.InstallDir - versions := Map() - maxes := Map() - for , fh in this.Hashes { - if fh.Path ~= 'i)^UX\\|^[A-Z]:|^\\\\|^(WindowSpy\.ahk|license\.txt)$' - . '|^Compiler\\(?!.*\.bin$)' - continue - try fh.Version := GetExeInfo(fh.Path = 'AutoHotkey.chm' ? 'AutoHotkeyU32.exe' : fh.Path).Version ; Auto-fix inaccurate versions in Hashes - if !files := versions.Get(fh.Version, 0) { - if IsSet(versionFilter) && !versionFilter(fh.Version) - continue - versions[fh.Version] := files := [] - v := RegExReplace(fh.Version, '^\d+\.\d+\b\K.*') - if VerCompare(prevMax := maxes.Get(v, ''), fh.Version) < 0 { - maxes[v] := fh.Version - if prevMax != '' - versions[prevMax].superseded := true - } - else - files.superseded := true - } - files.InsertAt(1, fh) - } - SetWorkingDir callerwd - return versions - } - - NotifyAssocChanged() { - DllCall("shell32\SHChangeNotify", "uint", 0x08000000 ; SHCNE_ASSOCCHANGED - , "uint", 0, "ptr", 0, "ptr", 0) - } - - GetConfirmation(message, icon:='!') { - if !this.Silent && MsgBox(message, this.DialogTitle, 'Icon' icon ' OkCancel') = 'Cancel' - ExitApp 1 - } - - WarnBox(message) { - if !this.Silent - MsgBox message, this.DialogTitle, "Icon!" - } - - InfoBox(message) { - if !this.Silent - MsgBox message, this.DialogTitle, "Iconi" - } - - FatalError(message) { - if !this.Silent - MsgBox message, this.DialogTitle, 'Iconx' - ExitApp 1 - } - - GetTargetUX() { - try { - ; For registered installations, InstallCommand allows for future changes. - return { - Version: RegRead(this.SoftwareKey, 'Version'), - InstallCommand: RegRead(this.SoftwareKey, 'InstallCommand') - } - } - try { - ; Target installation not in registry, or has no InstallCommand (e.g. too old). - ; Allow non-registry installations that follow protocol as commented below. - ux := {} - ; Version information must be provided by the file at this.HashesPath: - ux.Version := this.Hashes['UX\install.ahk'].Version - ; Interpreter must be located at the path calculated below: - interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe' - if FileExist(interpreter) { - ; Additional interpreters must be installable with this command line: - ux.InstallCommand := Format('"{1}" "{2}\UX\install.ahk" /install "%1"' - , interpreter, this.InstallDir) - return ux - } - } - ; Otherwise, UX script or appropriate interpreter not found. - } - - ; Delete a symbolic link, or do nothing if path does not refer to a symbolic link. - DeleteLink(path) { - switch this.GetLinkAttrib(path) { - case 'D': DirDelete path - case 'F': FileDelete path - } - } - - GetLinkAttrib(path) { - attrib := DllCall('GetFileAttributes', 'str', path) - ; FILE_ATTRIBUTE_REPARSE_POINT = 0x400 - ; FILE_ATTRIBUTE_DIRECTORY = 0x10 - return (attrib != -1 && (attrib & 0x400)) ? ((attrib & 0x10) ? 'D' : 'F') : '' - } - - UpdateV2Link() { - ; Create a symlink for AutoHotkey.exe to simplify use by tools. - DllCall('CreateSymbolicLink', 'str', this.InstallDir '\v2\AutoHotkey.exe' - , 'str', 'AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe', 'uint', 0) - } - - CreateWindowSpyRedirect() { - ; Permit overwrite only when upgrading a legacy v1 installation, - ; or if it is known to have been created by us. - if !FileExist('WindowSpy.ahk') || this.HasOwnProp('SoftwareKeyV1') - || this.Hashes.Get('WindowSpy.ahk', {Hash: ''}).Hash = HashFile('WindowSpy.ahk') { - FileOpen('WindowSpy.ahk', 'w').Write(' - ( - #include UX - #include inc\bounce-v1.ahk - /**/ - #requires AutoHotkey v2.0 - try Run('"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'), ExitApp() - #include WindowSpy.ahk - )') - this.AddFileHash('WindowSpy.ahk', this.Version) - } - } - - CreateStartShortcut() { - CreateAppShortcut( - lnk := this.StartFolder '\AutoHotkey.lnk', { - target: this.Interpreter, - args: Format('"{1}\UX\ui-dash.ahk"', this.InstallDir), - desc: "AutoHotkey Dash", - aumid: this.AppUserModelID, - uninst: this.UninstallCmd - } - ) - this.AddFileHash lnk, this.Version - - CreateAppShortcut( - lnk := this.StartFolder '\AutoHotkey Window Spy.lnk', { - target: this.Interpreter, - args: Format('"{1}\UX\WindowSpy.ahk"', this.InstallDir), - desc: "AutoHotkey Window Spy", - aumid: 'AutoHotkey.WindowSpy', - icon: Format('{1}\UX\inc\spy.ico', this.InstallDir), iconIndex: 0, - uninst: this.UninstallCmd - } - ) - this.AddFileHash lnk, this.Version - } - - CreateCompilerShortcut() { - CreateAppShortcut( - lnk := this.StartFolder '\Ahk2Exe.lnk', { - target: this.InstallDir '\Compiler\Ahk2Exe.exe', - desc: "Convert .ahk to .exe", - aumid: 'AutoHotkey.Ahk2Exe', - uninst: this.UninstallCmd - } - ) - this.AddFileHash lnk, this.Version - } - - MakeUIA(baseFile) { - SplitPath baseFile,, &baseDir,, &baseName - baseDir := baseDir = '.' ? '' : baseDir '\' - FileCopy baseFile, newPath := baseDir baseName '_UIA.exe', true - static abort := false ; Let "Abort" disable MakeUIA calls, but let other PostActions complete. - while !abort { - try { - EnableUIAccess newPath - break - } - catch as e { - try FileDelete newPath - if e.What != "EndUpdateResource" - throw - if this.Silent { - if A_Index > 4 - break - Sleep 500 - } - switch MsgBox("Unable to create " baseName ". Try adding an exclusion in your antivirus software. If that doesn't work, please report the issue.`n`nError: " e.Message - ,, "a/r/i") { - case "Abort": abort := true - case "Ignore": break - } - } - } - this.AddFileHash newPath, '' ; For uninstall - } - - IsTrustedLocation(path) { ; http://msdn.com/library/bb756929 - other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") - return InStr(path, A_ProgramFiles "\") = 1 - || other && InStr(path, other "\") = 1 - } - - ;} - - ;{ Upgrade from v1 - - PrepareUpgradeV1(installedVersion) { - ; This needs to be done before conflict-checking - if FileExist('license.txt') - this.AddFileHash('license.txt', installedVersion) - } - - UpgradeV1(installedVersion) { - try { ; Permit failure in case AutoHotkey.exe has been deleted. - exe := GetExeInfo('AutoHotkey.exe') - build := RegExReplace(exe.Description, '^AutoHotkey *') - } - - ; Set default launcher settings - if IsSet(build) && ConfigRead('Launcher\v1', 'Build', '!') = '!' - ConfigWrite(build, 'Launcher\v1', 'Build') - if ConfigRead('Launcher\v1', 'UTF8', '') = '' - && InStr(RegRead('HKCR\' this.ScriptProgId '\Shell\Open\Command',, ''), '/cp65001 ') - ConfigWrite(true, 'Launcher\v1', 'UTF8') - - ; Record these for Uninstall - add 'AutoHotkey{1}.exe', '', 'A32', 'U32', 'U64', 'A32_UIA', 'U32_UIA', 'U64_UIA' - add 'Compiler\{1}.bin', 'ANSI 32-bit', 'Unicode 32-bit', 'Unicode 64-bit', 'AutoHotkeySC' - add '{1}', 'Compiler\Ahk2Exe.exe', 'AutoHotkey.chm', A_WinDir '\ShellNew\Template.ahk' - - add(fmt, patterns*) { - for p in patterns - if FileExist(f := Format(fmt, p)) - this.AddFileHash f, installedVersion - } - - ; Remove obsolete files - for item in ['Installer.ahk', 'AutoHotkey Website.url'] - try FileDelete item - - ; Remove the v1 shortcuts from the Start menu - name := RegRead(this.SoftwareKeyV1, 'StartMenuFolder', '') - if name != '' - try DirDelete A_ProgramsCommon '\' name, true - - ; Remove the old sub-keys, which might be in the wrong reg view - try RegDeleteKey this.SoftwareKeyV1 - try RegDeleteKey this.UninstallKeyV1 - } - - DisplaceFile(sourcePath, destPath, version) { - SplitPath destPath,, &destDir - if destDir != "" - DirCreate destDir - FileMove sourcePath, destPath - try this.Hashes.Delete(sourcePath) - this.AddFileHash destPath, version - } - - DisplaceV1(v) { - DirCreate dir := 'v' v - displace(path) { - if FileExist(path) { - SplitPath path, &name - this.DisplaceFile path, dir '\' name, v - } - } - for build in ['U32', 'U64', 'A32'] { - displace 'AutoHotkey' build '.exe' - displace 'AutoHotkey' build '_UIA.exe' - } - try - defaultBinSize := FileGetSize(defaultBinPath := 'Compiler\AutoHotkeySC.bin') - for build in ['Unicode 32-bit', 'Unicode 64-bit', 'ANSI 32-bit'] { - try - if FileGetSize('Compiler\' build '.bin') = defaultBinSize - this.AddFileCopy this.InstallDir '\Compiler\' build '.bin', defaultBinPath - displace 'Compiler\' build '.bin' - } - - displace 'AutoHotkey.chm' - try { - exe := GetExeInfo('AutoHotkey.exe') - if exe.Version = v - && RegExMatch(exe.Description, ' (A|U)\w+ (32|64)-bit$', &m) { - ; Too early to add to FileItems, so use PostAction: - this.AddPostAction this.CopyDefaultExe.Bind(, 'AutoHotkey' m.1 m.2 '.exe') - } - } - } - - CopyDefaultExe(from) { - try - FileCopy from, 'AutoHotkey.exe', true - catch - return ; TODO: report to user? - this.AddFileHash 'AutoHotkey.exe', this.Version - } - - ;} -} +; This script contains AutoHotkey (un)installation routines. +; See the AutoHotkey v2 documentation for usage. +#include inc\bounce-v1.ahk +/* v1 stops here */ +#requires AutoHotkey v2.0 + +#SingleInstance Off ; Needed for elevation with *runas. + +#include inc\launcher-common.ahk +#include inc\HashFile.ahk +#include inc\CreateAppShortcut.ahk +#include inc\EnableUIAccess.ahk +#include inc\ShellRun.ahk + +if A_LineFile = A_ScriptFullPath + Install_Main + +Install_Main() { + try { + Installation.Instance := inst := Installation() + method := 'InstallFull' + params := [] + while A_Index <= A_Args.Length { + switch A_Args[A_Index], 'off' { + case '/install': + method := 'InstallExtraVersion' + inst.SourceDir := A_Args[++A_Index] + case '/uninstall': + method := 'Uninstall' + if A_Index < A_Args.Length && SubStr(A_Args[A_Index+1],1,1) != '/' + params.Push(A_Args[++A_Index]) + case '/to', '/installto': + inst.InstallDir := A_Args[++A_Index] + case '/elevate': + inst.RequireAdmin := true + case '/user': + inst.UserInstall := true + case '/silent': + inst.Silent := true + default: + MsgBox 'Invalid arg "' A_Args[A_Index] '"', inst.DialogTitle, "Iconx" + ExitApp 1 + } + } + inst.%method%(params*) + } + catch as e { + DllCall(CallbackCreate(errBox.Bind(e))) + ExitApp 1 + } + errBox(e) { + throw e + } +} + +class Installation { + ProductName := "AutoHotkey" + ProductURL := "https://autohotkey.com" + Publisher := "AutoHotkey Foundation LLC" + Version := A_AhkVersion + AppUserModelID := 'AutoHotkey.AutoHotkey' + + UserInstall := !A_IsAdmin + Interpreter := A_AhkPath + Silent := false + + ScriptProgId := 'AutoHotkeyScript' + SoftwareSubKey := 'Software\AutoHotkey' + RootKey => this.UserInstall ? 'HKCU' : 'HKLM' + SoftwareKey => this.RootKey '\' this.SoftwareSubKey + ClassesKey => this.RootKey '\Software\Classes' + FileTypeKey => this.ClassesKey '\' this.ScriptProgId + UninstallKey => this.RootKey '\Software\Microsoft\Windows\CurrentVersion\Uninstall\AutoHotkey' + StartFolder => (this.UserInstall ? A_Programs : A_ProgramsCommon) + UninstallCmd => this.CmdStr('UX\ui-uninstall.ahk', ((A_IsAdmin && this.UserInstall) ? '/elevate' : '')) + QUninstallCmd => this.CmdStr('UX\install.ahk', '/uninstall /silent') + + DialogTitle => this.ProductName " Setup" + + FileItems := [] ; [{Source, Dest}] + RegItems := [] ; [{Key, ValueName, Value}] + PreCheck := [] ; [Callback(this)] + PreAction := [] ; [Callback(this)] + PostAction := [] ; [Callback(this)] + + ResolveInstallDir() { + if hadInstallDir := this.HasProp('InstallDir') + DirCreate installDir := this.InstallDir + else + installDir := IsSet(InstallUtil) ? InstallUtil.DefaultDir : A_ScriptDir '\..' + Loop Files installDir, 'D' + installDir := A_LoopFileFullPath + this.InstallDir := installDir + SetRegView 64 + installDirs := [] + for rootKey in ['HKLM', 'HKCU'] + installDirs.Push RegRead(rootKey '\' this.SoftwareSubKey, 'InstallDir', '') + ; Override installation mode if already installed here + if installDirs[1] = installDir + this.UserInstall := false + else if installDirs[2] = installDir + this.UserInstall := true + ; If this.InstallDir wasn't set upon entry to this method... + else if !hadInstallDir { + ; Default to the location of an existing installation matching this.UserInstall + if installDirs[this.UserInstall?2:1] + this.InstallDir := installDirs[this.UserInstall?2:1] + ; Default to the location and mode of any other existing installation + else if installDirs[this.UserInstall?1:2] { + if !A_IsAdmin && this.UserInstall { + ; Use the existing all-user installation only if elevation is successful + try + RunWait '*runas ' DllCall('GetCommandLine', 'str') + catch + return ; Presume user cancelled; continue as user + else + ExitApp + } + this.InstallDir := installDirs[this.UserInstall?1:2], this.UserInstall := !this.UserInstall + } + } + } + + ResolveSourceDir() { + if !this.HasProp('SourceDir') { + if A_IsCompiled && IsSet(UnpackFiles) + this.SourceDir := UnpackFiles(this.InstallDir) + else + this.SourceDir := A_ScriptDir '\..' + } + Loop Files this.SourceDir, 'D' + this.SourceDir := A_LoopFileFullPath + else + throw ValueError("Invalid source directory",, this.SourceDir) + } + + HashesPath => this.InstallDir '\UX\installed-files.csv' + Hashes => ( + this.DefineProp('Hashes', {value: hashes := this.ReadHashes()}), + hashes + ) + + Apply() { + if !DirExist(this.InstallDir) + DirCreate this.InstallDir + SetWorkingDir this.InstallDir + + ; Execute pre-check actions + for item in this.PreCheck + item(this) + + ; Detect possible conflicts before taking action + this.PreApplyChecks() + + ; Execute pre-install actions + for item in this.PreAction + item(this) + + ; Install files + for item in this.FileItems { + SplitPath item.Dest,, &destDir + if destDir != '' + DirCreate destDir + try + FileCopy item.Source, item.Dest, true + catch + this.WarnBox 'Copy failed`nsource: ' item.Source '`ndest: ' item.Dest + else { + ; If source files were extracted from a zip, they may have a Zone.Identifier + ; stream identifying them as coming from the Internet. These must be deleted + ; to prevent warnings when the user runs the installed files. + try FileDelete item.Dest ":Zone.Identifier" + this.AddFileHash(item.Dest, this.Version) + } + } + + ; Install registry settings + for item in this.RegItems { + if item.HasProp('Value') { + RegWrite(item.Value, item.Value is Integer ? 'REG_DWORD' : 'REG_SZ' + , item.Key, item.ValueName) + } else { + try RegDelete(item.Key, item.ValueName) + } + } + + ; Execute post-install actions + for item in this.PostAction + item(this) + + ; Write file list to disk + if this.Hashes.Count + this.WriteHashes + } + + ElevationNeeded => !A_IsAdmin && (!this.UserInstall || this.HasProp('RequireAdmin')) + + ElevateIfNeeded() { + if this.ElevationNeeded { + try RunWait '*runas ' DllCall('GetCommandLine', 'str') + ExitApp + } + } + + ;{ Installation entry points + + InstallFull() { + SetRegView 64 + + this.ResolveInstallDir + this.ResolveSourceDir + + doFiles := this.InstallDir != this.SourceDir + + ; If a newer version is already installed, integrate with it + ux := doFiles && this.GetTargetUX() + if ux && VerCompare(ux.Version, this.Version) > 0 { + cmd := StrReplace(ux.InstallCommand, '%1', this.SourceDir,, &replaced) + if !replaced + cmd .= ' "' this.SourceDir '"' + if this.ElevationNeeded + cmd := '*runas ' cmd + try + exitCode := RunWait(cmd, this.InstallDir) + catch as e + if InStr(e.Message e.Extra, 'cancel') + exitCode := 1 + else + throw e + ExitApp exitCode + } + + this.ElevateIfNeeded + + ; If a legacy version is installed, upgrade it + wowKey(k) => StrReplace(k, '\Software\', '\Software\Wow6432Node\') + installedVersion := RegRead(key := wowKey(this.SoftwareKey), 'Version', '') + || RegRead(key := this.SoftwareKey, 'Version', '') + if SubStr(installedVersion, 1, 2) = '1.' { + this.SoftwareKeyV1 := key + this.UninstallKeyV1 := InStr(key, 'Wow64') ? wowKey(this.UninstallKey) : this.UninstallKey + this.AddPreCheck this.PrepareUpgradeV1.Bind(, installedVersion) + this.AddPreAction this.UpgradeV1.Bind(, installedVersion) + } + + if doFiles || FileExist(this.InstallDir '\UX\AutoHotkeyUX.exe') + this.Interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe' + + if doFiles { + if this.GetLinkAttrib(this.InstallDir '\v2') + this.AddPreAction this.DeleteLink.Bind(, 'v2') + this.AddCoreFiles 'v2' + ; Give UX its own AutoHotkey.exe for a few reasons: + ; 1. The Start menu shortcut needs a stable path, since pinning to taskbar creates + ; a copy that won't get updated (obsolete since using 'v2' and DisplaceFile now). + ; 2. LauncherConfigGui may store the path under HKCU, which mightn't get updated. + ; 3. It helps differentiate the launcher from other scripts in Task Manager. + ; 4. It makes the UX scripts independent from the installed interpreters. + srcExe := this.SourceDir '\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe' + dstExe := this.InstallDir '\UX\AutoHotkeyUX.exe' + if FileExist(srcExe) || (!FileExist(dstExe) && (srcExe := A_AhkPath)) + this.AddFileCopy srcExe, 'UX\AutoHotkeyUX.exe' ; Must be relative + this.AddPostAction this.UpdateV2Link + + this.AddUXFiles + this.AddMiscFiles + } + + this.AddPostAction this.CreateWindowSpyRedirect + + this.AddUninstallReg + this.AddSoftwareReg + this.AddFileTypeReg + + this.Apply + + if FileExist(this.InstallDir '\UX\reset-assoc.ahk') + RunWait this.CmdStr('UX\reset-assoc.ahk', '/check') + + if !this.Silent + ShellRun this.Interpreter, 'UX\ui-dash.ahk', this.InstallDir + } + + InstallExtraVersion() { + SetRegView 64 + + this.ResolveInstallDir + this.ResolveSourceDir + + Loop Files this.SourceDir '\AutoHotkey*.exe' { + exe := GetExeInfo(A_LoopFilePath) + break + } else + throw Error("AutoHotkey*.exe not found in source directory",, this.SourceDir) + + this.ElevateIfNeeded + + coreDir := 'v' (this.Version := exe.Version) + + if VerCompare(this.Version, '2.0-') < 0 { + try + exe := GetExeInfo(this.InstallDir '\AutoHotkeyU32.exe') + catch + {} + else if VerCompare(this.Version, exe.Version) > 0 + && !FileExist(this.InstallDir '\v' exe.Version) { + this.AddPreAction this.DisplaceV1.Bind(, exe.Version) + coreDir := '.' + } + } + + this.AddCoreFiles(coreDir) + + if FileExist(this.SourceDir '\Compiler\Ahk2Exe.exe') { + compilerVersion := GetExeInfo(this.SourceDir '\Compiler\Ahk2Exe.exe').Version + installedCompiler := this.Hashes.Get('Compiler\Ahk2Exe.exe', '') + if !installedCompiler || VerCompare(compilerVersion, installedCompiler.Version) > 0 + this.AddCompiler(this.SourceDir '\Compiler') + } + + this.Apply + } + + ;} + + ;{ Uninstallation + + UninstallRegistry() { + SetRegView 64 + delKey this.FileTypeKey + delKey this.ClassesKey '\.ahk' + delKey this.SoftwareKey + delKey this.UninstallKey + if this.RootKey = 'HKLM' { + delKey 'HKCU\' this.SoftwareSubKey + delKey 'HKCU\Software\Classes\' this.ScriptProgId + for k in ['AutoHotkey.exe', 'Ahk2Exe.exe'] ; made by v1 installer + delKey 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' k + } + + delKey(key) { + try + RegDeleteKey key + catch OSError as e + if e.number != 2 ; ERROR_FILE_NOT_FOUND + throw + } + + this.NotifyAssocChanged + } + + GetHashesForVersions(versions) { + versions := ',' versions ',' + files := Map(), files.CaseSense := "off" + for , cfiles in this.GetComponents(v => InStr(versions, ',' v ',')) + for fh in cfiles + files[fh.Path] := fh + return files + } + + Uninstall(versions:='') { + this.ResolveInstallDir + + this.ElevateIfNeeded + + files := versions = '' ? this.Hashes.Clone() : this.GetHashesForVersions(versions) + if !files.Count && versions = '' + this.GetConfirmation("Installation data missing. Files will not be deleted.", 'x') + + ; Close scripts and help files + this.PreUninstallChecks files + + ; Remove from registry only if being fully uninstalled + if versions = '' + this.UninstallRegistry + + ; Remove files + SetWorkingDir this.InstallDir + modified := "" + dirs := "" + for path, f in files { + if !FileExist(path) + continue + if HashFile(path) != f.Hash { + modified .= "`n" path + continue + } + if this.InstallDir '\' path = A_AhkPath { + postponed := A_AhkPath + this.Hashes.Delete(path) + continue + } + FileDelete path + this.Hashes.Delete(path) + SplitPath path,, &dir + if dir != "" + dirs .= dir "`n" + } + if modified != "" { + this.InfoBox("The following files were not deleted as they appear to have been modified:" + . modified) + } + + ; Update or remove hashes file + if this.Hashes.Count + this.WriteHashes + else + try FileDelete this.HashesPath + + ; Remove empty directories + for dir in StrSplit(Sort(RTrim(dirs, "`n"), 'UR'), "`n") { + this.DeleteLink dir '\AutoHotkey.exe' + try DirDelete dir, false + } + + if versions = '' ; Full uninstall + this.DeleteLink this.InstallDir '\v2' + + if IsSet(postponed) { + ; Try delete via cmd.exe after we exit + Run(A_ComSpec ' /c "' + 'ping -n 2 127.1 > NUL & ' + 'del "' postponed '" & ' + 'cd %TEMP% & ' + 'rmdir "' postponed '\.." & ' + 'rmdir "' A_WorkingDir '"' + '"',, 'Hide') + } + ExitApp + } + + ;} + + ;{ Conflict prevention + + PreApplyChecks() { + ; Check for files that might be overwritten + writeFiles := Map(), writeFiles.CaseSense := 'off' + hasChm := false + unknownFiles := '' + modifiedFiles := '' + hashes := this.Hashes + if (item := hashes.Get(this.InstallDir '\UX\AutoHotkeyUX.exe', false)) ; Erroneous entry by v2.0-beta.4 + hashes.Delete(item.Path), hashes[item.Path := 'UX\AutoHotkeyUX.exe'] := item ; Make it relative + for item in this.FileItems { + if !(attrib := FileExist(item.Dest)) + continue + if InStr(attrib, 'D') + this.FatalError("The following file cannot be installed because a directory by this name already exists:`n" + item.Dest "`n`nNo changes have been made.") + SplitPath item.Dest,, &destDir, &ext, &destName + if destDir = 'v2' && this.GetLinkAttrib(destDir) + continue ; Symlink should be deleted, so item.Dest won't exist. + if ext = 'exe' + writeFiles[this.InstallDir '\' item.Dest] := true + else if ext = 'chm' + hasChm := true + if !(curFile := hashes.Get(item.Dest, '')) + unknownFiles .= item.Dest "`n" + else if destDir = 'v2' && curFile.Version && curFile.Version != this.Version { + this.AddPreAction this.DisplaceFile.Bind(, item.Dest + , 'v' curFile.Version '\' destName '.' ext, curFile.Version) + if ext = 'exe' && FileExist('v2\' destName '_UIA.exe') + this.AddPreAction this.DisplaceFile.Bind(, 'v2\' destName '_UIA.exe' + , 'v' curFile.Version '\' destName '_UIA.exe', '') + } + else if curFile.Hash != HashFile(item.Dest) + modifiedFiles .= item.Dest "`n" + } + + ; Find any scripts being executed by files that will be overwritten + if writeFiles.Has(this.InstallDir '\AutoHotkeyU32.exe') ; Rough check for v1 upgrade + writeFiles[this.InstallDir '\AutoHotkey.exe'] := true + ours(exe) => writeFiles.Has(exe) || writeFiles.Has(StrReplace(exe, '_UIA')) + scripts := this.ScriptsUsingOurFiles(ours) + + ; Show confirmation prompt + message := "" + if scripts.Length { + message .= "The following scripts will be closed automatically:`n" + for w in scripts + message .= this.ScriptTitle(w) "`n" + message .= "`n" + } + if unknownFiles != '' { + message .= "The following files not created by setup will be overwritten:`n" + . unknownFiles + message .= "`n" + } + if modifiedFiles != '' { + message .= "The following files appear to contain modifications that will be lost:`n" + . modifiedFiles + message .= "`n" + } + if message != '' + this.GetConfirmation(message) + + this.CloseScriptsUsingOurFiles(scripts, ours) + } + + PreUninstallChecks(files) { + ours(exe) => files.Has(this.RelativePath(exe)) + scripts := this.ScriptsUsingOurFiles(ours) + this.CloseScriptsUsingOurFiles(scripts, ours) + } + + CloseScriptsUsingOurFiles(scripts, ours) { + ; Close scripts and help files + static WM_CLOSE := 0x10 + for w in WinGetList("AutoHotkey ahk_class HH Parent") + try PostMessage WM_CLOSE,,, w + for w in scripts + try PostMessage WM_CLOSE,,, w + ; Wait for windows/scripts to close + WinWaitClose "AutoHotkey ahk_class HH Parent" + loop { + Sleep 100 + ; Refresh the list in case scripts have started/stopped + scripts := this.ScriptsUsingOurFiles(ours) + ; Prompt again after around 3 seconds of waiting + if scripts.Length && Mod(A_Index, 30) = 0 { + message := "The following scripts must be closed manually before setup can continue:`n" + for w in scripts + message .= this.ScriptTitle(w) "`n" + this.GetConfirmation(message) + } + } until scripts.Length = 0 + } + + ScriptsUsingOurFiles(ours) { + scripts := [], dhw := A_DetectHiddenWindows + DetectHiddenWindows true + for w in WinGetList('ahk_class AutoHotkey') { + if w = A_ScriptHwnd + continue + if ours(WinGetProcessPath(w)) + scripts.Push(w) + } + DetectHiddenWindows dhw + return scripts + } + + ScriptTitle(wnd) { + try + return RegExReplace(WinGetTitle(wnd), ' - AutoHotkey v.*') + catch + return "(unable to retrieve title - already exited?)" + } + + ;} + + ;{ Components to install + + AddCoreFiles(destSubDir) { + this.AddFiles(this.SourceDir, destSubDir + , 'AutoHotkey*.exe', 'AutoHotkey.chm' + ) + this.AddFiles(this.SourceDir, destSubDir = '.' ? 'Compiler' : destSubDir + , 'Compiler\*.bin' ; Legacy base files for compiler - even if Ahk2Exe is not installed yet. + ) + + ; Queue creation of UIA executable files + if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) && VerCompare(this.Version, '1.1.19') >= 0 + Loop Files this.SourceDir '\AutoHotkey*.exe' + this.AddPostAction this.MakeUIA.Bind(, destSubDir '\' A_LoopFileName) + } + + AddMiscFiles() { + this.AddFiles(this.SourceDir, '.', 'license.txt') + } + + AddCompiler(compilerSourceDir) { + this.AddFiles(compilerSourceDir, 'Compiler', 'Ahk2Exe.exe') + this.AddVerb('Compile', 'Compiler\Ahk2Exe.exe', '/in "%l" %*', "Compile script") + this.AddVerb('Compile-Gui', 'Compiler\Ahk2Exe.exe', '/gui /in "%l" %*', "Compile script (GUI)...") + this.AddPostAction this.CreateCompilerShortcut + } + + AddUXFiles() { + this.AddFiles(this.SourceDir '\UX', 'UX', '*.ahk') + this.AddFiles(this.SourceDir '\UX', 'UX\inc', 'inc\*') + this.AddFiles(this.SourceDir '\UX\Templates', 'UX\Templates', '*.ahk') + this.AddPostAction this.CreateStartShortcut + } + + AddSoftwareReg() { + this.AddRegValues(this.SoftwareKey, [ + {ValueName: 'InstallDir', Value: this.InstallDir}, + {ValueName: 'InstallCommand', Value: this.CmdStr('UX\install.ahk', '/install "%1"')}, + {ValueName: 'Version', Value: this.Version}, + ]) + } + + AddUninstallReg() { + this.AddRegValues(this.UninstallKey, [ + {ValueName: 'DisplayName', Value: this.ProductName (this.RootKey = 'HKCU' ? " (user)" : "")}, + {ValueName: 'UninstallString', Value: this.UninstallCmd}, + {ValueName: 'QuietUninstallString', Value: this.QUninstallCmd}, + {ValueName: 'NoModify', Value: 1}, + {ValueName: 'DisplayIcon', Value: this.Interpreter}, + {ValueName: 'DisplayVersion', Value: this.Version}, + {ValueName: 'URLInfoAbout', Value: this.ProductURL}, + {ValueName: 'Publisher', Value: this.Publisher}, + {ValueName: 'InstallLocation', Value: this.InstallDir}, + ]) + + } + + AddFileTypeReg() { + this.AddRegValues(this.ClassesKey, [ + {Key: '.ahk', Value: this.ScriptProgId}, + {Key: '.ahk\ShellNew', ValueName: 'Command', Value: this.CmdStr('UX\ui-newscript.ahk', '"%1"')}, + {Key: '.ahk\ShellNew', ValueName: 'FileName'}, + {Key: '.ahk\PersistentHandler', Value: '{5e941d80-bf96-11cd-b579-08002b30bfeb}'} + ]) + this.AddRegValues(this.FileTypeKey, [ + {Value: "AutoHotkey Script"}, + {ValueName: 'AppUserModelID', Value: this.AppUserModelID}, ; Testing produced inconsistent results, but it seems sometimes this must be here, sometimes under the verb. + {Key: 'DefaultIcon', Value: this.Interpreter ",1"}, + {Key: 'Shell', Value: 'Open runas UIAccess Edit'}, ; Including 'runas' in lower-case fixes the shield icon not appearing on Windows 11. + {Key: 'Shell\Open', ValueName: 'FriendlyAppName', Value: 'AutoHotkey Launcher'}, + ]) + this.AddRunVerbs() + this.AddEditVerbIfUnset() + this.AddPostAction this.NotifyAssocChanged + } + + AddRunVerbs() { + aumid := {ValueName: 'AppUserModelID', Value: this.AppUserModelID} + this.AddVerb('Open', 'UX\launcher.ahk', '"%1" %*', "Run script", + aumid + ) + this.AddVerb('RunAs', 'UX\launcher.ahk', '"%1" %*', + aumid, {ValueName: 'HasLUAShield', Value: ""} + ) + if A_IsAdmin && this.IsTrustedLocation(this.InstallDir) { + this.AddVerb('UIAccess', 'UX\launcher.ahk', '/runwith UIA "%1" %*', + "Run with UI access", aumid) + } + ; Add *Launch as a hidden verb, accessible via Run('*Launch ' file). + this.AddVerb('Launch', 'UX\launcher.ahk', '/Launch "%1" %*', "Launch", + aumid, {ValueName: 'ProgrammaticAccessOnly', Value: ""} + ) + } + + AddEditVerbIfUnset() { + static v1_edit_cmd := 'notepad.exe %1' + ; Add edit verb only if it is undefined or has its default v1 value. + if RegRead(this.FileTypeKey '\Shell\Edit\Command',, v1_edit_cmd) = v1_edit_cmd + this.AddVerb('Edit', 'UX\ui-editor.ahk', '"%1"', "Edit script") + } + + ;} + + ;{ Utility functions + + RelativePath(p) => ( + i := this.InstallDir '\', + SubStr(p, 1, StrLen(i)) = i ? SubStr(p, StrLen(i) + 1) : p + ) + + CmdStr(script, args:='') + => RTrim(Format((InStr(script, '.ahk') ? '"{1}" ' : '') '"{2}\{3}" {4}' + , this.Interpreter, this.InstallDir, script, args)) + + AddRegValues(key, values) { + for v in values { + i := {} + i.Key := key (v.HasProp('Key') ? '\' v.Key : '') + i.ValueName := v.HasProp('ValueName') ? v.ValueName : '' + (v is Primitive) ? i.Value := v : + (v.HasProp('Value')) ? i.Value := v.Value : 0 + this.RegItems.Push(i) + } + } + + AddVerb(name, script, args, values*) { + this.AddRegValues(this.FileTypeKey '\Shell\' name, [ + {Key: 'Command', Value: this.CmdStr(script, args)}, + values* + ]) + } + + AddFileCopy(sourcePath, destPath) { + this.FileItems.Push {Source: sourcePath, Dest: destPath} + } + + AddFiles(sourceDir, destSubDir, patterns*) { + destSubDir := (destSubDir != '.' ? destSubDir '\' : '') + for p in patterns { + Loop Files sourceDir '\' p { + this.AddFileCopy A_LoopFileFullPath, destSubDir . A_LoopFileName + } + } + } + + AddPreCheck(f) => this.PreCheck.Push(f) + AddPreAction(f) => this.PreAction.Push(f) + AddPostAction(f) => this.PostAction.Push(f) + + ReadHashes() { + ; For maintainability, don't assume the caller has set the working dir. + wd := A_WorkingDir, A_WorkingDir := this.InstallDir + hashes := ReadHashes(this.HashesPath, item => FileExist(item.Path)) + A_WorkingDir := wd + return hashes + } + + AddFileHash(f, v) { + this.Hashes[f] := {Path: f, Hash: HashFile(f), Version: v} + } + + WriteHashes() { + s := "Hash,Version,Path,Description`r`n" + for ,item in this.Hashes { + if !item.HasProp('Description') { + try ; Cache the file description for the launcher + exe := GetExeInfo(item.Path) + catch + item.Description := "" + else { + item.Description := exe.Description + if InStr(item.Description, 'AutoHotkey') + item.Version := exe.Version ; Ensure accuracy for the launcher + } + } + s .= Format('{},{},"{}","{}"`r`n', item.Hash, item.Version, item.Path, item.Description) + } + FileOpen(this.HashesPath, 'w').Write(s) + } + + GetComponents(versionFilter?) { + callerwd := A_WorkingDir + SetWorkingDir this.InstallDir + versions := Map() + maxes := Map() + for , fh in this.Hashes { + if fh.Path ~= 'i)^UX\\|^[A-Z]:|^\\\\|^(WindowSpy\.ahk|license\.txt)$' + . '|^Compiler\\(?!.*\.bin$)' + continue + try fh.Version := GetExeInfo(fh.Path = 'AutoHotkey.chm' ? 'AutoHotkeyU32.exe' : fh.Path).Version ; Auto-fix inaccurate versions in Hashes + if !files := versions.Get(fh.Version, 0) { + if IsSet(versionFilter) && !versionFilter(fh.Version) + continue + versions[fh.Version] := files := [] + v := RegExReplace(fh.Version, '^\d+\.\d+\b\K.*') + if VerCompare(prevMax := maxes.Get(v, ''), fh.Version) < 0 { + maxes[v] := fh.Version + if prevMax != '' + versions[prevMax].superseded := true + } + else + files.superseded := true + } + files.InsertAt(1, fh) + } + SetWorkingDir callerwd + return versions + } + + NotifyAssocChanged() { + DllCall("shell32\SHChangeNotify", "uint", 0x08000000 ; SHCNE_ASSOCCHANGED + , "uint", 0, "ptr", 0, "ptr", 0) + } + + GetConfirmation(message, icon:='!') { + if !this.Silent && MsgBox(message, this.DialogTitle, 'Icon' icon ' OkCancel') = 'Cancel' + ExitApp 1 + } + + WarnBox(message) { + if !this.Silent + MsgBox message, this.DialogTitle, "Icon!" + } + + InfoBox(message) { + if !this.Silent + MsgBox message, this.DialogTitle, "Iconi" + } + + FatalError(message) { + if !this.Silent + MsgBox message, this.DialogTitle, 'Iconx' + ExitApp 1 + } + + GetTargetUX() { + try { + ; For registered installations, InstallCommand allows for future changes. + return { + Version: RegRead(this.SoftwareKey, 'Version'), + InstallCommand: RegRead(this.SoftwareKey, 'InstallCommand') + } + } + try { + ; Target installation not in registry, or has no InstallCommand (e.g. too old). + ; Allow non-registry installations that follow protocol as commented below. + ux := {} + ; Version information must be provided by the file at this.HashesPath: + ux.Version := this.Hashes['UX\install.ahk'].Version + ; Interpreter must be located at the path calculated below: + interpreter := this.InstallDir '\UX\AutoHotkeyUX.exe' + if FileExist(interpreter) { + ; Additional interpreters must be installable with this command line: + ux.InstallCommand := Format('"{1}" "{2}\UX\install.ahk" /install "%1"' + , interpreter, this.InstallDir) + return ux + } + } + ; Otherwise, UX script or appropriate interpreter not found. + } + + ; Delete a symbolic link, or do nothing if path does not refer to a symbolic link. + DeleteLink(path) { + switch this.GetLinkAttrib(path) { + case 'D': DirDelete path + case 'F': FileDelete path + } + } + + GetLinkAttrib(path) { + attrib := DllCall('GetFileAttributes', 'str', path) + ; FILE_ATTRIBUTE_REPARSE_POINT = 0x400 + ; FILE_ATTRIBUTE_DIRECTORY = 0x10 + return (attrib != -1 && (attrib & 0x400)) ? ((attrib & 0x10) ? 'D' : 'F') : '' + } + + UpdateV2Link() { + ; Create a symlink for AutoHotkey.exe to simplify use by tools. + DllCall('CreateSymbolicLink', 'str', this.InstallDir '\v2\AutoHotkey.exe' + , 'str', 'AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe', 'uint', 0) + } + + CreateWindowSpyRedirect() { + ; Permit overwrite only when upgrading a legacy v1 installation, + ; or if it is known to have been created by us. + if !FileExist('WindowSpy.ahk') || this.HasOwnProp('SoftwareKeyV1') + || this.Hashes.Get('WindowSpy.ahk', {Hash: ''}).Hash = HashFile('WindowSpy.ahk') { + FileOpen('WindowSpy.ahk', 'w').Write(' + ( + #include UX + #include inc\bounce-v1.ahk + /**/ + #requires AutoHotkey v2.0 + try Run('"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"'), ExitApp() + #include WindowSpy.ahk + )') + this.AddFileHash('WindowSpy.ahk', this.Version) + } + } + + CreateStartShortcut() { + if this.Hashes.Has(this.StartFolder '\AutoHotkey.lnk') + try FileDelete this.StartFolder '\AutoHotkey.lnk' + CreateAppShortcut( + lnk := this.StartFolder '\AutoHotkey Dash.lnk', { + target: this.Interpreter, + args: Format('"{1}\UX\ui-dash.ahk"', this.InstallDir), + desc: "AutoHotkey Dash", + aumid: this.AppUserModelID, + uninst: this.UninstallCmd + } + ) + this.AddFileHash lnk, this.Version + + CreateAppShortcut( + lnk := this.StartFolder '\AutoHotkey Window Spy.lnk', { + target: this.Interpreter, + args: Format('"{1}\UX\WindowSpy.ahk"', this.InstallDir), + desc: "AutoHotkey Window Spy", + aumid: 'AutoHotkey.WindowSpy', + icon: Format('{1}\UX\inc\spy.ico', this.InstallDir), iconIndex: 0, + uninst: this.UninstallCmd + } + ) + this.AddFileHash lnk, this.Version + } + + CreateCompilerShortcut() { + CreateAppShortcut( + lnk := this.StartFolder '\Ahk2Exe.lnk', { + target: this.InstallDir '\Compiler\Ahk2Exe.exe', + desc: "Convert .ahk to .exe", + aumid: 'AutoHotkey.Ahk2Exe', + uninst: this.UninstallCmd + } + ) + this.AddFileHash lnk, this.Version + } + + MakeUIA(baseFile) { + SplitPath baseFile,, &baseDir,, &baseName + baseDir := baseDir = '.' ? '' : baseDir '\' + FileCopy baseFile, newPath := baseDir baseName '_UIA.exe', true + static abort := false ; Let "Abort" disable MakeUIA calls, but let other PostActions complete. + while !abort { + try { + EnableUIAccess newPath + break + } + catch as e { + try FileDelete newPath + if e.What != "EndUpdateResource" + throw + if this.Silent { + if A_Index > 4 + break + Sleep 500 + } + switch MsgBox("Unable to create " baseName ". Try adding an exclusion in your antivirus software. If that doesn't work, please report the issue.`n`nError: " e.Message + ,, "a/r/i") { + case "Abort": abort := true + case "Ignore": break + } + } + } + this.AddFileHash newPath, '' ; For uninstall + } + + IsTrustedLocation(path) { ; http://msdn.com/library/bb756929 + other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") + return InStr(path, A_ProgramFiles "\") = 1 + || other && InStr(path, other "\") = 1 + } + + ;} + + ;{ Upgrade from v1 + + PrepareUpgradeV1(installedVersion) { + ; This needs to be done before conflict-checking + if FileExist('license.txt') + this.AddFileHash('license.txt', installedVersion) + } + + UpgradeV1(installedVersion) { + try { ; Permit failure in case AutoHotkey.exe has been deleted. + exe := GetExeInfo('AutoHotkey.exe') + build := RegExReplace(exe.Description, '^AutoHotkey *') + } + + ; Set default launcher settings + if IsSet(build) && ConfigRead('Launcher\v1', 'Build', '!') = '!' + ConfigWrite(build, 'Launcher\v1', 'Build') + if ConfigRead('Launcher\v1', 'UTF8', '') = '' + && InStr(RegRead('HKCR\' this.ScriptProgId '\Shell\Open\Command',, ''), '/cp65001 ') + ConfigWrite(true, 'Launcher\v1', 'UTF8') + + ; Record these for Uninstall + add 'AutoHotkey{1}.exe', '', 'A32', 'U32', 'U64', 'A32_UIA', 'U32_UIA', 'U64_UIA' + add 'Compiler\{1}.bin', 'ANSI 32-bit', 'Unicode 32-bit', 'Unicode 64-bit', 'AutoHotkeySC' + add '{1}', 'Compiler\Ahk2Exe.exe', 'AutoHotkey.chm', A_WinDir '\ShellNew\Template.ahk' + + add(fmt, patterns*) { + for p in patterns + if FileExist(f := Format(fmt, p)) + this.AddFileHash f, installedVersion + } + + ; Remove obsolete files + for item in ['Installer.ahk', 'AutoHotkey Website.url'] + try FileDelete item + + ; Remove the v1 shortcuts from the Start menu + name := RegRead(this.SoftwareKeyV1, 'StartMenuFolder', '') + if name != '' + try DirDelete A_ProgramsCommon '\' name, true + + ; Remove the old sub-keys, which might be in the wrong reg view + try RegDeleteKey this.SoftwareKeyV1 + try RegDeleteKey this.UninstallKeyV1 + } + + DisplaceFile(sourcePath, destPath, version) { + SplitPath destPath,, &destDir + if destDir != "" + DirCreate destDir + FileMove sourcePath, destPath + try this.Hashes.Delete(sourcePath) + this.AddFileHash destPath, version + } + + DisplaceV1(v) { + DirCreate dir := 'v' v + displace(path) { + if FileExist(path) { + SplitPath path, &name + this.DisplaceFile path, dir '\' name, v + } + } + for build in ['U32', 'U64', 'A32'] { + displace 'AutoHotkey' build '.exe' + displace 'AutoHotkey' build '_UIA.exe' + } + try + defaultBinSize := FileGetSize(defaultBinPath := 'Compiler\AutoHotkeySC.bin') + for build in ['Unicode 32-bit', 'Unicode 64-bit', 'ANSI 32-bit'] { + try + if FileGetSize('Compiler\' build '.bin') = defaultBinSize + this.AddFileCopy this.InstallDir '\Compiler\' build '.bin', defaultBinPath + displace 'Compiler\' build '.bin' + } + + displace 'AutoHotkey.chm' + try { + exe := GetExeInfo('AutoHotkey.exe') + if exe.Version = v + && RegExMatch(exe.Description, ' (A|U)\w+ (32|64)-bit$', &m) { + ; Too early to add to FileItems, so use PostAction: + this.AddPostAction this.CopyDefaultExe.Bind(, 'AutoHotkey' m.1 m.2 '.exe') + } + } + } + + CopyDefaultExe(from) { + try + FileCopy from, 'AutoHotkey.exe', true + catch + return ; TODO: report to user? + this.AddFileHash 'AutoHotkey.exe', this.Version + } + + ;} +} diff --git a/UX/launcher.ahk b/UX/launcher.ahk index 59a34d2..d73adfa 100644 --- a/UX/launcher.ahk +++ b/UX/launcher.ahk @@ -1,363 +1,426 @@ -; This script is intended for indirect use via commands registered by install.ahk. -; It can also be compiled as a replacement for AutoHotkey.exe, so tools which run -; scripts by executing AutoHotkey.exe can benefit from automatic version selection. -#requires AutoHotkey v2.0 - -;@Ahk2Exe-SetDescription AutoHotkey Launcher -#SingleInstance Off -#NoTrayIcon - -#include inc\identify.ahk -#include inc\launcher-common.ahk -#include inc\ui-base.ahk - -if A_ScriptFullPath == A_LineFile { - SetWorkingDir A_InitialWorkingDir - Main -} - -Main() { - switches := [] - while A_Args.length { - arg := A_Args.RemoveAt(1) - if SubStr(arg,1,1) != '/' { - ScriptPath := arg - break - } - nextArgValue() { - if !A_Args.Length { - MsgBox "Invalid command line switches; missing value for " arg ".", "AutoHotkey Launcher", "icon!" - ExitApp 1 - } - return A_Args.RemoveAt(1) - } - switch arg, false { - case '/RunWith': ; Launcher-specific - A_Args.runwith := nextArgValue() - case '/Launch': ; Launcher-specific - A_Args.launch := true - case '/Which': - A_Args.which := true - if trace.Enabled - trace.DefineProp 'call', {call: (this, s) => OutputDebug(s)} ; Don't use stdout. - case '/iLib', '/include': - switches.push(arg) - switches.push(nextArgValue()) - default: - switches.push(arg) - } - - } - if !IsSet(ScriptPath) - && !FileExist(ScriptPath := A_ScriptDir "\AutoHotkey.ahk") - && !FileExist(ScriptPath := A_MyDocuments "\AutoHotkey.ahk") { - ; TODO: something more useful? - if FileExist(A_ScriptDir "\AutoHotkey.chm") - Run 'hh.exe "ms-its:' A_ScriptDir '\AutoHotkey.chm::/docs/Welcome.htm"',, 'Max' - else - Run 'https://lexikos.github.io/v2/docs/Welcome.htm' - ExitApp - } - if ScriptPath = '*' - ExitApp 2 ; FIXME: code would need to be read in and then passed to the real AutoHotkey - IdentifyAndLaunch ScriptPath, A_Args, switches -} - -GetLaunchParameters(ScriptPath, interactive:=false) { - code := FileRead(ScriptPath, 'UTF-8') - if RegExMatch(code, 'im)^[ `t]*#Requires[ `t]+AutoHotkey[ `t]+(.*)', &m) { - ; Replace "; prefer x." with "x" and remove other comments - prefer := RegExReplace(m.1, 'i);\s*prefer([ `t]+[^;`r`n\.]+)|;.*', '$1') - ; Extract version requirement - if RegExMatch(prefer, '(?=)?v(\d\S+)', &m) - v := m.1, prefer := SubStr(prefer, 1, m.Pos-1) . SubStr(prefer, m.Pos + m.Len) - ; Insert commas as needed - prefer := RegExReplace(prefer, '[^\s,]\K\s+(?!$)', ",") - } - if IsSet(v) - i := {v: v, r: "#Requires"} - else if ConfigRead('Launcher', 'Identify', true) - i := IdentifyBySyntax(code) - else - i := {v: 0, r: "syntax-checking is disabled"} - v := i.v || ConfigRead('Launcher', 'Fallback', "") - trace "![Launcher] version " (v || "unknown") " -- " i.r - if !v - exe := interactive ? PromptMajorVersion(ScriptPath) : "" - else - if !exe := GetRequiredOrPreferredExe(v, prefer ?? '') - if interactive - exe := TryToInstallVersion(v, i.v ? i.r : '', ScriptPath) - lp := {exe: exe, id: i, v: v, switches: []} - if exe { - if GetMajor(exe.Version) = 1 && ConfigRead('Launcher\v1', 'UTF8', false) - lp.switches.Push('/CP65001') - } - return lp -} - -IdentifyAndLaunch(ScriptPath, args, switches) { - lp := GetLaunchParameters(ScriptPath, !(whichMode := args.HasProp('which'))) - if whichMode { - try FileAppend(lp.v "`n" - (lp.exe ? lp.exe.Path : "") "`n" - (lp.switches.Length ? lp.switches[1] : "") "`n", '*', 'UTF-8-RAW') - ExitApp lp.id.v ? GetMajor(lp.id.v) : 0 - } - if !lp.exe - ExitApp 2 - switches.Push(lp.switches*) - ExitApp LaunchScript(lp.exe.Path, ScriptPath, args, switches) -} - -TryToInstallVersion(v, r, ScriptPath) { - SplitPath ScriptPath, &name - m := ' script you are trying to run requires AutoHotkey v' v ', which is not installed.`n`nScript:`t' name - m := !(r && r != '#Requires') ? 'The' m : 'It looks like the' m '`nRule:`t' r - if downloadable := IsNumber(v) || VerCompare(v, '1.1.24.02') >= 0 { - ; Get current version compatible with v. - bv := v = 1 ? '1.1' : IsInteger(v) ? v '.0' : RegExReplace(v, '^\d+(?:\.\d+)?\b\K.*') - req := ComObject('Msxml2.XMLHTTP') - req.open('GET', Format('https://www.autohotkey.com/download/{}/version.txt', bv), false) - req.send() - if req.status = 200 && RegExMatch(cv := req.responseText, '^\d+\.[\w\+\-\.]+$') && VerCompare(cv, v) >= 0 - m .= '`n`nWe can try to download and install AutoHotkey v' cv ' for you, while retaining the ability to use the versions already installed.`n`nDownload and install AutoHotkey v' cv '?' - else - downloadable := false - } - if !A_IsAdmin && RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "") = ROOT_DIR - SetTimer(() => ( - WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) && - SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C - ), -25) - if MsgBox(m, 'AutoHotkey', downloadable ? 'Iconi y/n' : 'Icon!') != 'yes' - return false - if RunWait(Format('"{}" /script "{}\install-version.ahk" "{}"', A_AhkPath, A_ScriptDir, cv)) != 0 - return false - return exe := GetRequiredOrPreferredExe(v) -} - -GetRequiredOrPreferredExe(v, prefer:='') { - section := 'Launcher\v' GetMajor(v) - userv := ConfigRead(section, 'Version', "") - prefer := (A_Args.HasProp('runwith') ? A_Args.runwith ',' : '') . prefer - prefer .= ',' (ConfigRead(section, 'Build', (A_Is64bitOS ? "64," : "") "!ANSI")) - prefer .= ',' (ConfigRead(section, 'UIA', false) ? 'UIA' : '!UIA') - if vexact := (userv != "" && (IsInteger(v) || VerCompare(v, userv) < 0)) - v := userv - return LocateExeByVersion(v, vexact, Trim(prefer, ',')) -} - -LocateExeByVersion(v, vexact:=false, prefer:='!UIA, 64, !ANSI') { - trace '![Launcher] Attempting to locate v' v '; prefer ' prefer - majorVer := GetMajor(v), best := '', bestscore := 0 - IsInteger(v) && v .= '-' ; Allow pre-release versions. - for ,f in GetUsableAutoHotkeyExes() { - try { - relation := VerCompare(f.Version, v) - if vexact ? relation != 0 : (relation < 0 || GetMajor(f.Version) > majorVer) { - ; trace '![Launcher] Skipping v' f.Version ': ' f.Path - continue - } - fscore := 0 - Loop Parse prefer, ",", " " { - if A_LoopField = "" - continue - fscore <<= 1 - if !(A_LoopField ~= '^[<>=]' ? VerCompare(f.Version, A_LoopField) - : matchPref(f.Description, A_LoopField)) - continue - fscore |= 1 - } - ; trace '![Launcher] ' fscore ' v' f.Version ' ' f.Path - ; Prefer later version if all else matches. If version also matches, prefer later - ; files enumeration order is generally AutoHotkey.exe, ..A32.exe, ..U32.exe, ..U64.exe. - if bestscore < fscore - || bestscore = fscore && (vexact || VerCompare(f.Version, best.Version) > 0) - bestscore := fscore, best := f - } - catch as e { - trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message - trace "-[Launcher] " e.file ":" e.line - } - } - return best - matchPref(desc, pref) => SubStr(pref,1,1) != "!" ? InStr(desc, pref) : !InStr(desc, SubStr(pref,2)) -} - -PromptMajorVersion(ScriptPath:="") { - majors := LocateMajorVersions() - switch majors.Count { - case 1: - for , f in majors - return f - case 0: - trace '-[Launcher] Failed to locate any interpreters; fallback to launcher' - return {Path: A_AhkPath, Version: A_AhkVersion} - } - files := [] - for , f in majors - files.Push(f) - prompt := VersionSelectGui(ScriptPath, files) - prompt.Show - WinWaitClose prompt - if !prompt.HasProp('selection') { - trace '[Launcher] No version selected from menu' - ExitApp - } - return prompt.selection -} - -LocateMajorVersions(filePattern:='', fileLoopOpt:='R') { - majors := Map() - Loop 2 - if f := GetRequiredOrPreferredExe(A_Index) - majors[A_Index] := f - return majors -} - -class Handle { - __new(ptr:=0) => this.ptr := ptr - __delete() => DllCall("CloseHandle", "ptr", this) -} - -LaunchScript(exe, ahk, args:="", switches:="", encoding:="UTF-8") { - ; Pass our own stdin/stdout handles (if any) to the child process. - hStdIn := DllCall("GetStdHandle", "uint", -10, "ptr") - hStdOut := DllCall("GetStdHandle", "uint", -11, "ptr") - hStdErr := DllCall("GetStdHandle", "uint", -12, "ptr") - - ; Build command line to execute. - makeArgs(args) { - r := '' - for arg in args is object ? args : [args] - r .= ' ' (arg ~= '\s' ? '"' arg '"' : arg) - return r - } - switches := makeArgs(switches) - cmd := Format('"{1}"{2} "{3}"{4}', exe, switches, ahk, makeArgs(args)) - trace '>[Launcher] ' cmd - - ; For RunWait, stdout redirection, /validate, etc. to have the best chance of working, - ; let the launcher exit early only if it can detect that it was executed from Explorer - ; or the parent process appears to have exited already (or if the caller passed /launch). - waitClose := !args.HasProp('launch') - hParent := 0 - if IsSet(ProcessGetParent) { - try { - ; (PROCESS_QUERY_LIMITED_INFORMATION := 0x1000) | (SYNCHRONIZE := 0x100000) - if hParent := DllCall("OpenProcess", "uint", 0x101000, "int", false - , "uint", parentPid := ProcessGetParent(), "ptr") - hParent := Handle(hParent) - if !hParent || (parentName := ProcessGetName(parentPid)) = "explorer.exe" - waitClose := false - } - catch as e - trace '![Launcher] Failure checking parent process: ' e.Message - } - - try { - proc := RunWithHandles(cmd, {in: hStdIn, out: hStdOut, err: hStdErr}) - } - catch OSError as e { - if e.Number != 740 ; ERROR_ELEVATION_REQUIRED - throw - trace '![Launcher] elevation required; handles will not be redirected' - cmd := RegExReplace(cmd, ' /ErrorStdOut(?:=\S*)?') - Run cmd - ExitApp - } - - ; When the /launch switch is used, return the process ID as the launcher's exit code. - if args.HasProp('launch') - return proc.pid - - if waitClose { - ; Wait for either the child process or our parent process (if determined) to terminate. - NumPut 'ptr', proc.hProcess.ptr, 'ptr', hParent ? hParent.ptr : 0, waitHandles := Buffer(A_PtrSize*2) - loop { - Sleep -1 - waitResult := DllCall("MsgWaitForMultipleObjects", "uint", 1 + (hParent != 0), "ptr", waitHandles, "int", 0, "uint", -1, "uint", 0x04FF) - } until waitResult = 0 || waitResult = 1 - } - - DllCall("GetExitCodeProcess", "ptr", proc.hProcess, "uint*", &exitCode:=0) - if trace.Enabled { - ; We have to return something numeric for ExitApp, so currently exitCode is left as 259 - ; if the process is still running. - if exitCode = 259 && DllCall("WaitForSingleObject", "ptr", proc.hProcess, "uint", 0) = 258 { ; STILL_ACTIVE = 259, WAIT_TIMEOUT = 258 - if !(hParent ?? 1) || (waitResult ?? -1) = 1 - trace '>[Launcher] Process launched; now exiting because parent process has terminated.' - else if (parentName ?? "") = "explorer.exe" - trace '>[Launcher] Process launched; now exiting because parent is explorer.exe.' - else - trace '>[Launcher] Process launched; launcher exiting early.' - } - else - trace '>[Launcher] Exit code: ' exitCode - } - - return exitCode -} - -RunWithHandles(cmd, handles, workingDir:="") { - static STARTUPINFO_SIZE := A_PtrSize=8 ? 104 : 68 - , STARTUPINFO_dwFlags := A_PtrSize=8 ? 60 : 44 - , STARTUPINFO_hStdInput := A_PtrSize=8 ? 80 : 56 - , STARTF_USESTDHANDLES := 0x100 - , PROCESS_INFORMATION_SIZE := A_PtrSize=8 ? 24 : 16 - HandleValue(p) => HasProp(handles, p) && (IsInteger(h := handles.%p%) ? h : h.Ptr) - si := Buffer(STARTUPINFO_SIZE, 0) - NumPut("uint", STARTUPINFO_SIZE, si) - NumPut("uint", STARTF_USESTDHANDLES, si, STARTUPINFO_dwFlags) - NumPut("ptr", HandleValue("in") - , "ptr", HandleValue("out") - , "ptr", HandleValue("err") - , si, STARTUPINFO_hStdInput) - pi := Buffer(PROCESS_INFORMATION_SIZE) - if !DllCall("CreateProcess", "ptr", 0, "str", cmd, "ptr", 0, "int", 0, "int", true - , "int", 0x08000000, "int", 0, "ptr", workingDir ? StrPtr(workingDir) : 0 - , "ptr", si, "ptr", pi) - throw OSError(, -1, cmd) - return { hProcess: Handle(NumGet(pi, 0, "ptr")) - , hThread: Handle(NumGet(pi, A_PtrSize, "ptr")) - , pid: NumGet(pi, A_PtrSize*2, "uint") } -} - -class VersionSelectGui extends AutoHotkeyUxGui { - __new(script, files) { - SplitPath script, &scriptName - super.__new("Run " scriptName " with", '-MinimizeBox') - DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT - , 'int64*', 2 | (2<<32), 'int', 8) ; WTNCA_NODRAWICON=2 - lv := this.AddListMenu('vList LV0x40 w200', ["Version"]) - lv.OnEvent('Focus', 'Focused') - lv.OnEvent('LoseFocus', 'Focused') - lv.OnEvent('Click', 'Confirm') - il := IL_Create(,, false) - lv.SetImageList(il, 0) - for f in this.files := files { - lv.Add('Icon' IL_Add(il, f.Path), f.Version " " StrReplace(f.Description, "AutoHotkey ")) - } - lv.AutoSize(8) - lv.GetPos(&x, &y, &w, &h) - this.Show('AutoSize Hide') - this.AddButton('Default Hidden', "Confirm").OnEvent('Click', 'Confirm') - } - - Confirm(*) { - this.selection := this.files[this['List'].GetNext()] - this.Hide() - } - - Focused(ctrl, *) { - OnMessage(0x101, keyup, ctrl.Focused) - static keyup(wParam, lParam, nmsg, hwnd) { - local this := GuiFromHwnd(hwnd, true) - if IsDigit(GetKeyName(Format("vk{:x}", wParam))) && this['List'].GetNext() { - this.Confirm() - return true - } - } - } -} +; This script is intended for indirect use via commands registered by install.ahk. +; It can also be compiled as a replacement for AutoHotkey.exe, so tools which run +; scripts by executing AutoHotkey.exe can benefit from automatic version selection. +#requires AutoHotkey v2.0 + +;@Ahk2Exe-SetDescription AutoHotkey Launcher +#SingleInstance Off +#NoTrayIcon + +#include inc\identify.ahk +#include inc\launcher-common.ahk +#include inc\ui-base.ahk + +if A_ScriptFullPath == A_LineFile || A_LineFile == '*#1' { + SetWorkingDir A_InitialWorkingDir + Main +} + +Main() { + switches := [] + while A_Args.length { + arg := A_Args.RemoveAt(1) + if SubStr(arg,1,1) != '/' { + ScriptPath := arg + break + } + nextArgValue() { + if !A_Args.Length { + MsgBox "Invalid command line switches; missing value for " arg ".", "AutoHotkey Launcher", "icon!" + ExitApp 1 + } + return A_Args.RemoveAt(1) + } + switch arg, false { + case '/RunWith': ; Launcher-specific + A_Args.runwith := nextArgValue() + case '/Launch': ; Launcher-specific + A_Args.launch := true + case '/Which': + A_Args.which := true + if trace.Enabled + trace.DefineProp 'call', {call: (this, s) => OutputDebug(s "`n")} ; Don't use stdout. + case '/iLib', '/include': + switches.push(arg) + switches.push(nextArgValue()) + default: + switches.push(arg) + } + + } + if !IsSet(ScriptPath) + && !FileExist(ScriptPath := A_ScriptDir "\AutoHotkey.ahk") + && !FileExist(ScriptPath := A_MyDocuments "\AutoHotkey.ahk") + && !FileExist(ScriptPath := A_ScriptDir "\ui-dash.ahk") { + ExitApp + } + if ScriptPath = '*' + ExitApp 2 ; FIXME: code would need to be read in and then passed to the real AutoHotkey + IdentifyAndLaunch ScriptPath, A_Args, switches +} + +GetLaunchParameters(ScriptPath, interactive:=false) { + code := FileRead(ScriptPath, 'UTF-8') + require := prefer := rule := exe := "" + if RegExMatch(code, 'im)^[ `t]*#Requires[ `t]+AutoHotkey[ `t]+([^;`r`n]*)(?:[ `t]*;[ `t]*prefer[ `t]+([^;`r`n\.]+))?', &m) { + trace "![Launcher] " m.0 + require := RegExReplace(m.1, '[^\s,]\K\s+(?!$)', ",") + prefer := RegExReplace(m.2, '[^\s,]\K\s+(?!$)', ",") + rule := "#Requires" + } + else if ConfigRead('Launcher', 'Identify', true) { + i := IdentifyBySyntax(code) + trace "![Launcher] syntax says version " (i.v || "unknown") " -- " i.r + if i.v + require := String(i.v) + rule := i.r + } + else { + trace "![Launcher] version unknown - syntax-checking is disabled" + } + if !(hasv := RegExMatch(require, '(^|,)\s*(?!(32|64)-bit)[<>=]*v?\d')) { + ; No version specified or detected + if hasv := v := ConfigRead('Launcher', 'Fallback', "") { + require .= (require=''?'':',') v + trace "![Launcher] using fallback version " v + } + } + v := GetVersionToInstall(require) ; Currently used for multiple purposes + if !hasv + exe := interactive ? PromptMajorVersion(ScriptPath, require, prefer) : "" + else + if !exe := GetRequiredOrPreferredExe(require, prefer) + if interactive { + if v && !LocateExeByVersion(v, '') + exe := TryToInstallVersion(v, rule, ScriptPath, require, prefer) + else + RequirementNotMetMsgBox require, ScriptPath + } + lp := {exe: exe, id: exe ? GetMajor(exe.Version) : GetLikelyMajor(require), v: v, switches: []} + if exe { + if GetMajor(exe.Version) = 1 && ConfigRead('Launcher\v1', 'UTF8', false) + lp.switches.Push('/CP65001') + } + return lp +} + +ParseRequiresVersion(s) { + return RegExMatch(s, 'i)^(?!(?:32|64)-bit$)(?[<>=]*)v?(?(?\d+)\b\S*)', &m) ? m : 0 +} + +GetLikelyMajor(r) { + if IsNumber(r) + return Integer(r) + ; Usually there would be either a version number with no operator + ; or a range where the lower and upper bound have the same major. + Loop Parse r, ",", " `t" + if (m := ParseRequiresVersion(A_LoopField)) && m.op != '<' + return m.major + return '' +} + +GetVersionToInstall(r) { + ; TryToInstallVersion currently only supports the latest bug-fix release, + ; so don't try to install if there's a complex version requirement. + if IsNumber(r) + return r + v := "" + Loop Parse r, ",", " `t" { + if (m := ParseRequiresVersion(A_LoopField)) { + if m.op + return '' + v := m.version + } + } + return v +} + +IdentifyAndLaunch(ScriptPath, args, switches) { + lp := GetLaunchParameters(ScriptPath, !(whichMode := args.HasProp('which'))) + if whichMode { + try FileAppend(lp.v "`n" + (lp.exe ? lp.exe.Path : "") "`n" + (lp.switches.Length ? lp.switches[1] : "") "`n", '*', 'UTF-8-RAW') + ExitApp lp.id + } + if !lp.exe + ExitApp 2 + switches.Push(lp.switches*) + ExitApp LaunchScript(lp.exe.Path, ScriptPath, args, switches) +} + +TryToInstallVersion(v, r, ScriptPath, require, prefer) { + ; This is currently designed only for downloading the latest bug-fix of a given minor version. + SplitPath ScriptPath, &name + m := ' script you are trying to run requires AutoHotkey v' v ', which is not installed.`n`nScript:`t' name + m := !(r && r != '#Requires') ? 'The' m : 'It looks like the' m '`nRule:`t' r + if downloadable := IsNumber(v) || VerCompare(v, '1.1.24.02') >= 0 { + ; Get current version compatible with v. + bv := v = 1 ? '1.1' : IsInteger(v) ? v '.0' : RegExReplace(v, '^\d+(?:\.\d+)?\b\K.*') + req := ComObject('Msxml2.XMLHTTP') + req.open('GET', Format('https://www.autohotkey.com/download/{}/version.txt', bv), false) + try req.send() + if req.status = 200 && RegExMatch(cv := req.responseText, '^\d+\.[\w\+\-\.]+$') && VerCompare(cv, v) >= 0 + m .= '`n`nWe can try to download and install AutoHotkey v' cv ' for you, while retaining the ability to use the versions already installed.`n`nDownload and install AutoHotkey v' cv '?' + else + downloadable := false + } + if downloadable && !A_IsAdmin && RegRead('HKLM\SOFTWARE\AutoHotkey', 'InstallDir', "") = ROOT_DIR + SetTimer(() => ( + WinExist('ahk_class #32770 ahk_pid ' ProcessExist()) && + SendMessage(0x160C,, true, 'Button1') ; BCM_SETSHIELD := 0x160C + ), -25) + if MsgBox(m, 'AutoHotkey', downloadable ? 'Iconi y/n' : 'Icon!') != 'yes' + return false + if RunWait(Format('"{}" /script "{}\install-version.ahk" "{}"', A_AhkPath, A_ScriptDir, cv)) != 0 + return false + return exe := GetRequiredOrPreferredExe(require, prefer) +} + +RequirementNotMetMsgBox(require, ScriptPath) { + SplitPath ScriptPath, &name + MsgBox 'Unable to locate the appropriate interpreter to run this script.`n`nScript:`t' name '`nRequires: ' StrReplace(require, ',', ' '), 'AutoHotkey', 'Icon!' +} + +GetRequiredOrPreferredExe(require, prefer:='') { + if A_Args.HasProp('runwith') + prefer := A_Args.runwith ',' prefer + return LocateExeByVersion(require, Trim(prefer, ',')) +} + +LocateExeByVersion(require, prefer:='!UIA, 64, !ANSI') { + trace '![Launcher] locating exe: require ' require (prefer='' ? '' : '; prefer ' prefer) + best := '', bestscore := 0, cPrefMap := Map() + for ,f in GetUsableAutoHotkeyExes() { + try { + ; Check requirements first + fMajor := GetMajor(f.Version) + Loop Parse require, ",", " " { + if A_LoopField = "" + continue + if m := ParseRequiresVersion(A_LoopField) { + if !VerCompare(f.Version, (m.op ? '' : '>=') A_LoopField) { + ; trace '![Launcher] ' f.Version ' ' (m.op ? '' : '>=') A_LoopField ' = false' + continue 2 + } + if !m.op && fMajor > m.major { ; No operator implies it must be same major version + ; trace '![Launcher] major ' f.Version ' > ' m.version + continue 2 + } + } + else if !matchPref(f.Description, A_LoopField) { + ; trace '![Launcher] no match for "' A_LoopField '" in ' f.Description + continue 2 + } + } + ; Determine additional user preferences based on major version + if !(cPref := cPrefMap.Get(fMajor, 0)) { + section := 'Launcher\v' fMajor + cPref := ConfigRead(section, 'Version', "") + cPref := { + V: cPref ? '=' cPref ',' : '<0,', + D: ',' (ConfigRead(section, 'Build', (A_Is64bitOS ? "64," : "") "!ANSI")) + . ',' (ConfigRead(section, 'UIA', false) ? 'UIA' : '!UIA') + } + cPrefMap.Set(fMajor, cPref) + } + ; Calculate preference score + fscore := 0 + Loop Parse cPref.V prefer cPref.D, ",", " " { + if A_LoopField = "" + continue + fscore <<= 1 + if !(A_LoopField ~= '^[<>=]' ? VerCompare(f.Version, A_LoopField) + : matchPref(f.Description, A_LoopField)) + continue + fscore |= 1 + } + ; trace '![Launcher] ' fscore ' v' f.Version ' ' f.Path + ; Prefer later version if all else matches. If version also matches, prefer later files, + ; as enumeration order is generally AutoHotkey.exe, ..A32.exe, ..U32.exe, ..U64.exe. + if bestscore < fscore + || bestscore = fscore && VerCompare(f.Version, best.Version) >= 0 + bestscore := fscore, best := f + } + catch as e { + trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message + trace "-[Launcher] " e.file ":" e.line + } + } + return best + matchPref(desc, pref) => SubStr(pref,1,1) != "!" ? InStr(desc, pref) : !InStr(desc, SubStr(pref,2)) +} + +PromptMajorVersion(ScriptPath, require:='', prefer:='') { + majors := Map() + Loop 2 + if f := GetRequiredOrPreferredExe(A_Index ',' require, prefer) + majors[A_Index] := f + switch majors.Count { + case 1: + for , f in majors + return f + case 0: + trace '-[Launcher] Failed to locate any interpreters; fallback to launcher' + return {Path: A_AhkPath, Version: A_AhkVersion} + } + files := [] + for , f in majors + files.Push(f) + prompt := VersionSelectGui(ScriptPath, files) + prompt.Show + WinWaitClose prompt + if !prompt.HasProp('selection') { + trace '[Launcher] No version selected from menu' + ExitApp + } + return prompt.selection +} + +class Handle { + __new(ptr:=0) => this.ptr := ptr + __delete() => DllCall("CloseHandle", "ptr", this) +} + +LaunchScript(exe, ahk, args:="", switches:="", encoding:="UTF-8") { + ; Pass our own stdin/stdout handles (if any) to the child process. + hStdIn := DllCall("GetStdHandle", "uint", -10, "ptr") + hStdOut := DllCall("GetStdHandle", "uint", -11, "ptr") + hStdErr := DllCall("GetStdHandle", "uint", -12, "ptr") + + ; Build command line to execute. + makeArgs(args) { + r := '' + for arg in args is object ? args : [args] + r .= ' ' (arg ~= '\s' ? '"' arg '"' : arg) + return r + } + switches := makeArgs(switches) + cmd := Format('"{1}"{2} "{3}"{4}', exe, switches, ahk, makeArgs(args)) + trace '>[Launcher] ' cmd + + ; For RunWait, stdout redirection, /validate, etc. to have the best chance of working, + ; let the launcher exit early only if it can detect that it was executed from Explorer + ; or the parent process appears to have exited already (or if the caller passed /launch). + waitClose := !args.HasProp('launch') + hParent := 0 + if IsSet(ProcessGetParent) { + try { + ; (PROCESS_QUERY_LIMITED_INFORMATION := 0x1000) | (SYNCHRONIZE := 0x100000) + if hParent := DllCall("OpenProcess", "uint", 0x101000, "int", false + , "uint", parentPid := ProcessGetParent(), "ptr") + hParent := Handle(hParent) + if !hParent || (parentName := ProcessGetName(parentPid)) = "explorer.exe" + waitClose := false + } + catch as e + trace '![Launcher] Failure checking parent process: ' e.Message + } + + try { + proc := RunWithHandles(cmd, {in: hStdIn, out: hStdOut, err: hStdErr}) + } + catch OSError as e { + if e.Number != 740 ; ERROR_ELEVATION_REQUIRED + throw + trace '![Launcher] elevation required; handles will not be redirected' + cmd := RegExReplace(cmd, ' /ErrorStdOut(?:=\S*)?') + Run cmd + ExitApp + } + + ; When the /launch switch is used, return the process ID as the launcher's exit code. + if args.HasProp('launch') + return proc.pid + + if waitClose { + ; Wait for either the child process or our parent process (if determined) to terminate. + NumPut 'ptr', proc.hProcess.ptr, 'ptr', hParent ? hParent.ptr : 0, waitHandles := Buffer(A_PtrSize*2) + loop { + Sleep -1 + waitResult := DllCall("MsgWaitForMultipleObjects", "uint", 1 + (hParent != 0), "ptr", waitHandles, "int", 0, "uint", -1, "uint", 0x04FF) + } until waitResult = 0 || waitResult = 1 + } + + DllCall("GetExitCodeProcess", "ptr", proc.hProcess, "uint*", &exitCode:=0) + if trace.Enabled { + ; We have to return something numeric for ExitApp, so currently exitCode is left as 259 + ; if the process is still running. + if exitCode = 259 && DllCall("WaitForSingleObject", "ptr", proc.hProcess, "uint", 0) = 258 { ; STILL_ACTIVE = 259, WAIT_TIMEOUT = 258 + if !(hParent ?? 1) || (waitResult ?? -1) = 1 + trace '>[Launcher] Process launched; now exiting because parent process has terminated.' + else if (parentName ?? "") = "explorer.exe" + trace '>[Launcher] Process launched; now exiting because parent is explorer.exe.' + else + trace '>[Launcher] Process launched; launcher exiting early.' + } + else + trace '>[Launcher] Exit code: ' exitCode + } + + return exitCode +} + +RunWithHandles(cmd, handles, workingDir:="") { + static STARTUPINFO_SIZE := A_PtrSize=8 ? 104 : 68 + , STARTUPINFO_dwFlags := A_PtrSize=8 ? 60 : 44 + , STARTUPINFO_hStdInput := A_PtrSize=8 ? 80 : 56 + , STARTF_USESTDHANDLES := 0x100 + , PROCESS_INFORMATION_SIZE := A_PtrSize=8 ? 24 : 16 + HandleValue(p) => HasProp(handles, p) && (IsInteger(h := handles.%p%) ? h : h.Ptr) + si := Buffer(STARTUPINFO_SIZE, 0) + NumPut("uint", STARTUPINFO_SIZE, si) + NumPut("uint", STARTF_USESTDHANDLES, si, STARTUPINFO_dwFlags) + NumPut("ptr", HandleValue("in") + , "ptr", HandleValue("out") + , "ptr", HandleValue("err") + , si, STARTUPINFO_hStdInput) + pi := Buffer(PROCESS_INFORMATION_SIZE) + if !DllCall("CreateProcess", "ptr", 0, "str", cmd, "ptr", 0, "int", 0, "int", true + , "int", 0x08000000, "int", 0, "ptr", workingDir ? StrPtr(workingDir) : 0 + , "ptr", si, "ptr", pi) + throw OSError(, -1, cmd) + return { hProcess: Handle(NumGet(pi, 0, "ptr")) + , hThread: Handle(NumGet(pi, A_PtrSize, "ptr")) + , pid: NumGet(pi, A_PtrSize*2, "uint") } +} + +class VersionSelectGui extends AutoHotkeyUxGui { + __new(script, files) { + SplitPath script, &scriptName + super.__new("Run " scriptName " with", '-MinimizeBox') + DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT + , 'int64*', 2 | (2<<32), 'int', 8) ; WTNCA_NODRAWICON=2 + lv := this.AddListMenu('vList LV0x40 w200', ["Version"]) + lv.OnEvent('Focus', 'Focused') + lv.OnEvent('LoseFocus', 'Focused') + lv.OnEvent('Click', 'Confirm') + il := IL_Create(,, false) + lv.SetImageList(il, 0) + for f in this.files := files { + lv.Add('Icon' IL_Add(il, f.Path), f.Version " " StrReplace(f.Description, "AutoHotkey ")) + } + lv.AutoSize(8) + lv.GetPos(&x, &y, &w, &h) + this.Show('AutoSize Hide') + this.AddButton('Default Hidden', "Confirm").OnEvent('Click', 'Confirm') + } + + Confirm(*) { + if !(i := this['List'].GetNext()) + return + this.selection := this.files[i] + this.Hide() + } + + Focused(ctrl, *) { + OnMessage(0x101, keyup, ctrl.Focused) + static keyup(wParam, lParam, nmsg, hwnd) { + local this := GuiFromHwnd(hwnd, true) + if IsDigit(GetKeyName(Format("vk{:x}", wParam))) && this['List'].GetNext() { + this.Confirm() + return true + } + } + } +} diff --git a/UX/reload-v1.ahk b/UX/reload-v1.ahk index cadf63e..08ba895 100644 --- a/UX/reload-v1.ahk +++ b/UX/reload-v1.ahk @@ -1,22 +1,22 @@ -; This file is part of a trick for allowing a v2 script to relaunch itself with -; v2 when the user attempts to execute it with v1. See inc\bounce-v1.ahk. - -#NoTrayIcon - -if (A_ScriptFullPath = A_LineFile) -{ - MsgBox 16,, This script is not meant to be executed. - ExitApp 2 -} - -if (!A_Args.Length()) -{ - Loop Files, %A_ScriptDir%\..\AutoHotkey32.exe, FR - { - Run "%A_LoopFileLongPath%" /force "%A_ScriptFullPath%" - ExitApp - } -} - -MsgBox 16,, This script requires AutoHotkey v2, but was launched with v1. +; This file is part of a trick for allowing a v2 script to relaunch itself with +; v2 when the user attempts to execute it with v1. See inc\bounce-v1.ahk. + +#NoTrayIcon + +if (A_ScriptFullPath = A_LineFile) +{ + MsgBox 16,, This script is not meant to be executed. + ExitApp 2 +} + +if (!A_Args.Length()) +{ + Loop Files, %A_ScriptDir%\..\AutoHotkey32.exe, FR + { + Run "%A_LoopFileLongPath%" /force "%A_ScriptFullPath%" + ExitApp + } +} + +MsgBox 16,, This script requires AutoHotkey v2, but was launched with v1. ExitApp 2 \ No newline at end of file diff --git a/UX/reset-assoc.ahk b/UX/reset-assoc.ahk index e6536ff..c0c2507 100644 --- a/UX/reset-assoc.ahk +++ b/UX/reset-assoc.ahk @@ -1,34 +1,38 @@ -; This script clears any file type assocation made via the "open with" dialog, -; so that the standard registration under HKCR\.ahk can take effect. -#include inc\bounce-v1.ahk -/* v1 stops here */ -#requires AutoHotkey v2.0 - -keyname := "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ahk\UserChoice" -initial_progid := RegRead(keyname, "ProgId", "") -if A_Args.Length && A_Args[1] = '/check' { - if initial_progid = "" || initial_progid = "AutoHotkeyScript" - || MsgBox("It looks like you've used an unsupported method to set the default program for .ahk files. " - . "This will prevent the standard context menu and launcher (version auto-detect) functionality " - . "from working. Would you like this setting to be reset for you?", "AutoHotkey", "Icon! y/n") != "yes" - ExitApp -} -reg_file_path := A_Temp "\reset-ahk-file-association.reg" -FileOpen(reg_file_path, "w").Write("Windows Registry Editor Version 5.00`n" - . "[-" keyname "]`n") -EnvSet "__COMPAT_LAYER", "RunAsInvoker" -RunWait 'regedit.exe /S "' reg_file_path '"' -EnvSet "__COMPAT_LAYER", "" -DllCall("shell32\SHChangeNotify", "uint", 0x08000000, "uint", 0, "int", 0, "int", 0) ; SHCNE_ASSOCCHANGED -FileDelete reg_file_path -new_progid := RegRead(keyname, "ProgId", "") -if (new_progid != "" || A_LastError != 2) - MsgBox "Something went wrong and the reset probably " - . "didn't work.`n`nCurrent association: " - . (new_progid = "" ? "(unknown)" : new_progid), "AutoHotkey", "Icon!" -else if (initial_progid != "") - MsgBox "Association of .ahk files for the current user has been reset.", "AutoHotkey", "Iconi" -else - MsgBox "It looks as though the current user's settings " - . "weren't overriding the default .ahk file options. A reset was " +; This script clears any file type assocation made via the "open with" dialog, +; so that the standard registration under HKCR\.ahk can take effect. +#include inc\bounce-v1.ahk +/* v1 stops here */ +#requires AutoHotkey v2.0 + +keyname := "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ahk\UserChoice" +initial_progid := RegRead(keyname, "ProgId", "") +legacy_key := "HKCU\Software\Classes\.ahk" +legacy_assoc := RegRead(legacy_key,, "AutoHotkeyScript") +if A_Args.Length && A_Args[1] = '/check' { + if (initial_progid = "" || initial_progid = "AutoHotkeyScript") && legacy_assoc = "AutoHotkeyScript" + || MsgBox("It looks like you've used an unsupported method to set the default program for .ahk files. " + . "This will prevent the standard context menu and launcher (version auto-detect) functionality " + . "from working. Would you like this setting to be reset for you?", "AutoHotkey", "Icon! y/n") != "yes" + ExitApp +} +reg_file_path := A_Temp "\reset-ahk-file-association.reg" +FileOpen(reg_file_path, "w").Write("Windows Registry Editor Version 5.00`n" + . "[-" keyname "]`n") +EnvSet "__COMPAT_LAYER", "RunAsInvoker" +RunWait 'regedit.exe /S "' reg_file_path '"' +EnvSet "__COMPAT_LAYER", "" +if legacy_assoc != "AutoHotkeyScript" + RegWrite "AutoHotkeyScript", "REG_SZ", legacy_key +DllCall("shell32\SHChangeNotify", "uint", 0x08000000, "uint", 0, "int", 0, "int", 0) ; SHCNE_ASSOCCHANGED +FileDelete reg_file_path +new_progid := RegRead(keyname, "ProgId", "") +if (new_progid != "" || A_LastError != 2) + MsgBox "Something went wrong and the reset probably " + . "didn't work.`n`nCurrent association: " + . (new_progid = "" ? "(unknown)" : new_progid), "AutoHotkey", "Icon!" +else if (initial_progid != "" || legacy_assoc != "AutoHotkeyScript") + MsgBox "Association of .ahk files for the current user has been reset.", "AutoHotkey", "Iconi" +else + MsgBox "It looks as though the current user's settings " + . "weren't overriding the default .ahk file options. A reset was " . "attempted anyway, but it probably had no effect.", "AutoHotkey", "Icon!" \ No newline at end of file diff --git a/UX/ui-dash.ahk b/UX/ui-dash.ahk index b7a2109..65bbf42 100644 --- a/UX/ui-dash.ahk +++ b/UX/ui-dash.ahk @@ -1,200 +1,200 @@ -; Dash: AutoHotkey's "main menu". -; Run the script to show the GUI. -#include inc\bounce-v1.ahk -/* v1 stops here */ -#requires AutoHotkey v2.0 - -#NoTrayIcon -#SingleInstance Force - -#include inc\ui-base.ahk -#include ui-launcherconfig.ahk -#include ui-editor.ahk -#include ui-newscript.ahk - -DashRegKey := 'HKCU\Software\AutoHotkey\Dash' - -class AutoHotkeyDashGui extends AutoHotkeyUxGui { - __new() { - super.__new("AutoHotkey Dash") - - lv := this.AddListMenu('vLV LV0x40 w250', ["Name", "Desc"]) - lv.OnEvent("Click", "ItemClicked") - lv.OnEvent("ItemFocus", "ItemFocused") - lv.OnNotify(-155, "KeyPressed") - - this.AddButton("xp yp wp yp Hidden Default").OnEvent("Click", "EnterPressed") - - il := IL_Create(,, true) - lv.SetImageList(il, 0) - il2 := IL_Create(,, false) - lv.SetImageList(il2, 1) - addIcon(p*) =>(IL_Add(il, p*), IL_Add(il2, p*)) - - lv.Add("Icon" addIcon(A_AhkPath, 2) - , "New script", "Create a script or manage templates") - lv.Add("Icon" addIcon("imageres.dll", -111) - , "Compile", "Open Ahk2Exe - convert .ahk to .exe") - lv.Add("Icon" addIcon("imageres.dll", -99) - , "Help files (F1)") - lv.Add("Icon" addIcon(A_ScriptDir '\inc\spy.ico', 1) - , "Window spy") - lv.Add("Icon" addIcon("imageres.dll", -116) - , "Launch settings", "Configure how .ahk files are opened") - lv.Add("Icon" addIcon("notepad.exe", 1) - , "Editor settings", "Set your default script editor") - ; lv.Add("Icon" addIcon("mmc.exe") - ; , "Maintenance", "Repair settings or add/remove versions") - ; lv.Add(, "Auto-start", "Run scripts automatically at logon") - ; lv.Add(, "Downloads", "Get related tools") - - lv.AutoSize() - lv.GetPos(,,, &h) - - if !RegRead(DashRegKey, 'SuppressIntro', false) { - this.SetFont('s12') - this.AddText('yp x+m', "Welcome!") - this.SetFont('s9') - this.AddText('xp', "This is the Dash. It provides access to tools, settings and help files.") - this.AddText('xp', "To learn how to use AutoHotkey, refer to:") - this.AddLink('xp', " - ( - `s • Using the Program - • How to Write Hotkeys - • How to Send Keystrokes - • How to Run Programs - • How to Manage Windows - • Quick Reference - )").OnEvent('Click', 'LinkClicked') - - checkBox := this.AddCheckbox('Checked', "Show this info next time") - checkBox.GetPos(,,, &hc) - checkBox.Move(, h - hc) - checkBox.OnEvent('Click', 'SetIntroPref') - } - - this.Show("Hide h" (h + this.MarginY*2)) - } - - LinkClicked(ctrl, id, href) { - if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm') - Run 'hh.exe "ms-its:' chm '::docs/' href '"' - else - Run 'https://www.autohotkey.com/docs/v2/' href - } - - SetIntroPref(checkBox, *) { - if checkBox.Value { ; Show intro - try RegDelete(DashRegKey, 'SuppressIntro') - } else - RegWrite(true, 'REG_DWORD', DashRegKey, 'SuppressIntro') - } - - KeyPressed(lv, lParam) { - switch NumGet(lParam, A_PtrSize * 3, "Short") { - case 0x70: ; F1 - ShowHelpFile() - } - } - - EnterPressed(*) { - lv := this["LV"] - this.ItemClicked(lv, lv.GetNext(,'F')) - } - - ItemClicked(lv, item) { - switch item && RegExReplace(lv.GetText(item), ' .*') { - case "New": - NewScriptGui.Show() - case "Compile": - if WinExist("Ahk2Exe ahk_class AutoHotkeyGUI") - WinActivate - else if FileExist(ROOT_DIR '\Compiler\Ahk2Exe.exe') - Run '"' ROOT_DIR '\Compiler\Ahk2Exe.exe"' - else - Run Format('"{1}" /script "{2}\install-ahk2exe.ahk"', A_AhkPath, A_ScriptDir) - case "Help": - ShowHelpFile() - case "Window": - try { - Run '"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"' - return - } - static AHK_FILE_WINDOWSPY := 0xFF7A ; 65402 - static WM_COMMAND := 0x111 ; 273 - SendMessage WM_COMMAND, AHK_FILE_WINDOWSPY, 0, A_ScriptHwnd - if WinWait("Window Spy ahk_class AutoHotkeyGUI",, 1) - WinActivate - case "Launch": - LauncherConfigGui.Show() - case "Editor": - DefaultEditorGui.Show() - } - } - - ItemFocused(lv, item) { - static WM_CHANGEUISTATE := 0x127 ; 295 - SendMessage WM_CHANGEUISTATE, 0x10001, 0, lv - } -} - -ShowHelpFile() { - SetWorkingDir ROOT_DIR - main := Map(), sub := Map() - other := Map(), other.CaseSense := "off" - hashes := ReadHashes('UX\installed-files.csv', item => item.Path ~= 'i)\.chm$') - Loop Files "*.chm", "FR" { - SplitPath A_LoopFilePath,, &dir,, &name - if name = "AutoHotkey" { - if !(f := hashes.Get(A_LoopFilePath, false)) - continue - v := GetMajor(f.Version) - if !(cur := main.Get(v, false)) || VerCompare(cur.Version, f.Version) < 0 - main[v] := f - sub[f.Version] := f - } - else - other[A_LoopFilePath] := name (dir != "" && name != dir ? " (" dir ")" : "") - } - - if sub.Count = 1 && other.Count = 0 { - for , f in main { ; Don't bother showing online options in this case. - Run f.Path - return - } - } - - m := Menu() - if main.Count { - m.Add "Offline help", (*) => 0 - m.Disable "1&" - } - for , f in main { - m.Add "v&" f.Version, openIt.Bind(f.Path) - sub.Delete(f.Version) - } - if sub.Count { - subm := Menu() - for , f in sub - subm.Insert "1&", "v" f.Version, openIt.Bind(f.Path) - m.Add "More", subm - } - - m.Add "Online help", (*) => 0 - m.Disable "Online help" - prefix := main.Count ? "v" : "v&" - m.Add prefix "1.1", (*) => Run("https://www.autohotkey.com/docs/v1/") - m.Add prefix "2.0", (*) => Run("https://www.autohotkey.com/docs/v2/") - - if other.Count { - m.Add "Other files", (*) => 0 - m.Disable "Other files" - } - for f, t in other - m.Add t, openIt.Bind(f) - - m.Show - openIt(f, *) => Run(f) -} - -AutoHotkeyDashGui.Show() +; Dash: AutoHotkey's "main menu". +; Run the script to show the GUI. +#include inc\bounce-v1.ahk +/* v1 stops here */ +#requires AutoHotkey v2.0 + +#NoTrayIcon +#SingleInstance Force + +#include inc\ui-base.ahk +#include ui-launcherconfig.ahk +#include ui-editor.ahk +#include ui-newscript.ahk + +DashRegKey := 'HKCU\Software\AutoHotkey\Dash' + +class AutoHotkeyDashGui extends AutoHotkeyUxGui { + __new() { + super.__new("AutoHotkey Dash") + + lv := this.AddListMenu('vLV LV0x40 w250', ["Name", "Desc"]) + lv.OnEvent("Click", "ItemClicked") + lv.OnEvent("ItemFocus", "ItemFocused") + lv.OnNotify(-155, "KeyPressed") + + this.AddButton("xp yp wp yp Hidden Default").OnEvent("Click", "EnterPressed") + + il := IL_Create(,, true) + lv.SetImageList(il, 0) + il2 := IL_Create(,, false) + lv.SetImageList(il2, 1) + addIcon(p*) =>(IL_Add(il, p*), IL_Add(il2, p*)) + + lv.Add("Icon" addIcon(A_AhkPath, 2) + , "New script", "Create a script or manage templates") + lv.Add("Icon" addIcon("imageres.dll", -111) + , "Compile", "Open Ahk2Exe - convert .ahk to .exe") + lv.Add("Icon" addIcon("imageres.dll", -99) + , "Help files (F1)") + lv.Add("Icon" addIcon(A_ScriptDir '\inc\spy.ico', 1) + , "Window spy") + lv.Add("Icon" addIcon("imageres.dll", -116) + , "Launch settings", "Configure how .ahk files are opened") + lv.Add("Icon" addIcon("notepad.exe", 1) + , "Editor settings", "Set your default script editor") + ; lv.Add("Icon" addIcon("mmc.exe") + ; , "Maintenance", "Repair settings or add/remove versions") + ; lv.Add(, "Auto-start", "Run scripts automatically at logon") + ; lv.Add(, "Downloads", "Get related tools") + + lv.AutoSize() + lv.GetPos(,,, &h) + + if !RegRead(DashRegKey, 'SuppressIntro', false) { + this.SetFont('s12') + this.AddText('yp x+m', "Welcome!") + this.SetFont('s9') + this.AddText('xp', "This is the Dash. It provides access to tools, settings and help files.") + this.AddText('xp', "To learn how to use AutoHotkey, refer to:") + this.AddLink('xp', " + ( + `s • Using the Program + • How to Write Hotkeys + • How to Send Keystrokes + • How to Run Programs + • How to Manage Windows + • Quick Reference + )").OnEvent('Click', 'LinkClicked') + + checkBox := this.AddCheckbox('Checked', "Show this info next time") + checkBox.GetPos(,,, &hc) + checkBox.Move(, h - hc) + checkBox.OnEvent('Click', 'SetIntroPref') + } + + this.Show("Hide h" (h + this.MarginY*2)) + } + + LinkClicked(ctrl, id, href) { + if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm') + Run 'hh.exe "ms-its:' chm '::docs/' href '"' + else + Run 'https://www.autohotkey.com/docs/v2/' href + } + + SetIntroPref(checkBox, *) { + if checkBox.Value { ; Show intro + try RegDelete(DashRegKey, 'SuppressIntro') + } else + RegWrite(true, 'REG_DWORD', DashRegKey, 'SuppressIntro') + } + + KeyPressed(lv, lParam) { + switch NumGet(lParam, A_PtrSize * 3, "Short") { + case 0x70: ; F1 + ShowHelpFile() + } + } + + EnterPressed(*) { + lv := this["LV"] + this.ItemClicked(lv, lv.GetNext(,'F')) + } + + ItemClicked(lv, item) { + switch item && RegExReplace(lv.GetText(item), ' .*') { + case "New": + NewScriptGui.Show() + case "Compile": + if WinExist("Ahk2Exe ahk_class AutoHotkeyGUI") + WinActivate + else if FileExist(ROOT_DIR '\Compiler\Ahk2Exe.exe') + Run '"' ROOT_DIR '\Compiler\Ahk2Exe.exe"' + else + Run Format('"{1}" /script "{2}\install-ahk2exe.ahk"', A_AhkPath, A_ScriptDir) + case "Help": + ShowHelpFile() + case "Window": + try { + Run '"' A_MyDocuments '\AutoHotkey\WindowSpy.ahk"' + return + } + static AHK_FILE_WINDOWSPY := 0xFF7A ; 65402 + static WM_COMMAND := 0x111 ; 273 + SendMessage WM_COMMAND, AHK_FILE_WINDOWSPY, 0, A_ScriptHwnd + if WinWait("Window Spy ahk_class AutoHotkeyGUI",, 1) + WinActivate + case "Launch": + LauncherConfigGui.Show() + case "Editor": + DefaultEditorGui.Show() + } + } + + ItemFocused(lv, item) { + static WM_CHANGEUISTATE := 0x127 ; 295 + SendMessage WM_CHANGEUISTATE, 0x10001, 0, lv + } +} + +ShowHelpFile() { + SetWorkingDir ROOT_DIR + main := Map(), sub := Map() + other := Map(), other.CaseSense := "off" + hashes := ReadHashes('UX\installed-files.csv', item => item.Path ~= 'i)\.chm$') + Loop Files "*.chm", "FR" { + SplitPath A_LoopFilePath,, &dir,, &name + if name = "AutoHotkey" { + if !(f := hashes.Get(A_LoopFilePath, false)) + continue + v := GetMajor(f.Version) + if !(cur := main.Get(v, false)) || VerCompare(cur.Version, f.Version) < 0 + main[v] := f + sub[f.Version] := f + } + else + other[A_LoopFilePath] := name (dir != "" && name != dir ? " (" dir ")" : "") + } + + if sub.Count = 1 && other.Count = 0 { + for , f in main { ; Don't bother showing online options in this case. + Run f.Path + return + } + } + + m := Menu() + if main.Count { + m.Add "Offline help", (*) => 0 + m.Disable "1&" + } + for , f in main { + m.Add "v&" f.Version, openIt.Bind(f.Path) + sub.Delete(f.Version) + } + if sub.Count { + subm := Menu() + for , f in sub + subm.Insert "1&", "v" f.Version, openIt.Bind(f.Path) + m.Add "More", subm + } + + m.Add "Online help", (*) => 0 + m.Disable "Online help" + prefix := main.Count ? "v" : "v&" + m.Add prefix "1.1", (*) => Run("https://www.autohotkey.com/docs/v1/") + m.Add prefix "2.0", (*) => Run("https://www.autohotkey.com/docs/v2/") + + if other.Count { + m.Add "Other files", (*) => 0 + m.Disable "Other files" + } + for f, t in other + m.Add t, openIt.Bind(f) + + m.Show + openIt(f, *) => Run(f) +} + +AutoHotkeyDashGui.Show() diff --git a/UX/ui-editor.ahk b/UX/ui-editor.ahk index 02409e1..e15acf4 100644 --- a/UX/ui-editor.ahk +++ b/UX/ui-editor.ahk @@ -1,238 +1,238 @@ -; This script shows a GUI for setting the default .ahk editor. -#requires AutoHotkey v2.0 - -#NoTrayIcon -#SingleInstance Off - -#include launcher.ahk -#include inc\CommandLineToArgs.ahk - -class EditorSelectionGui extends AutoHotkeyUxGui { - __new(cmdLine) { - super.__new("Select an editor") - - lv := this.AddListMenu('vEds LV0x40 w300', ["Editor"]) - this.IconList := il := IL_Create(,, true) - lv.SetImageList(il, 0) - for app in this.Apps := GetEditorApps() { - try - icon := IL_Add(il, app.exe) - catch - icon := -1 - lv.Add('Icon' icon, app.name) - } - this.SelectEditorByCmd(cmdLine) - lv.AutoSize(8) - lv.GetPos(&x, &y, &w, &h) - x += w - y += h - - this.AddText('xm w' w ' y' y, "Command line") - this.AddEdit('xm wp r2 -WantReturn vCmd', cmdLine).OnEvent('Change', 'CmdChanged') - - this.AddText('xm h25 18 w' w) - this.AddPicture('x56 Icon-81 w16 yp+4', "imageres.dll") - this.AddLink('x76 yp-1', "Editors with AutoHotkey support") - .OnEvent('Click', 'ShowHelpEditors') - - this.AddButton('xm w80', "&Browse").OnEvent('Click', 'Browse') - this.AddButton('Default yp w80 x' x - 160 - this.MarginY, "&OK").OnEvent('Click', 'Confirm') - this.AddButton('yp w80 x' x - 80, "&Cancel").OnEvent('Click', (c, *) => c.Gui.Hide()) - - this["Eds"].OnEvent("ItemFocus", "EditorSelected") - this.CmdChanged() - } - - ShowHelpEditors(*) { - if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm') - Run 'hh.exe "ms-its:' chm '::docs/lib/Edit.htm#Editors"' - else - Run 'https://www.autohotkey.com/docs/v2/lib/Edit.htm#Editors' - } - - Browse(*) { - app := this.FileSelect(3,,, "Apps (*.exe; *.ahk)") - if app = "" - return - this['Cmd'].Value := this.GetAppCmd(app) - this.CmdChanged() - } - - GetAppCmd(app) { - SplitPath app,,, &ext - if ext != "ahk" - return Format('"{1}" "%l"', app) - lp := GetLaunchParameters(app, true) - if !lp.exe - return "" - ; Try to use a path that will work if the user installs a new version and removes this one - ; (rather than invoking the launcher every time the edit verb is executed). - adaptivePath := RegExReplace(lp.exe.Path, lp.v = 1 ? '\\v1[^\\]*(?=\\[^\\]*$)' : '\\v2\K[^\\]+(?=\\[^\\]*$)') - trace '![Launcher] ' adaptivePath - try - adaptiveExe := GetExeInfo(adaptivePath) - if !IsSet(adaptiveExe) || VerCompare(adaptiveExe.Version, lp.v) < 0 - adaptivePath := "" - cmd := Format('"{}"', FileExist(adaptivePath) ? adaptivePath : lp.exe.Path) - for sw in lp.switches - cmd .= ' ' sw - return cmd .= Format(' "{}" "%l"', app) - } - - EditorSelected(lv, index) { - this['Cmd'].Value := this.Apps[index].cmd - this.CmdChanged() - } - - CmdChanged(ctrl:=unset, *) { - if IsSet(ctrl) && n := this['Eds'].GetNext() - this['Eds'].Modify(n, '-Select') - cmd := this['Cmd'].Value - this['OK'].Enabled := cmd != "" && FindExecutable(CommandLineToArgs(cmd)[1]) != "" - } - - SelectEditorByCmd(cmd) { - if cmd = "" - return - for app in this.Apps { - if app.cmd = cmd { - this['Eds'].Modify(A_Index, 'Focus Select') - return - } - } - ; App not in the list, so add it. - args := CommandLineToArgs(cmd) - try { - exe := GetExeInfo(FindExecutable(args.RemoveAt(1))) - app := {cmd: cmd, exe: exe.Path, name: exe.Description} - if SubStr(app.name, 1, 10) = "AutoHotkey" && (ahk := ahkArg(args)) { - ; The app itself appears to be a script, so show the script name. - app.name := ahk - } - this.Apps.Push(app) - try - icon := IL_Add(this.IconList, app.exe, IsSet(ahk) && ahk ? 2 : 1) - catch - icon := -1 - this['Eds'].Add('Focus Select Icon' icon, app.name) - } - - ahkArg(args) { - for arg in args { - if SubStr(arg, 1, 1) = '/' - continue - return SubStr(arg, -4) = '.ahk' ? arg : '' - } - return '' - } - } - - Confirm(*) { - this.Hide() - this.OnConfirm(this['Cmd'].Value) - } -} - -class DefaultEditorGui extends EditorSelectionGui { - __new(scriptToEdit:=unset) { - cmd := RegRead('HKCR\AutoHotkeyScript\shell\edit\command',, '') - if InStr(cmd, A_LineFile) - cmd := '' - super.__new(cmd) - if IsSet(scriptToEdit) - this.ScriptToEdit := scriptToEdit - } - - OnConfirm(cmd) { - RegWrite(cmd, 'REG_SZ', 'HKCU\Software\Classes\AutoHotkeyScript\shell\edit\command') - if this.HasProp('ScriptToEdit') - Run('edit "' this.ScriptToEdit '"') - } -} - -GetEditorApps() { - apps := [] - apps.byName := Map(), apps.byName.CaseSense := 'off' - addAssoc(assoc, flag, src) { - static ASSOCSTR_COMMAND := 1 - static ASSOCSTR_EXECUTABLE := 2 - static ASSOCSTR_FRIENDLYAPPNAME := 4 - try { - name := AssocQueryString(flag, ASSOCSTR_FRIENDLYAPPNAME, assoc) - exe := AssocQueryString(flag, ASSOCSTR_EXECUTABLE, assoc) - } - if !IsSet(exe) || name ~= 'i)^AutoHotkey|^Ahk2Exe' - return - try - cmd := AssocQueryString(flag, ASSOCSTR_COMMAND, assoc) - catch - cmd := '"' exe '" "%l"' - else if cmd = "" - return - if !(InStr(cmd, "%1") || InStr(cmd, "%l")) - cmd .= ' "%l"' - if name = "NOTEPAD.EXE" - name := "Notepad" - if apps.byName.Has(name) - return - app := {cmd: cmd, exe: exe, name: name} - apps.byName[name] := app - apps.Push(app) - } - addAppExe(app, src) { - static ASSOCF_INIT_BYEXENAME := 2 ; 0x2 - addAssoc app, ASSOCF_INIT_BYEXENAME, src - } - addProgId(app, src) { - addAssoc app, 0, src - } - - for ext in ["ahk", "txt"] { - owl := "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\." ext "\OpenWithList" - mru := RegRead(owl, "MRUList", "") - Loop Parse mru { - if app := RegRead(owl, A_LoopField, "") - addAppExe app, "Explorer\." ext "\OWL" - } - Loop Reg "HKCR\." ext "\OpenWithProgIds", "V" { - addProgId A_LoopRegName, "HKCR\." ext "\OWPI" - } - Loop Reg "HKCR\." ext "\OpenWithList", "K" { - if !InStr(A_LoopRegName, "Ahk2Exe") - addAppExe A_LoopRegName, "HKCR\." ext "\OWL" - } - } - addAppExe "notepad.exe", "explicit" - return apps -} - -; Return the path of an executable file, if given something that could be executed -; in a shell verb (excluding args). Although shell verb commands require an exe, -; they do also look in \App Paths\, like ShellExecute, unlike CreateProcess. -FindExecutable(name) { - if SubStr(name, -4) != ".exe" ; For odd cases like 'notepad "%1"' - name .= ".exe" - if FileExist(name) - return name - if InStr(name, "\") || InStr(name, ":") - return "" - buf := Buffer(260*2) - if DllCall("shell32\FindExecutable", "str", name, "ptr", 0, "ptr", buf, "uint") > 32 ; "Returns a value greater than 32 if successful" - return StrGet(buf) - static AppPaths := '\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' - path := RegRead('HKCU' AppPaths name,, '') || RegRead('HKLM' AppPaths name,, '') - ; The system permits quotes, and some applications are registered that way. - return StrReplace(path, '"') -} - -AssocQueryString(flags, strtype, assoc) { - DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc - , "ptr", 0, "ptr", 0, "uint*", &bufsize := 0, "hresult") - buf := Buffer(bufsize * 2) - DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc - , "ptr", 0, "ptr", buf, "uint*", &bufsize, "hresult") - return StrGet(buf, "UTF-16") -} - -if A_LineFile = A_ScriptFullPath - DefaultEditorGui.Show(A_Args*) +; This script shows a GUI for setting the default .ahk editor. +#requires AutoHotkey v2.0 + +#NoTrayIcon +#SingleInstance Off + +#include launcher.ahk +#include inc\CommandLineToArgs.ahk + +class EditorSelectionGui extends AutoHotkeyUxGui { + __new(cmdLine) { + super.__new("Select an editor") + + lv := this.AddListMenu('vEds LV0x40 w300', ["Editor"]) + this.IconList := il := IL_Create(,, true) + lv.SetImageList(il, 0) + for app in this.Apps := GetEditorApps() { + try + icon := IL_Add(il, app.exe) + catch + icon := -1 + lv.Add('Icon' icon, app.name) + } + this.SelectEditorByCmd(cmdLine) + lv.AutoSize(8) + lv.GetPos(&x, &y, &w, &h) + x += w + y += h + + this.AddText('xm w' w ' y' y, "Command line") + this.AddEdit('xm wp r2 -WantReturn vCmd', cmdLine).OnEvent('Change', 'CmdChanged') + + this.AddText('xm h25 18 w' w) + this.AddPicture('x56 Icon-81 w16 yp+4', "imageres.dll") + this.AddLink('x76 yp-1', "Editors with AutoHotkey support") + .OnEvent('Click', 'ShowHelpEditors') + + this.AddButton('xm w80', "&Browse").OnEvent('Click', 'Browse') + this.AddButton('Default yp w80 x' x - 160 - this.MarginY, "&OK").OnEvent('Click', 'Confirm') + this.AddButton('yp w80 x' x - 80, "&Cancel").OnEvent('Click', (c, *) => c.Gui.Hide()) + + this["Eds"].OnEvent("ItemFocus", "EditorSelected") + this.CmdChanged() + } + + ShowHelpEditors(*) { + if FileExist(chm := ROOT_DIR '\v2\AutoHotkey.chm') + Run 'hh.exe "ms-its:' chm '::docs/misc/Editors.htm"' + else + Run 'https://www.autohotkey.com/docs/v2/misc/Editors.htm' + } + + Browse(*) { + app := this.FileSelect(3,,, "Apps (*.exe; *.ahk)") + if app = "" + return + this['Cmd'].Value := this.GetAppCmd(app) + this.CmdChanged() + } + + GetAppCmd(app) { + SplitPath app,,, &ext + if ext != "ahk" + return Format('"{1}" "%l"', app) + lp := GetLaunchParameters(app, true) + if !lp.exe + return "" + ; Try to use a path that will work if the user installs a new version and removes this one + ; (rather than invoking the launcher every time the edit verb is executed). + adaptivePath := RegExReplace(lp.exe.Path, lp.v = 1 ? '\\v1[^\\]*(?=\\[^\\]*$)' : '\\v2\K[^\\]+(?=\\[^\\]*$)') + trace '![Launcher] ' adaptivePath + try + adaptiveExe := GetExeInfo(adaptivePath) + if !IsSet(adaptiveExe) || VerCompare(adaptiveExe.Version, lp.v) < 0 + adaptivePath := "" + cmd := Format('"{}"', FileExist(adaptivePath) ? adaptivePath : lp.exe.Path) + for sw in lp.switches + cmd .= ' ' sw + return cmd .= Format(' "{}" "%l"', app) + } + + EditorSelected(lv, index) { + this['Cmd'].Value := this.Apps[index].cmd + this.CmdChanged() + } + + CmdChanged(ctrl:=unset, *) { + if IsSet(ctrl) && n := this['Eds'].GetNext() + this['Eds'].Modify(n, '-Select') + cmd := this['Cmd'].Value + this['OK'].Enabled := cmd != "" && FindExecutable(CommandLineToArgs(cmd)[1]) != "" + } + + SelectEditorByCmd(cmd) { + if cmd = "" + return + for app in this.Apps { + if app.cmd = cmd { + this['Eds'].Modify(A_Index, 'Focus Select') + return + } + } + ; App not in the list, so add it. + args := CommandLineToArgs(cmd) + try { + exe := GetExeInfo(FindExecutable(args.RemoveAt(1))) + app := {cmd: cmd, exe: exe.Path, name: exe.Description} + if SubStr(app.name, 1, 10) = "AutoHotkey" && (ahk := ahkArg(args)) { + ; The app itself appears to be a script, so show the script name. + app.name := ahk + } + this.Apps.Push(app) + try + icon := IL_Add(this.IconList, app.exe, IsSet(ahk) && ahk ? 2 : 1) + catch + icon := -1 + this['Eds'].Add('Focus Select Icon' icon, app.name) + } + + ahkArg(args) { + for arg in args { + if SubStr(arg, 1, 1) = '/' + continue + return SubStr(arg, -4) = '.ahk' ? arg : '' + } + return '' + } + } + + Confirm(*) { + this.Hide() + this.OnConfirm(this['Cmd'].Value) + } +} + +class DefaultEditorGui extends EditorSelectionGui { + __new(scriptToEdit:=unset) { + cmd := RegRead('HKCR\AutoHotkeyScript\shell\edit\command',, '') + if InStr(cmd, A_LineFile) + cmd := '' + super.__new(cmd) + if IsSet(scriptToEdit) + this.ScriptToEdit := scriptToEdit + } + + OnConfirm(cmd) { + RegWrite(cmd, 'REG_SZ', 'HKCU\Software\Classes\AutoHotkeyScript\shell\edit\command') + if this.HasProp('ScriptToEdit') + Run('edit "' this.ScriptToEdit '"') + } +} + +GetEditorApps() { + apps := [] + apps.byName := Map(), apps.byName.CaseSense := 'off' + addAssoc(assoc, flag, src) { + static ASSOCSTR_COMMAND := 1 + static ASSOCSTR_EXECUTABLE := 2 + static ASSOCSTR_FRIENDLYAPPNAME := 4 + try { + name := AssocQueryString(flag, ASSOCSTR_FRIENDLYAPPNAME, assoc) + exe := AssocQueryString(flag, ASSOCSTR_EXECUTABLE, assoc) + } + if !IsSet(exe) || name ~= 'i)^AutoHotkey|^Ahk2Exe' + return + try + cmd := AssocQueryString(flag, ASSOCSTR_COMMAND, assoc) + catch + cmd := '"' exe '" "%l"' + else if cmd = "" + return + if !(InStr(cmd, "%1") || InStr(cmd, "%l")) + cmd .= ' "%l"' + if name = "NOTEPAD.EXE" + name := "Notepad" + if apps.byName.Has(name) + return + app := {cmd: cmd, exe: exe, name: name} + apps.byName[name] := app + apps.Push(app) + } + addAppExe(app, src) { + static ASSOCF_INIT_BYEXENAME := 2 ; 0x2 + addAssoc app, ASSOCF_INIT_BYEXENAME, src + } + addProgId(app, src) { + addAssoc app, 0, src + } + + for ext in ["ahk", "txt"] { + owl := "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\." ext "\OpenWithList" + mru := RegRead(owl, "MRUList", "") + Loop Parse mru { + if app := RegRead(owl, A_LoopField, "") + addAppExe app, "Explorer\." ext "\OWL" + } + Loop Reg "HKCR\." ext "\OpenWithProgIds", "V" { + addProgId A_LoopRegName, "HKCR\." ext "\OWPI" + } + Loop Reg "HKCR\." ext "\OpenWithList", "K" { + if !InStr(A_LoopRegName, "Ahk2Exe") + addAppExe A_LoopRegName, "HKCR\." ext "\OWL" + } + } + addAppExe "notepad.exe", "explicit" + return apps +} + +; Return the path of an executable file, if given something that could be executed +; in a shell verb (excluding args). Although shell verb commands require an exe, +; they do also look in \App Paths\, like ShellExecute, unlike CreateProcess. +FindExecutable(name) { + if SubStr(name, -4) != ".exe" ; For odd cases like 'notepad "%1"' + name .= ".exe" + if FileExist(name) + return name + if InStr(name, "\") || InStr(name, ":") + return "" + buf := Buffer(260*2) + if DllCall("shell32\FindExecutable", "str", name, "ptr", 0, "ptr", buf, "uint") > 32 ; "Returns a value greater than 32 if successful" + return StrGet(buf) + static AppPaths := '\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\' + path := RegRead('HKCU' AppPaths name,, '') || RegRead('HKLM' AppPaths name,, '') + ; The system permits quotes, and some applications are registered that way. + return StrReplace(path, '"') +} + +AssocQueryString(flags, strtype, assoc) { + DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc + , "ptr", 0, "ptr", 0, "uint*", &bufsize := 0, "hresult") + buf := Buffer(bufsize * 2) + DllCall("shlwapi\AssocQueryStringW", "int", flags, "int", strtype, "wstr", assoc + , "ptr", 0, "ptr", buf, "uint*", &bufsize, "hresult") + return StrGet(buf, "UTF-16") +} + +if A_LineFile = A_ScriptFullPath + DefaultEditorGui.Show(A_Args*) diff --git a/UX/ui-launcherconfig.ahk b/UX/ui-launcherconfig.ahk index c9449ab..644f155 100644 --- a/UX/ui-launcherconfig.ahk +++ b/UX/ui-launcherconfig.ahk @@ -1,183 +1,191 @@ -; This script shows a GUI for configuring the launcher. -#requires AutoHotkey v2.0 - -#NoTrayIcon - -#include inc\launcher-common.ahk -#include inc\ui-base.ahk - -GetVersions() { - vmap := Map(1, Map(), 2, Map()) - for ,f in GetUsableAutoHotkeyExes() { - try - vmap[GetMajor(f.Version)][f.Version] := true - catch as e - trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message - } - vmap[1] := [vmap[1]*] - vmap[2] := [vmap[2]*] - return vmap -} - -class LauncherConfigGui extends AutoHotkeyUxGui { - __new() { - super.__new("AutoHotkey Launch Config") - - cmd := RegRead('HKCR\AutoHotkeyScript\shell\open\command',, '') - usingLauncher := InStr(cmd, 'UX\launcher.ahk') != 0 - currentExe := !usingLauncher && RegExMatch(cmd, '^"(.*?)"(?= )', &m) ? m.1 : "" - try - if currentExe && GetExeInfo(currentExe).Description = "AutoHotkey Launcher" ; Support compiled launcher - usingLauncher := true, currentExe := "" - if InStr(currentExe, '\AutoHotkeyUX.exe') - currentExe := "" ; Don't default to AutoHotkeyUX.exe when disabling launcher - - versions := GetVersions() - - ; this.AddCheckbox("Checked", "Enable drag && drop on .ahk files") - - this.AddRadio('vUseLauncher Checked' usingLauncher, "Auto-detect version when launching script") - .OnEvent('Click', 'ChangedMode') - this.AddRadio('vUseSpecific Checked' (!usingLauncher), "Run all scripts with a specific interpreter") - .OnEvent('Click', 'ChangedMode') - - tab := this.AddTab('w0 h0 y+0 vTab -TabStop', ["Launcher", "Specific"]) - - tab.UseTab(1) - this.AddText('xm yp+12 Section', "Preferred interpreter by major version") - this.AddDDL('vVersion1 y+3 w110 Choose1', ["Latest 1.x", versions[1]*]) - .OnEvent('Change', "ChangedVersion") - this.AddComboBox('vBuild1 yp w150', ["Unicode 64-bit", "Unicode 32-bit", "ANSI 32-bit"]) - .OnEvent('Change', 'ChangedBuild') - this.AddCheckBox('vUIA1 x+m yp+2', "UI Access") - .OnEvent('Click', 'ChangedUIA') - this.AddDDL('vVersion2 xs w110 Choose1', ["Latest 2.x", versions[2]*]) - .OnEvent('Change', 'ChangedVersion') - this.AddComboBox('vBuild2 yp w150', ["64-bit", "32-bit"]) - .OnEvent('Change', 'ChangedBuild') - this.AddCheckBox('vUIA2 x+m yp+2', "UI Access") - .OnEvent('Click', 'ChangedUIA') - this.AddText('xs y+m+8', "When detection fails") - this.AddDDL('vFallback y+3 w110 Choose3', ["Use v1.x", "Use v2.x", "Ask the user"]) - .OnEvent('Change', "ChangedFallback") - this.AddCheckbox('vIdentify xs y+m+8 Checked', "Try to identify version based on syntax") - .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher', 'Identify')) - this.AddCheckbox('vLauncherUTF8 xs Checked', "Default to UTF-8 even for v1 scripts") - .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher\v1', 'UTF8')) - - tab.UseTab(2) - exeBox := this.AddEdit('vExePath xs ys w326 ReadOnly') - if currentExe - exeBox.Text := currentExe - else if FileExist(f := ROOT_DIR '\v2\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe') - exeBox.Text := f - - static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' SysGet(49), &imgtype) - this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse") - .OnEvent('Click', 'BrowseForExe') - - this.AddCheckBox('vCustomUTF8 xm y+m+4 Hidden', "Default to UTF-8") - .OnEvent('Click', 'UpdateVerbs') - - tab.UseTab() - this.AddButton('vClose x292 w70 Default', "&Close") - .OnEvent('Click', (ctrl, *) => ctrl.Gui.Hide()) - - ; size := 32 * A_ScreenDPI // 96 - ; DllCall("comctl32\LoadIconWithScaleDown", "ptr", 0, "int", 32514, "int", size, "int", size, "ptr*", &icon:=0, "hresult") - ; help := this.AddButton('0x40 x322 ym w' 40 ' h' 40) - ; SendMessage(0xF7, 1, icon, help) - - tab.Choose(usingLauncher ? 1 : 2) - - Loop 2 { - section := 'Launcher\v' A_Index - v := ConfigRead(section, 'Version', '') - try this['Version' A_Index].Text := v || 'Latest ' A_Index '.x' - try this['Build' A_Index].Text := ConfigRead(section, 'Build', '') - try this['UIA' A_Index].Value := ConfigRead(section, 'UIA', false) - if this['Build' A_Index].Text = "" - ControlChooseIndex(1, this['Build' A_Index]) - } - v := ConfigRead('Launcher', 'Fallback', '') - IsInteger(v) && this['Fallback'].Value := v - this['Identify'].Value := ConfigRead('Launcher', 'Identify', true) - this['LauncherUTF8'].Value := ConfigRead('Launcher\v1', 'UTF8', false) - - this.ChangedMode() - this.ChangedExe() - } - - ChangedMode(sourceCtrl:=false, *) { - usingLauncher := this['UseLauncher'].Value - this['Tab'].Choose(usingLauncher ? 1 : 2) - this[usingLauncher ? 'LauncherUTF8' : 'CustomUTF8'].GetPos(, &y1, , &h1) - this['Close'].GetPos(,,, &h2) - this['Close'].Move(, y1) - this.Show('h' (y1 + h2 + 10)) - if sourceCtrl - this.UpdateVerbs() - } - - ChangedExe() { - exe := this['ExePath'].Text - if exe = "" - return - try - exeVersion := FileGetVersion(exe) - catch { - MsgBox "The selected EXE appears to be invalid.`n`nSpecifically: " exe,, 'Icon!' - return - } - if !this['CustomUTF8'].Visible && this['LauncherUTF8'].Value - this['CustomUTF8'].Value := true - this['CustomUTF8'].Visible := VerCompare(exeVersion, '2') < 0 - } - - ChangedVersion(ctrl, *) => - ConfigWrite(ctrl.Value = 1 ? '' : ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Version') - - ChangedBuild(ctrl, *) => - ConfigWrite(ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Build') - - ChangedUIA(ctrl, *) => - ConfigWrite(ctrl.Value, 'Launcher\v' SubStr(ctrl.Name, -1), 'UIA') - - ChangedFallback(ctrl, *) => - ConfigWrite(ctrl.Value = 3 ? '' : ctrl.Value, 'Launcher', 'Fallback') - - BrowseForExe(*) { - exe := this.FileSelect('3', this['ExePath'].Text, "Select an AutoHotkey.exe", "EXE Files (*.exe)") - if exe = "" - return - this['ExePath'].Text := exe - this.ChangedExe() - this.UpdateVerbs() - } - - UpdateVerbs(*) { - ; FIXME: reg key and FriendlyAppName should be defined in only one place - static key := 'HKCU\Software\Classes\AutoHotkeyScript\Shell\' - if this['UseLauncher'].Value { - cmd := Format('"{1}" "{2}\launcher.ahk" "%1" %*', A_AhkPath, A_ScriptDir) - appname := "AutoHotkey Launcher" - } else { - exe := this['ExePath'].Text - if exe = "" - return - if !FileExist(exe) - throw - cmd := Format('"{1}" {2}"%1" %*', exe, this['CustomUTF8'].Value ? '/cp65001 ' : '') - ; Deleting FriendlyAppName from HKCU won't work if the installer uses HKLM, - ; so explicitly set it to the file's description - appname := GetExeInfo(exe).Description - } - RegWrite appname, 'REG_SZ', key 'Open', 'FriendlyAppName' - RegWrite cmd, 'REG_SZ', key 'Open\Command' - if RegRead('HKCR\AutoHotkeyScript\Shell\RunAs',, '') - RegWrite cmd, 'REG_SZ', key 'RunAs\Command' - } -} - -if A_ScriptFullPath = A_LineFile +; This script shows a GUI for configuring the launcher. +#requires AutoHotkey v2.0 + +#NoTrayIcon + +#include inc\launcher-common.ahk +#include inc\ui-base.ahk + +GetVersions() { + vmap := Map(1, Map(), 2, Map()) + for ,f in GetUsableAutoHotkeyExes() { + try + vmap[GetMajor(f.Version)][f.Version] := true + catch as e + trace "-[Launcher] " type(e) " checking file " A_LoopFileName ": " e.message + } + vmap[1] := [vmap[1]*] + vmap[2] := [vmap[2]*] + return vmap +} + +class LauncherConfigGui extends AutoHotkeyUxGui { + __new() { + super.__new("AutoHotkey Launch Config") + + cmd := RegRead('HKCR\AutoHotkeyScript\shell\open\command',, '') + usingLauncher := InStr(cmd, 'UX\launcher.ahk') != 0 + currentExe := !usingLauncher && RegExMatch(cmd, '^"(.*?)"(?= )', &m) ? m.1 : "" + try + if currentExe && GetExeInfo(currentExe).Description = "AutoHotkey Launcher" ; Support compiled launcher + usingLauncher := true, currentExe := "" + if InStr(currentExe, '\AutoHotkeyUX.exe') + currentExe := "" ; Don't default to AutoHotkeyUX.exe when disabling launcher + + versions := GetVersions() + + ; this.AddCheckbox("Checked", "Enable drag && drop on .ahk files") + + this.AddRadio('vUseLauncher Checked' usingLauncher, "Auto-detect version when launching script") + .OnEvent('Click', 'ChangedMode') + this.AddRadio('vUseSpecific Checked' (!usingLauncher), "Run all scripts with a specific interpreter") + .OnEvent('Click', 'ChangedMode') + + tab := this.AddTab('w0 h0 y+0 vTab -TabStop', ["Launcher", "Specific"]) + + tab.UseTab(1) + this.AddText('xm yp+12 Section', "Preferred interpreter by major version") + this.AddDDL('vVersion1 y+3 w110 Choose1', ["Latest 1.x", versions[1]*]) + .OnEvent('Change', "ChangedVersion") + this.AddComboBox('vBuild1 yp w150', ["Unicode 64-bit", "Unicode 32-bit", "ANSI 32-bit"]) + .OnEvent('Change', 'ChangedBuild') + this.AddCheckBox('vUIA1 x+m yp+2', "UI Access") + .OnEvent('Click', 'ChangedUIA') + this.AddDDL('vVersion2 xs w110 Choose1', ["Latest 2.x", versions[2]*]) + .OnEvent('Change', 'ChangedVersion') + this.AddComboBox('vBuild2 yp w150', ["64-bit", "32-bit"]) + .OnEvent('Change', 'ChangedBuild') + this.AddCheckBox('vUIA2 x+m yp+2', "UI Access") + .OnEvent('Click', 'ChangedUIA') + this.AddText('xs y+m+8', "When detection fails") + this.AddDDL('vFallback y+3 w110 Choose3', ["Use v1.x", "Use v2.x", "Ask the user"]) + .OnEvent('Change', "ChangedFallback") + this.AddCheckbox('vIdentify xs y+m+8 Checked', "Try to identify version based on syntax") + .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher', 'Identify')) + this.AddCheckbox('vLauncherUTF8 xs Checked', "Default to UTF-8 even for v1 scripts") + .OnEvent('Click', (c, *) => ConfigWrite(c.Value, 'Launcher\v1', 'UTF8')) + + tab.UseTab(2) + exeBox := this.AddEdit('vExePath xs ys w326 ReadOnly') + if currentExe + exeBox.Text := currentExe + else if FileExist(f := ROOT_DIR '\v2\AutoHotkey' (A_Is64bitOS ? '64' : '32') '.exe') + exeBox.Text := f + + static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' SysGet(49), &imgtype) + this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse") + .OnEvent('Click', 'BrowseForExe') + + this.AddCheckBox('vCustomUTF8 xm y+m+4 Hidden', "Default to UTF-8") + .OnEvent('Click', 'UpdateVerbs') + + tab.UseTab() + this.AddButton('vClose x292 w70 Default', "&Close") + .OnEvent('Click', (ctrl, *) => ctrl.Gui.Hide()) + + ; size := 32 * A_ScreenDPI // 96 + ; DllCall("comctl32\LoadIconWithScaleDown", "ptr", 0, "int", 32514, "int", size, "int", size, "ptr*", &icon:=0, "hresult") + ; help := this.AddButton('0x40 x322 ym w' 40 ' h' 40) + ; SendMessage(0xF7, 1, icon, help) + + tab.Choose(usingLauncher ? 1 : 2) + + Loop 2 { + section := 'Launcher\v' A_Index + v := ConfigRead(section, 'Version', '') + try this['Version' A_Index].Text := v || 'Latest ' A_Index '.x' + try this['Build' A_Index].Text := ConfigRead(section, 'Build', '') + try this['UIA' A_Index].Value := ConfigRead(section, 'UIA', false) + if this['Build' A_Index].Text = "" + ControlChooseIndex(1, this['Build' A_Index]) + } + v := ConfigRead('Launcher', 'Fallback', '') + IsInteger(v) && this['Fallback'].Value := v + this['Identify'].Value := ConfigRead('Launcher', 'Identify', true) + this['LauncherUTF8'].Value := ConfigRead('Launcher\v1', 'UTF8', false) + + this.ChangedMode() + this.ChangedExe() + } + + ChangedMode(sourceCtrl:=false, *) { + usingLauncher := this['UseLauncher'].Value + this['Tab'].Choose(usingLauncher ? 1 : 2) + this[usingLauncher ? 'LauncherUTF8' : 'CustomUTF8'].GetPos(, &y1, , &h1) + this['Close'].GetPos(,,, &h2) + this['Close'].Move(, y1) + this.Show('h' (y1 + h2 + 10)) + if sourceCtrl + this.UpdateVerbs() + } + + ChangedExe() { + exe := this['ExePath'].Text + if exe = "" + return + try + exeVersion := FileGetVersion(exe) + catch { + MsgBox "The selected EXE appears to be invalid.`n`nSpecifically: " exe,, 'Icon!' + return + } + if !this['CustomUTF8'].Visible && this['LauncherUTF8'].Value + this['CustomUTF8'].Value := true + this['CustomUTF8'].Visible := VerCompare(exeVersion, '2') < 0 + } + + ChangedVersion(ctrl, *) => + ConfigWrite(ctrl.Value = 1 ? '' : ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Version') + + ChangedBuild(ctrl, *) => + ConfigWrite(ctrl.Text, 'Launcher\v' SubStr(ctrl.Name, -1), 'Build') + + ChangedUIA(ctrl, *) => + ConfigWrite(ctrl.Value, 'Launcher\v' SubStr(ctrl.Name, -1), 'UIA') + + ChangedFallback(ctrl, *) => + ConfigWrite(ctrl.Value = 3 ? '' : ctrl.Value, 'Launcher', 'Fallback') + + BrowseForExe(*) { + exe := this.FileSelect('3', this['ExePath'].Text, "Select an AutoHotkey.exe", "EXE Files (*.exe)") + if exe = "" + return + this['ExePath'].Text := exe + this.ChangedExe() + this.UpdateVerbs() + } + + UpdateVerbs(*) { + ; FIXME: reg key and FriendlyAppName should be defined in only one place + static key := 'HKCU\Software\Classes\AutoHotkeyScript\Shell\' + if this['UseLauncher'].Value { + cmd := Format('"{1}" "{2}\launcher.ahk" "%1" %*', A_AhkPath, A_ScriptDir) + appname := "AutoHotkey Launcher" + } else { + exe := this['ExePath'].Text + if exe = "" + return + if !FileExist(exe) + throw + exe_uia := SubStr(exe, 1, -4) "_UIA.exe" + switches := this['CustomUTF8'].Visible && this['CustomUTF8'].Value ? '/cp65001 ' : '' + cmd := Format('"{1}" {2}"%1" %*', exe, switches) + ; Deleting FriendlyAppName from HKCU won't work if the installer uses HKLM, + ; so explicitly set it to the file's description + appname := GetExeInfo(exe).Description + } + RegWrite appname, 'REG_SZ', key 'Open', 'FriendlyAppName' + RegWrite cmd, 'REG_SZ', key 'Open\Command' + RegWrite cmd, 'REG_SZ', key 'RunAs\Command' + if RegRead('HKCR\AutoHotkeyScript\Shell\UIAccess\Command',, '') { + if IsSet(exe_uia) && FileExist(exe_uia) + cmd := StrReplace(cmd, exe, exe_uia) + else + cmd := Format('"{1}" "{2}\launcher.ahk" /runwith UIA "%1" %*', A_AhkPath, A_ScriptDir) + RegWrite cmd, 'REG_SZ', key 'UIAccess\Command' + } + } +} + +if A_ScriptFullPath = A_LineFile LauncherConfigGui.Show() \ No newline at end of file diff --git a/UX/ui-newscript.ahk b/UX/ui-newscript.ahk index e5881f9..7f5b205 100644 --- a/UX/ui-newscript.ahk +++ b/UX/ui-newscript.ahk @@ -1,296 +1,296 @@ -#requires AutoHotkey v2.0 - -#NoTrayIcon -#SingleInstance Off - -#include inc\common.ahk -#include inc\ui-base.ahk - -class NewScriptGui extends AutoHotkeyUxGui { - __new(path:="") { - super.__new("New Script") - - SplitPath path,, &dir,, &name - if this.ExplorerHwnd := WinActive("ahk_class CabinetWClass") { - this.Opt '+Owner' this.ExplorerHwnd - if dir = "" - dir := GetPathForExplorerWindow(this.ExplorerHwnd) - } - if dir = "" - dir := ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey") - - name := this.AddEdit('vName w272', name != "New AutoHotkey Script" ? name : "") - static EM_SETCUEBANNER := 0x1501 - SendMessage(EM_SETCUEBANNER, true, StrPtr("Untitled"), name) - - static IconSize := SysGet(49) ; SM_CXSMICON - - static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' IconSize, &imgtype) - this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse") - .OnEvent('Click', 'Browse') - - this.AddEdit('vDir xm w300 r1 ReadOnly -TabStop', dir) - - static LVS_SHOWSELALWAYS := 8 ; Seems to have the opposite effect with Explorer theme, at least on Windows 11. - lv := this.AddListMenu("vLV xm w300 -" LVS_SHOWSELALWAYS, ["Name", "Desc", "Path", "Exec"]) - lv.OnEvent('DoubleClick', 'DoubleClicked') - lv.OnEvent('ContextMenu', 'RightClicked') - - deft := GetDefaultTemplate() - lv.Add(deft = "" ? 'Select' : '', "Empty", "Clean slate") - for ,t in this.Templates := GetScriptTemplates() { - if ConfigRead('New\HideTemplate', t.name, false) - continue - lv.Add(deft = t.name ? 'Select Focus' : '', t.name, t.desc) - } - - lv.AutoSize(8) - lv.GetPos(&x, &y, &w, &h) - - static DefaultsIcon := LoadPicture("imageres.dll", 'Icon-114 w' IconSize, &imgtype) - this.AddIconButton('vDefaults r1 w28 xm y' y + h + this.MarginY, DefaultsIcon, "&Defaults") - .OnEvent('Click', 'ChangeDefaults') - - this.AddButton('vCreate yp w75 x150', "&Create").OnEvent('Click', 'Confirm') - this.AddButton('vEdit Default yp wp xm+225', "&Edit").OnEvent('Click', 'Confirm') - if ConfigRead('New', 'DefaultButton', 'Edit') = 'Create' - this['Create'].Opt('Default') - - if this.ExplorerHwnd { - this.Show('AutoSize Hide') - WinGetPos(,, &gw, &gh, this) - WinGetPos(&x, &y, &ew, &eh) - WinMove(x + (ew - gw) // 2, y + (eh - gh) // 2,,, this) - } - } - - static __new() { - OnMessage(WM_KEYDOWN := 0x100, KeyDown) - KeyDown(wParam, lParam, nmsg, hwnd) { - static VK_UP := 0x26 - static VK_DOWN := 0x28 - if !(wParam = VK_UP || wParam = VK_DOWN) - return - g := GuiFromHwnd(hwnd, true) - if g is NewScriptGui && g.FocusedCtrl is Gui.Edit { - PostMessage nmsg, wParam, lParam, g['LV'] - return true - } - } - } - - Browse(*) { - path := this.FileSelect('S', this['Dir'].Value "\" this['Name'].Value, this.Title, "Script Files (*.ahk)") - if path = "" - return - SplitPath path, &name, &dir - this['Name'].Value := name - this['Dir'].Value := dir - this['LV'].Focus() - } - - ChangeDefaults(btn, *) { - lv := this["LV"] - - m := Menu() - m.Add "Default to Create", setDefBtn, "Radio" - m.Add "Default to Edit", setDefBtn, "Radio" - m.Add "Stay open", toggleStayOpen - if stayOpen := ConfigRead('New', 'StayOpen', false) - m.Check "Stay open" - m.Add - m.Add "Set folder as default", (*) => SetDefaultDir(this['Dir'].Value) - if this['Dir'].Value = GetDefaultDir() - m.Check "Set folder as default" - m.Add "Open templates folder", (*) => OpenTemplatesFolder() - - static DM_GETDEFID := 0x400 - m.Check (1 + (SendMessage(DM_GETDEFID,,, this) & 0xFFFF = DllCall('GetDlgCtrlID', 'ptr', this['Edit'].Hwnd))) "&" - - this.Opt "-DPIScale" - btn.GetPos &x, &y, &w, &h - ;btn.Focus ; This would also apply the "default" style, which would not revert correctly. - ControlFocus btn - m.Show x, y + h - - setDefBtn(itemname, itempos, *) { - itemname := SubStr(itemname, 12) - this[itemname].Opt('Default') - ConfigWrite(itemname, 'New', 'DefaultButton') - } - toggleStayOpen(*) { - ConfigWrite(stayOpen := !stayOpen, 'New', 'StayOpen') - } - } - - Confirm(btn, *) { - lv := this['LV'] - if !index := lv.GetNext() - return MsgBox("You need to select a template first.",, 'icon!') - t := index = 1 ? '' : this.Templates[lv.GetText(index)] - - stayOpen := GetKeyState('Ctrl') || ConfigRead('New', 'StayOpen', false) - - DirCreate dir := this['Dir'].Value - basename := this['Name'].Value - (basename != '') || basename := "Untitled" - SubStr(basename, -4) = ".ahk" && basename := SubStr(basename, 1, -4) - newPath := dir "\" basename ".ahk" - while FileExist(newPath) - newPath := dir "\" basename "-" A_Index ".ahk" - - if t && t.execute - RunWait Format('"{1}" "{2}" {3}', t.path, newPath, btn.Name), dir - else { - code := t = '' ? '' : FileRead(t.path) - code := RegExReplace(code, 'si)/\*\s+\[NewScriptTemplate\].*\*/\R?') - FileOpen(newPath, 'w', 'UTF-8').Write(code) - } - - if this.ExplorerHwnd && (xp := GetExplorerByHwnd(this.ExplorerHwnd)) - || dir = A_Desktop && (xp := GetExplorerForDesktop()) { - SplitPath newPath, &basename - SelectExplorerItem xp, basename - } - else if btn.Name != 'Edit' - Run 'explorer /select,"' newPath '"' - - if btn.Name = 'Edit' - Run 'edit "' newPath '"' - - if !stayOpen - this.Hide - } - - RightClicked(lv, item, isRClick, x, y) { - if !item - return - t := item > 1 ? this.Templates[lv.GetText(item)] : {name: ''} - - m := Menu() - if item > 1 { - m.Add "&Edit template", (*) => EditTemplate(t) - m.Add "&Hide template", hideTemplate - } - m.Add "Set as &default", (*) => SetDefaultTemplate(t.name) - if t.name = GetDefaultTemplate() - m.Check "Set as &default" - m.Show x, y - - hideTemplate(*) { - ConfigWrite(true, 'New\HideTemplate', t.name) - lv.Delete item - static LVM_ARRANGE := 0x1016 - SendMessage LVM_ARRANGE,,, lv ; Fill in any gap left by item (in Tile view). - } - } - - DoubleClicked(lv, row) { - if row - this.Confirm(this.GetDefaultButton()) - } - - GetDefaultButton() { - static DM_GETDEFID := 0x400 - hwnd := DllCall("GetDlgItem", "ptr", this.hwnd, "int", SendMessage(DM_GETDEFID,,, this) & 0xFFFF, "uint") - return hwnd && this[hwnd] - } -} - -class NewScriptTemplate { - __new(path, name:="", description:="") { - this.path := path - if name = "" - SplitPath path,,,, &name - this.name := name - this.desc := IniRead(path, 'NewScriptTemplate', 'Description', description) - this.execute := false - switch IniRead(path, 'NewScriptTemplate', 'Execute', false) { - case true, "true": this.execute := true - } - } -} - -OpenTemplatesFolder() { - dir := GetUserTemplateFolder(true) - if dir != "" - Run dir "\" -} - -GetUserTemplateFolder(checkAndPrompt:=false) { - static dir := A_MyDocuments "\AutoHotkey\Templates" - if checkAndPrompt && !FileExist(dir) { - if MsgBox("User-created templates should be placed in the following folder, " - "which does not yet exist:`n`n" dir "`n`nCreate it now?",, "YesNo") = "No" - return - DirCreate dir - } - return dir -} - -EditTemplate(t) { - dir := GetUserTemplateFolder(true) - if dir = "" - return - userPath := dir "\" t.name ".ahk" - if !FileExist(userPath) { - FileCopy t.path, userPath - t.path := userPath - } - Run 'edit "' userPath '"' -} - -GetScriptTemplates() { - tmap := Map(), tmap.CaseSense := "off" - sources := [ - A_ScriptDir "\Templates", - GetUserTemplateFolder() - ] - if FileExist(t := A_WinDir '\ShellNew\Template.ahk') - tmap["Legacy"] := NewScriptTemplate(t, "Legacy", "From " A_WinDir "\ShellNew") - for source in sources { - loop files source "\*.ahk" { - t := NewScriptTemplate(A_LoopFilePath) - tmap[t.name] := t - } - } - return tmap -} - -SetDefaultTemplate(t) => ConfigWrite(t = "Empty" ? "" : t, 'New', 'DefaultTemplate') -GetDefaultTemplate() => ConfigRead('New', 'DefaultTemplate', "") - -SetDefaultDir(dir) => ConfigWrite(dir, 'New', 'DefaultDir') -GetDefaultDir() => ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey") - -SelectExplorerItem(e, name) { - sfv := e.Document - if fi := sfv.Folder.ParseName(name) - sfv.SelectItem(fi, 1|4|8|16) - return e -} - -GetExplorerByHwnd(hwnd) { - for window in ComObject("Shell.Application").Windows - if window.hwnd = hwnd - return window -} - -GetExplorerForDesktop() { - hwndBuf := Buffer(4, 0), hwndRef := ComValue(0x4003, hwndBuf.Ptr) - return ComObject("Shell.Application").Windows.FindWindowSW(0, "", 8, hwndRef, 1) -} - -GetPathForExplorerWindow(hwnd) { - try return GetExplorerByHwnd(hwnd).Document.Folder.Self.Path -} - -GetExplorerByPath(path) { - for window in ComObject("Shell.Application").Windows - try if window.Document.Folder.Self.Path = path - return window - return "" -} - -if A_ScriptFullPath = A_LineFile - NewScriptGui.Show(A_Args.Length ? A_Args[1] : "") +#requires AutoHotkey v2.0 + +#NoTrayIcon +#SingleInstance Off + +#include inc\common.ahk +#include inc\ui-base.ahk + +class NewScriptGui extends AutoHotkeyUxGui { + __new(path:="") { + super.__new("New Script") + + SplitPath path,, &dir,, &name + if this.ExplorerHwnd := WinActive("ahk_class CabinetWClass") { + this.Opt '+Owner' this.ExplorerHwnd + if dir = "" + dir := GetPathForExplorerWindow(this.ExplorerHwnd) + } + if dir = "" + dir := ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey") + + name := this.AddEdit('vName w272', name != "New AutoHotkey Script" ? name : "") + static EM_SETCUEBANNER := 0x1501 + SendMessage(EM_SETCUEBANNER, true, StrPtr("Untitled"), name) + + static IconSize := SysGet(49) ; SM_CXSMICON + + static BrowseIcon := LoadPicture("imageres.dll", 'Icon-1025 w' IconSize, &imgtype) + this.AddIconButton('vBrowse x+0 yp-1 w28 hp+2', BrowseIcon, "&Browse") + .OnEvent('Click', 'Browse') + + this.AddEdit('vDir xm w300 r1 ReadOnly -TabStop', dir) + + static LVS_SHOWSELALWAYS := 8 ; Seems to have the opposite effect with Explorer theme, at least on Windows 11. + lv := this.AddListMenu("vLV xm w300 -" LVS_SHOWSELALWAYS, ["Name", "Desc", "Path", "Exec"]) + lv.OnEvent('DoubleClick', 'DoubleClicked') + lv.OnEvent('ContextMenu', 'RightClicked') + + deft := GetDefaultTemplate() + lv.Add(deft = "" ? 'Select Focus' : '', "Empty", "Clean slate") + for ,t in this.Templates := GetScriptTemplates() { + if ConfigRead('New\HideTemplate', t.name, false) + continue + lv.Add(deft = t.name ? 'Select Focus' : '', t.name, t.desc) + } + + lv.AutoSize(8) + lv.GetPos(&x, &y, &w, &h) + + static DefaultsIcon := LoadPicture("imageres.dll", 'Icon-114 w' IconSize, &imgtype) + this.AddIconButton('vDefaults r1 w28 xm y' y + h + this.MarginY, DefaultsIcon, "&Defaults") + .OnEvent('Click', 'ChangeDefaults') + + this.AddButton('vCreate yp w75 x150', "&Create").OnEvent('Click', 'Confirm') + this.AddButton('vEdit Default yp wp xm+225', "&Edit").OnEvent('Click', 'Confirm') + if ConfigRead('New', 'DefaultButton', 'Edit') = 'Create' + this['Create'].Opt('Default') + + if this.ExplorerHwnd { + this.Show('AutoSize Hide') + WinGetPos(,, &gw, &gh, this) + WinGetPos(&x, &y, &ew, &eh) + WinMove(x + (ew - gw) // 2, y + (eh - gh) // 2,,, this) + } + } + + static __new() { + OnMessage(WM_KEYDOWN := 0x100, KeyDown) + KeyDown(wParam, lParam, nmsg, hwnd) { + static VK_UP := 0x26 + static VK_DOWN := 0x28 + if !(wParam = VK_UP || wParam = VK_DOWN) + return + gc := GuiCtrlFromHwnd(hwnd) + if gc.Gui is NewScriptGui && gc is Gui.Edit { + PostMessage nmsg, wParam, lParam, gc.Gui['LV'] + return true + } + } + } + + Browse(*) { + path := this.FileSelect('S', this['Dir'].Value "\" this['Name'].Value, this.Title, "Script Files (*.ahk)") + if path = "" + return + SplitPath path, &name, &dir + this['Name'].Value := name + this['Dir'].Value := dir + this['LV'].Focus() + } + + ChangeDefaults(btn, *) { + lv := this["LV"] + + m := Menu() + m.Add "Default to Create", setDefBtn, "Radio" + m.Add "Default to Edit", setDefBtn, "Radio" + m.Add "Stay open", toggleStayOpen + if stayOpen := ConfigRead('New', 'StayOpen', false) + m.Check "Stay open" + m.Add + m.Add "Set folder as default", (*) => SetDefaultDir(this['Dir'].Value) + if this['Dir'].Value = GetDefaultDir() + m.Check "Set folder as default" + m.Add "Open templates folder", (*) => OpenTemplatesFolder() + + static DM_GETDEFID := 0x400 + m.Check (1 + (SendMessage(DM_GETDEFID,,, this) & 0xFFFF = DllCall('GetDlgCtrlID', 'ptr', this['Edit'].Hwnd))) "&" + + this.Opt "-DPIScale" + btn.GetPos &x, &y, &w, &h + ;btn.Focus ; This would also apply the "default" style, which would not revert correctly. + ControlFocus btn + m.Show x, y + h + + setDefBtn(itemname, itempos, *) { + itemname := SubStr(itemname, 12) + this[itemname].Opt('Default') + ConfigWrite(itemname, 'New', 'DefaultButton') + } + toggleStayOpen(*) { + ConfigWrite(stayOpen := !stayOpen, 'New', 'StayOpen') + } + } + + Confirm(btn, *) { + lv := this['LV'] + if !index := lv.GetNext() + return MsgBox("You need to select a template first.",, 'icon!') + t := index = 1 ? '' : this.Templates[lv.GetText(index)] + + stayOpen := GetKeyState('Ctrl') || ConfigRead('New', 'StayOpen', false) + + DirCreate dir := this['Dir'].Value + basename := this['Name'].Value + (basename != '') || basename := "Untitled" + SubStr(basename, -4) = ".ahk" && basename := SubStr(basename, 1, -4) + newPath := dir "\" basename ".ahk" + while FileExist(newPath) + newPath := dir "\" basename "-" A_Index ".ahk" + + if t && t.execute + RunWait Format('"{1}" "{2}" {3}', t.path, newPath, btn.Name), dir + else { + code := t = '' ? '' : FileRead(t.path) + code := RegExReplace(code, 'si)/\*\s+\[NewScriptTemplate\].*\*/\R?') + FileOpen(newPath, 'w', 'UTF-8').Write(code) + } + + if this.ExplorerHwnd && (xp := GetExplorerByHwnd(this.ExplorerHwnd)) + || dir = A_Desktop && (xp := GetExplorerForDesktop()) { + SplitPath newPath, &basename + SelectExplorerItem xp, basename + } + else if btn.Name != 'Edit' + Run 'explorer /select,"' newPath '"' + + if btn.Name = 'Edit' + Run 'edit "' newPath '"' + + if !stayOpen + this.Hide + } + + RightClicked(lv, item, isRClick, x, y) { + if !item + return + t := item > 1 ? this.Templates[lv.GetText(item)] : {name: ''} + + m := Menu() + if item > 1 { + m.Add "&Edit template", (*) => EditTemplate(t) + m.Add "&Hide template", hideTemplate + } + m.Add "Set as &default", (*) => SetDefaultTemplate(t.name) + if t.name = GetDefaultTemplate() + m.Check "Set as &default" + m.Show x, y + + hideTemplate(*) { + ConfigWrite(true, 'New\HideTemplate', t.name) + lv.Delete item + static LVM_ARRANGE := 0x1016 + SendMessage LVM_ARRANGE,,, lv ; Fill in any gap left by item (in Tile view). + } + } + + DoubleClicked(lv, row) { + if row + this.Confirm(this.GetDefaultButton()) + } + + GetDefaultButton() { + static DM_GETDEFID := 0x400 + hwnd := DllCall("GetDlgItem", "ptr", this.hwnd, "int", SendMessage(DM_GETDEFID,,, this) & 0xFFFF, "uint") + return hwnd && this[hwnd] + } +} + +class NewScriptTemplate { + __new(path, name:="", description:="") { + this.path := path + if name = "" + SplitPath path,,,, &name + this.name := name + this.desc := IniRead(path, 'NewScriptTemplate', 'Description', description) + this.execute := false + switch IniRead(path, 'NewScriptTemplate', 'Execute', false) { + case true, "true": this.execute := true + } + } +} + +OpenTemplatesFolder() { + dir := GetUserTemplateFolder(true) + if dir != "" + Run dir "\" +} + +GetUserTemplateFolder(checkAndPrompt:=false) { + static dir := A_MyDocuments "\AutoHotkey\Templates" + if checkAndPrompt && !FileExist(dir) { + if MsgBox("User-created templates should be placed in the following folder, " + "which does not yet exist:`n`n" dir "`n`nCreate it now?",, "YesNo") = "No" + return + DirCreate dir + } + return dir +} + +EditTemplate(t) { + dir := GetUserTemplateFolder(true) + if dir = "" + return + userPath := dir "\" t.name ".ahk" + if !FileExist(userPath) { + FileCopy t.path, userPath + t.path := userPath + } + Run 'edit "' userPath '"' +} + +GetScriptTemplates() { + tmap := Map(), tmap.CaseSense := "off" + sources := [ + A_ScriptDir "\Templates", + GetUserTemplateFolder() + ] + if FileExist(t := A_WinDir '\ShellNew\Template.ahk') + tmap["Legacy"] := NewScriptTemplate(t, "Legacy", "From " A_WinDir "\ShellNew") + for source in sources { + loop files source "\*.ahk" { + t := NewScriptTemplate(A_LoopFilePath) + tmap[t.name] := t + } + } + return tmap +} + +SetDefaultTemplate(t) => ConfigWrite(t = "Empty" ? "" : t, 'New', 'DefaultTemplate') +GetDefaultTemplate() => ConfigRead('New', 'DefaultTemplate', "") + +SetDefaultDir(dir) => ConfigWrite(dir, 'New', 'DefaultDir') +GetDefaultDir() => ConfigRead('New', 'DefaultDir', A_MyDocuments "\AutoHotkey") + +SelectExplorerItem(e, name) { + sfv := e.Document + if fi := sfv.Folder.ParseName(name) + sfv.SelectItem(fi, 1|4|8|16) + return e +} + +GetExplorerByHwnd(hwnd) { + for window in ComObject("Shell.Application").Windows + if window.hwnd = hwnd + return window +} + +GetExplorerForDesktop() { + hwndBuf := Buffer(4, 0), hwndRef := ComValue(0x4003, hwndBuf.Ptr) + return ComObject("Shell.Application").Windows.FindWindowSW(0, "", 8, hwndRef, 1) +} + +GetPathForExplorerWindow(hwnd) { + try return GetExplorerByHwnd(hwnd).Document.Folder.Self.Path +} + +GetExplorerByPath(path) { + for window in ComObject("Shell.Application").Windows + try if window.Document.Folder.Self.Path = path + return window + return "" +} + +if A_ScriptFullPath = A_LineFile + NewScriptGui.Show(A_Args.Length ? A_Args[1] : "") diff --git a/UX/ui-setup.ahk b/UX/ui-setup.ahk index 396f914..65673ad 100644 --- a/UX/ui-setup.ahk +++ b/UX/ui-setup.ahk @@ -1,171 +1,175 @@ -; This script shows the initial setup GUI. -; It is not intended for use after installation. -#requires AutoHotkey v2.0 - -#NoTrayIcon -#SingleInstance Force - -#include inc\ui-base.ahk - -A_ScriptName := "AutoHotkey Setup" -SetRegView 64 -InstallGui.Show() - -class InstallGui extends AutoHotkeyUxGui { - __new() { - super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox') - - DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT - , 'int64*', 3 | (3<<32), 'int', 8) ; WTNCA_NODRAWCAPTION=1, WTNCA_NODRAWICON=2 - - static TitleBack := 'BackgroundWhite' - static TitleFore := 'c3F627F' - static TotalWidth := 350 - this.AddText('x0 y0 w' TotalWidth ' h84 ' TitleBack) - this.AddPicture('x32 y16 w32 h32 ' TitleBack, A_AhkPath) - this.SetFont('s12', 'Segoe UI') - this.AddText('x+20 yp+4 ' TitleFore ' ' TitleBack, "AutoHotkey v" A_AhkVersion) - this.SetFont('s9') - - ; SS_SUNKEN := 0x1000 ; 4096 - this.AddText('x-4 y84 w' TotalWidth+4 ' h188 0x1000 -Background') - - this.AddText('xm yp+16', 'Install &to:') - dirEdit := this.AddEdit('vInstallDir w' TotalWidth - (2 * this.MarginX) - 88) - this.AddButton('vBrowseButton w80 x+8 yp-1', '&Browse') - .OnEvent('Click', 'Browse') - - rect := Buffer(16, 0) - DllCall('GetClientRect', 'ptr', dirEdit.hwnd, 'ptr', rect) - NumPut('int', 4, 'int', 2, rect) - ; DllCall('InflateRect', 'ptr', rect, 'int', 0, 'int', ) - EM_SETRECT := 0xB3 ; 179 - SendMessage 0xB3, 0, rect.ptr, dirEdit - - this.AddGroupBox('xm y+0 w' TotalWidth - (2 * this.MarginX) ' h44') - this.AddText('xp+8 yp+16', "Install mode:") - this.AddRadio('vModeAll x+m yp Checked', "&All users") - .OnEvent('Click', 'ModeChange') - this.AddRadio('vModeUser x+4', "&Current user") - .OnEvent('Click', 'ModeChange') - this.AddRadio('vModePortable x+4 Disabled', "Portable") - .OnEvent('Click', 'ModeChange') - - this.AddButton('vInstallButton x' (TotalWidth - 80) // 2 ' w80 y+36 Default', "&Install") - .OnEvent('Click', 'Install') - - this.ModeChange() - - this.MarginY := -1 - this.Show('Hide w' TotalWidth) - this['InstallButton'].Focus() - } - - Browse(*) { - if ControlGetStyle(this['InstallDir']) & 0x800 { ; ES_READONLY - rootKey := this['ModeAll'].Value ? 'HKLM' : 'HKCU' - message := "Changing the installation directory is not recommended." - if InStr(RegRead(rootKey '\Software\AutoHotkey', 'Version', ''), '1.') = 1 - message .= "`n`nThis installation package is designed to allow both v1 and v2 to be associated with .ahk files, but only if they are installed in the same directory." - message .= "`n`nExisting files will not be moved." - if MsgBox(message,, "OKCancel Icon!") != "OK" - return - this['InstallDir'].Opt('-ReadOnly') - } - dir := this.FileSelect('D', this['InstallDir'].Value '\', "Select installation directory") - if dir != '' { - this['InstallDir'].Value := dir - this.InstallDirChange() - } - } - - ModeChange(p*) { - if !this.CheckAlreadyInstalled(p.Length = 0, true) { - ; Ensure InstallDir makes sense for the new mode - static DefaultAllDir := (EnvGet('ProgramW6432') || A_ProgramFiles) '\AutoHotkey' - static DefaultUserDir := EnvGet('LocalAppData') '\Programs\AutoHotkey' - installDir := this['InstallDir'].Value - if this['ModeAll'].Value { - if installDir = '' || installDir = DefaultUserDir - this['InstallDir'].Value := DefaultAllDir, this['InstallDir'].Opt('-ReadOnly') - } else - if installDir = '' || IsInProgramFiles(installDir) - this['InstallDir'].Value := DefaultUserDir, this['InstallDir'].Opt('-ReadOnly') - } - this.UpdateShield() - } - - InstallDirChange(*) { - this.UpdateShield() - } - - UpdateShield() { - requireAdmin := this['ModeAll'].Value && !A_IsAdmin - SendMessage 0x160C, 0, requireAdmin, this['InstallButton'] ; BCM_SETSHIELD - } - - CheckAlreadyInstalled(setMode:=false, setDir:=false) { - for rootKey in setMode ? ['HKCU', 'HKLM'] : [this['ModeAll'].Value ? 'HKLM' : 'HKCU'] { - dir := RegRead(rootKey '\Software\AutoHotkey', 'InstallDir', '') - if dir != '' { - if setDir { - this['InstallDir'].Value := dir - this['InstallDir'].Opt('+ReadOnly') - } - if setMode - this[A_Index = 1 ? 'ModeUser' : 'ModeAll'].Value := true - return dir - } - } - return '' - } - - Install(*) { - problem := '' - requireAdmin := this['ModeAll'].Value - installDir := this['InstallDir'].Value - buf := Buffer(260*2) - n := DllCall('GetFullPathName', 'str', installDir, 'uint', 260, 'ptr', buf, 'ptr', 0) - if !n || n > 259 { - MsgBox "Please enter a valid path.",, 'Icon!' - return - } - fullPath := StrGet(buf) - if installDir != fullPath { - problem .= '"' installDir '" resolves to "' fullPath '".`n`n' - installDir := fullPath - } - dir := this.CheckAlreadyInstalled() - if dir && dir != installDir - problem .= 'The existing installation in "' dir '" will not be moved or integrated with the new installation.`n`n' - if requireAdmin && !IsInProgramFiles(installDir) && dir != installDir - problem .= 'Enabling UI Access will not be possible because the installation directory is not a sub-directory of Program Files. Without UI Access, non-elevated scripts cannot interact with windows of elevated programs.`n`n' - if problem && MsgBox(problem,, 'OKCancel Default2 Icon!') = 'Cancel' - return - if A_IsCompiled && IsSet(Installation) - cmd := Format('"{1}" /to "{2}"', A_ScriptFullPath, installDir) - else - cmd := Format('"{1}" /script "{2}\install.ahk" /to "{3}"', A_AhkPath, A_ScriptDir, installDir) - if !requireAdmin - cmd .= ' /user' - else if !A_IsAdmin - cmd := '*RunAs ' cmd - try - Run cmd,,, &pid - catch as e { - if A_LastError != 1223 ; ERROR_CANCELLED - MsgBox e.Message "`n`n" e.Extra,, 'IconX' - } else { - this['InstallButton'].Enabled := false - this['InstallButton'].Text := "Installing..." - ProcessWaitClose pid - ExitApp - } - } -} - -IsInProgramFiles(path) { - other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") - return InStr(path, A_ProgramFiles "\") = 1 - || other && InStr(path, other "\") = 1 +; This script shows the initial setup GUI. +; It is not intended for use after installation. +#requires AutoHotkey v2.0 + +#NoTrayIcon +#SingleInstance Force + +#include inc\ui-base.ahk + +A_ScriptName := "AutoHotkey Setup" +SetRegView 64 +InstallGui.Show() + +class InstallGui extends AutoHotkeyUxGui { + __new() { + super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox') + + DllCall('uxtheme\SetWindowThemeAttribute', 'ptr', this.hwnd, 'int', 1 ; WTA_NONCLIENT + , 'int64*', 3 | (3<<32), 'int', 8) ; WTNCA_NODRAWCAPTION=1, WTNCA_NODRAWICON=2 + + static TitleBack := 'BackgroundWhite' + static TitleFore := 'c3F627F' + static TotalWidth := 350 + this.AddText('x0 y0 w' TotalWidth ' h84 ' TitleBack) + this.AddPicture('x32 y16 w32 h32 ' TitleBack, A_AhkPath) + this.SetFont('s12', 'Segoe UI') + this.AddText('x+20 yp+4 ' TitleFore ' ' TitleBack, "AutoHotkey v" A_AhkVersion) + this.SetFont('s9') + + ; SS_SUNKEN := 0x1000 ; 4096 + this.AddText('x-4 y84 w' TotalWidth+4 ' h188 0x1000 -Background') + + this.AddText('xm yp+16', 'Install &to:') + dirEdit := this.AddEdit('vInstallDir w' TotalWidth - (2 * this.MarginX) - 88) + this.AddButton('vBrowseButton w80 x+8 yp-1', '&Browse') + .OnEvent('Click', 'Browse') + + rect := Buffer(16, 0) + DllCall('GetClientRect', 'ptr', dirEdit.hwnd, 'ptr', rect) + NumPut('int', 4, 'int', 2, rect) + ; DllCall('InflateRect', 'ptr', rect, 'int', 0, 'int', ) + EM_SETRECT := 0xB3 ; 179 + SendMessage 0xB3, 0, rect.ptr, dirEdit + + this.AddGroupBox('xm y+0 w' TotalWidth - (2 * this.MarginX) ' h44') + this.AddText('xp+8 yp+16', "Install mode:") + this.AddRadio('vModeAll x+m yp Checked', "&All users") + .OnEvent('Click', 'ModeChange') + this.AddRadio('vModeUser x+4', "&Current user") + .OnEvent('Click', 'ModeChange') + this.AddRadio('vModePortable x+4 Disabled', "Portable") + .OnEvent('Click', 'ModeChange') + + this.AddButton('vInstallButton x' (TotalWidth - 80) // 2 ' w80 y+36 Default', "&Install") + .OnEvent('Click', 'Install') + + this.ModeChange() + + this.MarginY := -1 + this.Show('Hide w' TotalWidth) + this['InstallButton'].Focus() + } + + Browse(*) { + if ControlGetStyle(this['InstallDir']) & 0x800 { ; ES_READONLY + rootKey := this['ModeAll'].Value ? 'HKLM' : 'HKCU' + message := "Changing the installation directory is not recommended." + if InStr(RegRead(rootKey '\Software\AutoHotkey', 'Version', ''), '1.') = 1 + message .= "`n`nThis installation package is designed to allow both v1 and v2 to be associated with .ahk files, but only if they are installed in the same directory." + message .= "`n`nExisting files will not be moved." + if MsgBox(message,, "OKCancel Icon!") != "OK" + return + this['InstallDir'].Opt('-ReadOnly') + } + dir := this.FileSelect('D', this['InstallDir'].Value '\', "Select installation directory") + if dir != '' { + this['InstallDir'].Value := dir + this.InstallDirChange() + } + } + + ModeChange(p*) { + if !this.CheckAlreadyInstalled(p.Length = 0, true) { + ; Ensure InstallDir makes sense for the new mode + installDir := this['InstallDir'].Value + if this['ModeAll'].Value { + if installDir = '' || installDir = InstallUtil.DefaultUserDir + this['InstallDir'].Value := InstallUtil.DefaultAllDir, this['InstallDir'].Opt('-ReadOnly') + } else + if installDir = '' || IsInProgramFiles(installDir) + this['InstallDir'].Value := InstallUtil.DefaultUserDir, this['InstallDir'].Opt('-ReadOnly') + } + this.UpdateShield() + } + + InstallDirChange(*) { + this.UpdateShield() + } + + UpdateShield() { + requireAdmin := this['ModeAll'].Value && !A_IsAdmin + SendMessage 0x160C, 0, requireAdmin, this['InstallButton'] ; BCM_SETSHIELD + } + + CheckAlreadyInstalled(setMode:=false, setDir:=false) { + for rootKey in setMode ? ['HKCU', 'HKLM'] : [this['ModeAll'].Value ? 'HKLM' : 'HKCU'] { + dir := RegRead(rootKey '\Software\AutoHotkey', 'InstallDir', '') + if dir != '' { + if setDir { + this['InstallDir'].Value := dir + this['InstallDir'].Opt('+ReadOnly') + } + if setMode + this[A_Index = 1 ? 'ModeUser' : 'ModeAll'].Value := true + return dir + } + } + return '' + } + + Install(*) { + problem := '' + requireAdmin := this['ModeAll'].Value + installDir := this['InstallDir'].Value + buf := Buffer(260*2) + n := DllCall('GetFullPathName', 'str', installDir, 'uint', 260, 'ptr', buf, 'ptr', 0) + if !n || n > 259 { + MsgBox "Please enter a valid path.",, 'Icon!' + return + } + fullPath := StrGet(buf) + if installDir != fullPath { + problem .= '"' installDir '" resolves to "' fullPath '".`n`n' + installDir := fullPath + } + dir := this.CheckAlreadyInstalled() + if dir && dir != installDir + problem .= 'The existing installation in "' dir '" will not be moved or integrated with the new installation.`n`n' + if requireAdmin && !IsInProgramFiles(installDir) && dir != installDir + problem .= 'Enabling UI Access will not be possible because the installation directory is not a sub-directory of Program Files. Without UI Access, non-elevated scripts cannot interact with windows of elevated programs.`n`n' + if problem && MsgBox(problem,, 'OKCancel Default2 Icon!') = 'Cancel' + return + if A_IsCompiled && IsSet(Installation) + cmd := Format('"{1}" /to "{2}"', A_ScriptFullPath, installDir) + else + cmd := Format('"{1}" /script "{2}\install.ahk" /to "{3}"', A_AhkPath, A_ScriptDir, installDir) + if !requireAdmin + cmd .= ' /user' + else if !A_IsAdmin + cmd := '*RunAs ' cmd + try + Run cmd,,, &pid + catch as e { + if A_LastError != 1223 ; ERROR_CANCELLED + MsgBox e.Message "`n`n" e.Extra,, 'IconX' + } else { + this['InstallButton'].Enabled := false + this['InstallButton'].Text := "Installing..." + ProcessWaitClose pid + ExitApp + } + } +} + +class InstallUtil { + static DefaultAllDir := (EnvGet('ProgramW6432') || A_ProgramFiles) '\AutoHotkey' + static DefaultUserDir := EnvGet('LocalAppData') '\Programs\AutoHotkey' + static DefaultDir := A_IsAdmin ? this.DefaultAllDir : this.DefaultUserDir +} + +IsInProgramFiles(path) { + other := EnvGet(A_PtrSize=8 ? "ProgramFiles(x86)" : "ProgramW6432") + return InStr(path, A_ProgramFiles "\") = 1 + || other && InStr(path, other "\") = 1 } \ No newline at end of file diff --git a/UX/ui-uninstall.ahk b/UX/ui-uninstall.ahk index c24618f..eab3cf5 100644 --- a/UX/ui-uninstall.ahk +++ b/UX/ui-uninstall.ahk @@ -1,68 +1,68 @@ -; This script shows a GUI for uninstalling AutoHotkey or specific versions. -#include inc\bounce-v1.ahk -/* v1 stops here */ -#requires AutoHotkey v2.0 - -#include inc\ui-base.ahk -#include install.ahk - -#NoTrayIcon -#SingleInstance Force - -A_ScriptName := "AutoHotkey Setup" -SetRegView 64 -ModifySetupGui.Show() - -class ModifySetupGui extends AutoHotkeyUxGui { - __new() { - super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox') - - this.inst := Installation() - this.inst.ResolveInstallDir() - versions := this.inst.GetComponents() - - this.AddText(, "Remove which versions?") - iv := this.AddListView('vComponents Checked -Hdr R10 w248', ["Version"]) - iv.OnEvent('ItemCheck', 'Checked') - for v, files in versions - iv.Add(files.HasProp('superseded') ? 'Check' : '', v) - - anyChecked := iv.GetNext(0, 'C') - this.AddButton('vRemoveAll w120 ' (anyChecked ? '' : 'Default'), "Remove &all") - .OnEvent('Click', 'ClickedRemove') - this.AddButton('vRemove w120 yp ' (anyChecked ? 'Default' : 'Disabled'), "Remove &checked") - .OnEvent('Click', 'ClickedRemove') - - if !this.inst.UserInstall && !A_IsAdmin { - SendMessage 0x160C,, true, 'Button1', this ; BCM_SETSHIELD := 0x160C - SendMessage 0x160C,, true, 'Button2', this - } - } - - ClickedRemove(btn, *) { - cmd := '' - if btn.Name = 'Remove' { - n := 0, iv := this['Components'], count := 0 - while n := iv.GetNext(n, 'C') - cmd .= ',' iv.GetText(n), ++count - if count = iv.GetCount() - cmd := '' ; All are selected - do a full uninstall - else - cmd := ' "' SubStr(cmd, 2) '"' - } - cmd := Format('"{1}" "{2}\install.ahk" /uninstall{3}', A_AhkPath, A_ScriptDir, cmd) - if (!this.inst.UserInstall || A_Args.Length && A_Args[1] = '/elevate') && !A_IsAdmin - cmd := '*RunAs ' cmd - try { - Run cmd - ExitApp - } - catch as e - if !InStr(e.Message e.Extra, 'cancel') - throw e - } - - Checked(iv, *) { - this['Remove'].Enabled := iv.GetNext(0, 'C') - } +; This script shows a GUI for uninstalling AutoHotkey or specific versions. +#include inc\bounce-v1.ahk +/* v1 stops here */ +#requires AutoHotkey v2.0 + +#include inc\ui-base.ahk +#include install.ahk + +#NoTrayIcon +#SingleInstance Force + +A_ScriptName := "AutoHotkey Setup" +SetRegView 64 +ModifySetupGui.Show() + +class ModifySetupGui extends AutoHotkeyUxGui { + __new() { + super.__new(A_ScriptName, '-MinimizeBox -MaximizeBox') + + this.inst := Installation() + this.inst.ResolveInstallDir() + versions := this.inst.GetComponents() + + this.AddText(, "Remove which versions?") + iv := this.AddListView('vComponents Checked -Hdr R10 w248', ["Version"]) + iv.OnEvent('ItemCheck', 'Checked') + for v, files in versions + iv.Add(files.HasProp('superseded') ? 'Check' : '', v) + + anyChecked := iv.GetNext(0, 'C') + this.AddButton('vRemoveAll w120 ' (anyChecked ? '' : 'Default'), "Remove &all") + .OnEvent('Click', 'ClickedRemove') + this.AddButton('vRemove w120 yp ' (anyChecked ? 'Default' : 'Disabled'), "Remove &checked") + .OnEvent('Click', 'ClickedRemove') + + if !this.inst.UserInstall && !A_IsAdmin { + SendMessage 0x160C,, true, 'Button1', this ; BCM_SETSHIELD := 0x160C + SendMessage 0x160C,, true, 'Button2', this + } + } + + ClickedRemove(btn, *) { + cmd := '' + if btn.Name = 'Remove' { + n := 0, iv := this['Components'], count := 0 + while n := iv.GetNext(n, 'C') + cmd .= ',' iv.GetText(n), ++count + if count = iv.GetCount() + cmd := '' ; All are selected - do a full uninstall + else + cmd := ' "' SubStr(cmd, 2) '"' + } + cmd := Format('"{1}" "{2}\install.ahk" /uninstall{3}', A_AhkPath, A_ScriptDir, cmd) + if (!this.inst.UserInstall || A_Args.Length && A_Args[1] = '/elevate') && !A_IsAdmin + cmd := '*RunAs ' cmd + try { + Run cmd + ExitApp + } + catch as e + if !InStr(e.Message e.Extra, 'cancel') + throw e + } + + Checked(iv, *) { + this['Remove'].Enabled := iv.GetNext(0, 'C') + } } \ No newline at end of file diff --git a/WindowSpy.ahk b/WindowSpy.ahk index 2a9a294..e1050f8 100644 --- a/WindowSpy.ahk +++ b/WindowSpy.ahk @@ -1,2 +1,2 @@ -#Requires AutoHotkey v2.0-beta +#Requires AutoHotkey v2.0-beta #Include UX\WindowSpy.ahk \ No newline at end of file