; Look Slots hotkeys for Adobe Lightroom Classic (Windows). ; ; Lightroom Classic has no way to assign a keyboard shortcut to a plugin's ; menu item, so this script does it: you press a key, it presses the menu ; item for you. It uses AutoHotkey v2 - a free, open-source Windows ; automation tool (autohotkey.com). AutoHotkey v1 will NOT run this file. ; ; Ctrl+Alt+1..9 Copy Develop Settings to Slot 1..9 ; Ctrl+Alt+Shift+1..9 Paste Develop Settings from Slot 1..9 ; ; The hotkeys are live ONLY while Lightroom is the focused window. In every ; other app your keys behave exactly as before. ; ; AI-mask assist: when the plugin pastes AI masks (Select Subject/Sky/People) ; it writes a marker file under %USERPROFILE%\.develop-clipboard\. This script ; watches for it and presses Settings > Update AI Settings, so the masks ; re-segment on the photos you pasted onto - the Lightroom SDK has no API for ; that command. This is the same behaviour the macOS (Hammerspoon) helper has. ; ; --------------------------------------------------------------------------- ; About the indent in front of plugin menu items ; --------------------------------------------------------------------------- ; Lightroom indents every item under File > Plug-in Extras by baking literal ; leading spaces into the item's real name (three spaces, in every build we ; know of). AutoHotkey's MenuSelect matches an item name from the START of the ; name, so that indent has to be part of the string we ask for. ; ; Rather than betting the script on "three", we try a list of candidate indents ; until one works, remember the winner for the rest of the session, and - if ; none of them work - read Lightroom's real menu through the Windows API and ; use the item's literal name, whatever its indent turns out to be. If even ; that fails you get a message saying exactly which step failed, plus a log ; file you can send to support@lightroom-tools.com. ; ; Lightroom's OWN menus (Settings > Update AI Settings, Photo > Develop ; Settings > Update AI Settings) are never indented, so the indent logic is ; deliberately not applied to them. #Requires AutoHotkey v2.0 #SingleInstance Force LR := "ahk_exe Lightroom.exe" SUPPORT := "support@lightroom-tools.com" ; Indent candidates, most likely first. Three spaces is what Lightroom Classic ; has always used; the rest are insurance. PREFIXES := [" ", "", " ", " ", " ", "`t"] MenuPrefix := "" ; the indent that actually worked MenuPrefixKnown := false ; ...once we have proven it LOG_DIR := EnvGet("USERPROFILE") . "\.develop-clipboard" LOG_FILE := LOG_DIR . "\look-slots-ahk.log" MF_BYPOSITION := 0x400 lastFailureShown := 0 ; --------------------------------------------------------------------------- ; Menu pressing ; --------------------------------------------------------------------------- ; Presses a menu path on the active window. Returns true on success. ; MenuSelect throws if any part of the path doesn't exist (e.g. the Settings ; menu only exists in the Develop module), so every call is guarded. A call ; that throws presses nothing - it cannot fire the wrong menu item. TryMenu(parts*) { try { MenuSelect("A", "", parts*) ; "A" = active window, "" = any window text return true } catch { return false } } ; Presses File > Plug-in Extras > , working out the indent as needed. PressPluginItem(itemName) { global MenuPrefix, MenuPrefixKnown, PREFIXES ; Fast path: the indent we already proved this session. if (MenuPrefixKnown && TryMenu("File", "Plug-in Extras", MenuPrefix . itemName)) return true ; Discovery: try each candidate indent. The first one that lands, wins. for prefix in PREFIXES { if TryMenu("File", "Plug-in Extras", prefix . itemName) { MenuPrefix := prefix MenuPrefixKnown := true return true } } ; Last resort: read Lightroom's real menu and use the item's literal name. literal := FindPluginItemLiteral(itemName) if (literal != "" && TryMenu("File", "Plug-in Extras", literal)) { MenuPrefix := SubStr(literal, 1, StrLen(literal) - StrLen(LTrim(literal, " `t"))) MenuPrefixKnown := true return true } MenuPrefixKnown := false return false } CopyToSlot(n) { item := "Copy Develop Settings to Slot " . n if !PressPluginItem(item) ReportFailure("Copy to Slot " . n, item) } PasteFromSlot(n) { item := "Paste Develop Settings from Slot " . n if !PressPluginItem(item) ReportFailure("Paste from Slot " . n, item) } ; --------------------------------------------------------------------------- ; Hotkeys (18 of them), live only while Lightroom is the focused window. ; ; The handlers read the slot number off the hotkey name rather than capturing a ; loop variable: a function created at the top level of a script does NOT close ; over top-level variables (closures capture an enclosing FUNCTION's locals ; only), so `(*) => CopyToSlot(n)` inside a loop would leave all nine hotkeys ; pointing at the last value of n. Reading the hotkey name sidesteps that. ; --------------------------------------------------------------------------- CopyHotkey(thisHotkey) { CopyToSlot(SubStr(thisHotkey, -1)) ; last character = the digit } PasteHotkey(thisHotkey) { PasteFromSlot(SubStr(thisHotkey, -1)) } HotIfWinActive(LR) Loop 9 { Hotkey("^!" . A_Index, CopyHotkey) ; Ctrl+Alt+N Hotkey("^!+" . A_Index, PasteHotkey) ; Ctrl+Alt+Shift+N } HotIfWinActive() ; --------------------------------------------------------------------------- ; AI-mask update assist ; --------------------------------------------------------------------------- MARKER := LOG_DIR . "\ai-update-request" WatchAiMarker(*) { global MARKER, LR if !FileExist(MARKER) return if !WinActive(LR) ; wait until Lightroom is focused again return try FileDelete(MARKER) ; delete first, so a slow menu press can't double-fire Sleep 700 ; let Lightroom finish committing the paste ; Lightroom's own menus - no plugin indent. The Develop module has ; Settings > Update AI Settings; Library nests it under Photo. if (TryMenu("Settings", "Update AI Settings") || TryMenu("Photo", "Develop Settings", "Update AI Settings")) return TrayTip("Couldn't reach Update AI Settings. If you just pasted AI masks, " . "run Settings > Update AI Settings once by hand.", "Look Slots", "Icon!") } SetTimer(WatchAiMarker, 400) ; --------------------------------------------------------------------------- ; Diagnostics ; ; Everything below runs only when something went wrong, or when the user picks ; a tray-menu item. It reads Lightroom's real Windows menu, so a failure report ; can say WHICH step failed instead of just "it didn't work". ; --------------------------------------------------------------------------- ; Lightroom's main window is the one that owns a menu bar (its dialogs do not). FindLrMainWindow() { global LR try { for hwnd in WinGetList(LR) { if DllCall("GetMenu", "Ptr", hwnd, "Ptr") return hwnd } } catch { return 0 } return 0 } MenuItemTitles(hMenu) { global MF_BYPOSITION titles := [] count := DllCall("GetMenuItemCount", "Ptr", hMenu, "Int") if (count <= 0) return titles buf := Buffer(1024, 0) Loop count { len := DllCall("GetMenuStringW", "Ptr", hMenu, "UInt", A_Index - 1 , "Ptr", buf, "Int", 511, "UInt", MF_BYPOSITION, "Int") titles.Push(len > 0 ? StrGet(buf, "UTF-16") : "") } return titles } ; Compare menu titles ignoring the indent and any & accelerator markers. MenuNorm(s) { return Trim(StrReplace(s, "&", "")) } MenuIndexOf(titles, want) { for i, t in titles { if (MenuNorm(t) = MenuNorm(want)) ; = is a case-insensitive compare return i } return 0 } ; Walks Lightroom's real menu. Returns Map("stage", , ; "items", ). stage is one of: ; no-window / no-menubar / no-file-menu / no-file-submenu / ; no-plugin-extras / no-plugin-extras-submenu / extras InspectPluginMenu() { info := Map("stage", "no-window", "items", []) hwnd := FindLrMainWindow() if !hwnd return info hMenu := DllCall("GetMenu", "Ptr", hwnd, "Ptr") if !hMenu { info["stage"] := "no-menubar" return info } top := MenuItemTitles(hMenu) fileIdx := MenuIndexOf(top, "File") if !fileIdx { info["stage"] := "no-file-menu" info["items"] := top return info } hFile := DllCall("GetSubMenu", "Ptr", hMenu, "Int", fileIdx - 1, "Ptr") if !hFile { info["stage"] := "no-file-submenu" return info } fileItems := MenuItemTitles(hFile) extrasIdx := MenuIndexOf(fileItems, "Plug-in Extras") if !extrasIdx { info["stage"] := "no-plugin-extras" info["items"] := fileItems return info } hExtras := DllCall("GetSubMenu", "Ptr", hFile, "Int", extrasIdx - 1, "Ptr") if !hExtras { info["stage"] := "no-plugin-extras-submenu" return info } info["stage"] := "extras" info["items"] := MenuItemTitles(hExtras) return info } ; The item's literal name (indent and all), or "" if it isn't in the menu. FindPluginItemLiteral(itemName) { try { info := InspectPluginMenu() if (info["stage"] != "extras") return "" for t in info["items"] { if (Trim(t) = Trim(itemName)) return t } } catch { return "" } return "" } JoinTitles(titles, max) { out := "" for i, t in titles { if (i > max) { out .= "`n ... and " . (titles.Length - max) . " more" break } out .= "`n [" . i . "] " . (Trim(t) = "" ? "(separator)" : Trim(t)) } return out } ; A plain-English explanation of where the menu walk stopped. DiagnoseMenu(itemName) { try { info := InspectPluginMenu() } catch as e { return "Could not read Lightroom's menu at all (" . e.Message . ")." } switch info["stage"] { case "no-window": return "STEP 1 FAILED: no Lightroom window with a menu bar was found. " . "Is Lightroom Classic running with its main window open?" case "no-menubar": return "STEP 1 FAILED: the Lightroom window has no standard Windows " . "menu bar." case "no-file-menu": return "STEP 2 FAILED: found the menu bar but no File menu." . "`n`nTop-level menus I can see:" . JoinTitles(info["items"], 12) case "no-file-submenu": return "STEP 2 FAILED: the File menu exists but would not open." case "no-plugin-extras": return "STEP 3 FAILED: found the File menu, but no 'Plug-in Extras' " . "item inside it." . "`n`nItems in the File menu:" . JoinTitles(info["items"], 25) case "no-plugin-extras-submenu": return "STEP 3 FAILED: 'Plug-in Extras' exists but its submenu would " . "not open." case "extras": if (FindPluginItemLiteral(itemName) != "") { return "STEP 4 FAILED: '" . itemName . "' IS in File > Plug-in " . "Extras, but Lightroom would not run it. It may be greyed " . "out - select a photo in the filmstrip and try again." } return "STEP 4 FAILED: File > Plug-in Extras opens fine (" . info["items"].Length . " items), but '" . itemName . "' is not " . "in it. Is Look Slots installed AND ticked in File > Plug-in " . "Manager?" . "`n`nItems in Plug-in Extras:" . JoinTitles(info["items"], 30) } return "Unknown failure (stage: " . info["stage"] . ")." } LogLine(text) { global LOG_DIR, LOG_FILE try { if !DirExist(LOG_DIR) DirCreate(LOG_DIR) FileAppend(FormatTime(, "yyyy-MM-dd HH:mm:ss") . " " . text . "`n`n" , LOG_FILE, "UTF-8") } catch { return } } ReportFailure(label, itemName) { global SUPPORT, LOG_FILE, lastFailureShown detail := DiagnoseMenu(itemName) LogLine(label . " -> " . detail) ; Don't stack message boxes if the key gets mashed. if (A_TickCount - lastFailureShown < 8000) return lastFailureShown := A_TickCount MsgBox("Look Slots could not press the menu item for " . label . "." . "`n`n" . detail . "`n`nPlease email this whole message (Ctrl+C copies it) to " . SUPPORT . "`nA copy is saved at: " . LOG_FILE , "Look Slots - menu item not found", "Icon!") } ShowDiagnostics(*) { global MenuPrefix, MenuPrefixKnown, SUPPORT item := "Copy Develop Settings to Slot 1" literal := FindPluginItemLiteral(item) if (literal != "") { indent := StrLen(literal) - StrLen(LTrim(literal, " `t")) detail := "Menu looks good: found '" . item . "' in File > Plug-in " . "Extras, indented by " . indent . " character(s)." } else { detail := DiagnoseMenu(item) } used := MenuPrefixKnown ? "Indent used by the hotkeys so far: " . StrLen(MenuPrefix) . " character(s)." : "The hotkeys have not pressed a menu item yet this session." MsgBox("Look Slots diagnostics`n`n" . detail . "`n`n" . used . "`n`nCtrl+C copies this. Send it to " . SUPPORT . " if the hotkeys are " . "not working.", "Look Slots diagnostics", "Iconi") } ShowMenuDump(*) { global SUPPORT try { info := InspectPluginMenu() } catch as e { MsgBox("Could not read Lightroom's menu: " . e.Message, "Look Slots", "Icon!") return } MsgBox("Stage reached: " . info["stage"] . JoinTitles(info["items"], 40) . "`n`nCtrl+C copies this. Send it to " . SUPPORT . " if the hotkeys are " . "not working.", "Look Slots - Lightroom menu dump", "Iconi") } A_TrayMenu.Add() A_TrayMenu.Add("Look Slots: check Lightroom menu", ShowDiagnostics) A_TrayMenu.Add("Look Slots: dump Plug-in Extras", ShowMenuDump) TrayTip("Ctrl+Alt+1..9 copies a slot, Ctrl+Alt+Shift+1..9 pastes it - while " . "Lightroom is the focused window.", "Look Slots hotkeys loaded", "Iconi")