Instantly share code, notes, and snippets.
Created
January 23, 2018 13:05
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save latenitefilms/fc62c0ccdc97f0dacf45bf322aa49782 to your computer and use it in GitHub Desktop.
MenuBar Debugging
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-------------------------------------------------------------------------------- | |
-------------------------------------------------------------------------------- | |
-- F I N A L C U T P R O A P I -- | |
-------------------------------------------------------------------------------- | |
-------------------------------------------------------------------------------- | |
--- === cp.apple.finalcutpro.MenuBar === | |
--- | |
--- Represents the Final Cut Pro menu bar, providing functions that allow different tasks to be accomplished. | |
-------------------------------------------------------------------------------- | |
-- | |
-- EXTENSIONS: | |
-- | |
-------------------------------------------------------------------------------- | |
local log = require("hs.logger").new("menubar") | |
local fnutils = require("hs.fnutils") | |
local archiver = require("cp.plist.archiver") | |
local axutils = require("cp.ui.axutils") | |
local just = require("cp.just") | |
local plist = require("cp.plist") | |
-------------------------------------------------------------------------------- | |
-- | |
-- THE MODULE: | |
-- | |
-------------------------------------------------------------------------------- | |
local MenuBar = {} | |
MenuBar.ROLE = "AXMenuBar" | |
--- cp.apple.finalcutpro.MenuBar:new(App) -> MenuBar | |
--- Function | |
--- Constructs a new MenuBar for the specified App. | |
--- | |
--- Parameters: | |
--- * app - The App instance the MenuBar belongs to. | |
--- | |
--- Returns: | |
--- * a new MenuBar instance | |
--- | |
function MenuBar:new(app) | |
local o = { | |
_app = app, | |
_itemFinders = {}, | |
} | |
setmetatable(o, self) | |
self.__index = self | |
return o | |
end | |
-- TODO: Add documentation | |
function MenuBar:app() | |
return self._app | |
end | |
-- TODO: Add documentation | |
function MenuBar:UI() | |
return axutils.cache(self, "_ui", function() | |
local appUI = self:app():UI() | |
return appUI and axutils.childWith(appUI, "AXRole", MenuBar.ROLE) | |
end) | |
end | |
function MenuBar:isShowing() | |
return self:UI() ~= nil | |
end | |
-- TODO: Add documentation | |
function MenuBar:getMainMenu() | |
if not MenuBar._mainMenu then | |
MenuBar._mainMenu = self:_loadMainMenu() | |
end | |
return MenuBar._mainMenu | |
end | |
--- cp.apple.finalcutpro.MenuBar:selectMenu(path) -> boolean | |
--- Function | |
--- Selects a Final Cut Pro Menu Item based on the list of menu titles in English. | |
--- | |
--- Parameters: | |
--- * path - The list of menu items you'd like to activate, for example: | |
--- select({"View", "Browser", "as List"}) | |
--- | |
--- Returns: | |
--- * `true` if the press was successful. | |
function MenuBar:selectMenu(path) | |
local menuItemUI = self:findMenuUI(path) | |
if menuItemUI then | |
return menuItemUI:doPress() | |
end | |
return false | |
end | |
-- TODO: Add documentation | |
local function _isMenuChecked(menu) | |
return menu:attributeValue("AXMenuItemMarkChar") ~= nil | |
end | |
-- TODO: Add documentation | |
function MenuBar:isChecked(path) | |
local menuItemUI = self:findMenuUI(path) | |
return menuItemUI and _isMenuChecked(menuItemUI) | |
end | |
-- TODO: Add documentation | |
function MenuBar:isEnabled(path) | |
log.df("Starting MenuBar:isEnabled. Path: %s", hs.inspect(path)) | |
local menuItemUI = self:findMenuUI(path) | |
log.df("Finishing MenuBar:isEnabled. menuItemUI: %s", hs.inspect(menuItemUI)) | |
return menuItemUI and menuItemUI:attributeValue("AXEnabled") | |
end | |
-- TODO: Add documentation | |
function MenuBar:checkMenu(path, wait) | |
local menuItemUI = self:findMenuUI(path) | |
if menuItemUI then | |
if not _isMenuChecked(menuItemUI) then | |
menuItemUI:doPress() | |
if wait then | |
just.doUntil(function() return _isMenuChecked(menuItemUI) end, 5) | |
end | |
end | |
return true | |
end | |
return false | |
end | |
-- TODO: Add documentation | |
function MenuBar:uncheckMenu(path, wait) | |
local menuItemUI = self:findMenuUI(path) | |
if menuItemUI then | |
if _isMenuChecked(menuItemUI) then | |
menuItemUI:doPress() | |
if wait then | |
just.doUntil(function() return not _isMenuChecked(menuItemUI) end, 5) | |
end | |
end | |
return true | |
end | |
return false | |
end | |
--- cp.apple.finalcutpro.MenuBar:addMenuFinder(finder) -> nothing | |
--- Method | |
--- Registers an `AXMenuItem` finder function. The finder's job is to take an individual 'find' step and return either the matching child, or nil if it can't be found. It is used by the [addMenuFinder](#addMenuFinder) function. The `finder` should have the following signature: | |
--- | |
--- ```lua | |
--- function(parentItem, path, childName, language) -> childItem | |
--- ``` | |
--- | |
--- The elements are: | |
--- * `parentItem` - The `AXMenuItem` containing the children. E.g. the `Go To` menu under `Window`. | |
--- * `path` - An array of strings in the specified language leading to the parent item. E.g. `{"Window", "Go To"}`. | |
--- * `childName` - The name of the next child to find, in the specified language. E.g. `"Libraries"`. | |
--- * `language` - The language that the menu titles are in. | |
--- * `childItem` - The `AXMenuItem` that was found, or `nil` if not found. | |
--- | |
--- Parameters: | |
--- * `finder` - The finder function | |
--- | |
--- Returns: | |
--- * The `AXMenuItem` found, or `nil`. | |
function MenuBar:addMenuFinder(finder) | |
self._itemFinders[#self._itemFinders+1] = finder | |
end | |
-- Looks through the `menuMap` to find a matching title in the source language, | |
-- and returns the equivalent in the target language, or the original title if it can't be found. | |
local function _translateTitle(menuMap, title, sourceLanguage, targetLanguage) | |
if menuMap then | |
for _,item in ipairs(menuMap) do | |
if menuMap[sourceLanguage] == title then | |
return menuMap[targetLanguage] | |
end | |
end | |
end | |
return title | |
end | |
--- cp.apple.finalcutpro.MenuBar:findMenuUI(path) -> Menu UI | |
--- Method | |
--- Finds a specific Menu UI element for the provided path. | |
--- E.g. `findMenuUI({"Edit", "Copy"})` returns the 'Copy' menu item in the 'Edit' menu. | |
--- | |
--- Each step on the path can be either one of: | |
--- * a string - The exact name of the menu item. | |
--- * a number - The menu item number, starting from 1. | |
--- * a function - Passed one argument - the Menu UI to check - returning `true` if it matches. | |
--- | |
--- Parameters: | |
--- * `path` - The path list to search for. | |
--- * `language` - The language code the path is in. E.g. "en" or "fr". Defaults to the | |
--- | |
--- Returns: | |
--- * The Menu UI, or `nil` if it could not be found. | |
function MenuBar:findMenuUI(path, language) | |
log.df("-------------------------------") | |
log.df("-------------------------------") | |
log.df("-------------------------------") | |
log.df("-------------------------------") | |
log.df("-------------------------------") | |
log.df("Starting findMenuUI.") | |
log.df("path: %s", hs.inspect(path)) | |
log.df("language: %s", hs.inspect(language)) | |
log.df("--") | |
-- Start at the top of the menu bar list | |
local menuMap = self:getMainMenu() | |
local menuUI = self:UI() | |
language = language or "en" | |
local appLang = self:app():currentLanguage() or "en" | |
log.df("menuMap: %s", menuMap) | |
log.df("menuUI: %s", hs.inspect(menuUI)) | |
log.df("appLang: %s", hs.inspect(appLang)) | |
log.df("--") | |
if not menuUI then | |
log.df("Could not find menuUI.") | |
log.df("End of findMenuUI") | |
log.df("-------------------------------") | |
return nil | |
end | |
local menuItemUI = nil | |
local menuItemName = nil | |
local currentPath = {} | |
-- step through the path | |
for i,step in ipairs(path) do | |
menuItemUI = nil | |
-- check what type of step it is | |
log.df("step: %s (%s)", step, step and type(step)) | |
if type(step) == "number" then -- access it by index | |
menuItemUI = menuUI[step] | |
menuItemName = _translateTitle(menuMap, menuItemUI, appLang, language) | |
elseif type(step) == "function" then -- check each child against the function | |
for i,child in ipairs(menuUI) do | |
if step(child) then | |
menuItemUI = child | |
menuItemName = _translateTitle(menuMap, menuItemUI, appLang, language) | |
break | |
end | |
end | |
else | |
log.df("Look it up by name...") | |
-- look it up by name | |
menuItemName = step | |
-- check with the finder functions | |
log.df("menuItemName: %s", hs.inspect(menuItemName)) | |
log.df("self._itemFinders: %s", hs.inspect(self._itemFinders)) | |
for _,finder in ipairs(self._itemFinders) do | |
menuItemUI = finder(menuUI, currentPath, step, language) | |
if menuItemUI then | |
break | |
end | |
end | |
log.df("menuItemUI: %s", hs.inspect(menuItemUI)) | |
if not menuItemUI and menuMap then | |
-- See if the menu is in the map. | |
log.df("See if the menu is in the map.") | |
log.df("menuMap: %s", menuMap) | |
for _,item in ipairs(menuMap) do | |
log.df("item[language]: '%s' = step: '%s'", item[language], step) | |
if item[language] == step then | |
log.df("found a match!") | |
log.df("item: %s", item) | |
menuItemUI = item.item | |
log.df("menuItemUI: %s", hs.inspect(menuItemUI)) | |
if not axutils.isValid(menuItemUI) then | |
log.df("menuItemUI is invalid") | |
log.df("menuUI: %s", hs.inspect(menuUI)) | |
log.df("item[appLang]: %s", hs.inspect(item[appLang])) | |
menuItemUI = axutils.childWith(menuUI, "AXTitle", item[appLang]) | |
log.df("menuItemUI: %s", hs.inspect(menuItemUI)) | |
-- cache the menu item, since getting children can be expensive. | |
log.df("cache the menu item, since getting children can be expensive.") | |
item.item = menuItemUI | |
log.df("item.item: %s", item.item) | |
else | |
log.df("menuItemUI is valid") | |
end | |
menuMap = item.submenu | |
log.df("menuMap: %s", menuMap) | |
break | |
end | |
end | |
end | |
if not menuItemUI then | |
-- We don't have it in our list, so look it up manually. Hopefully they are in English! | |
log.wf("Searching manually for '%s' in '%s'.", step, table.concat(currentPath, ", ")) | |
log.df("currentPath: %s", hs.inspect(currentPath)) | |
log.df("menuUI: %s", hs.inspect(menuUI)) | |
menuItemUI = axutils.childWith(menuUI, "AXTitle", step) | |
log.df("menuItemUI: %s", hs.inspect(menuItemUI)) | |
end | |
end | |
if menuItemUI then | |
if #menuItemUI == 1 then | |
-- Assign the contained AXMenu to the menuUI - it contains the next set of AXMenuItems | |
menuUI = menuItemUI[1] | |
end | |
table.insert(currentPath, menuItemName) | |
else | |
log.wf("Unable to find a menu called '%s'", step) | |
log.df("End of findMenuUI") | |
log.df("-------------------------------") | |
return nil | |
end | |
end | |
log.df("End of findMenuUI") | |
log.df("-------------------------------") | |
return menuItemUI | |
end | |
-- TODO: Add documentation | |
-- Returns the set of menu items in the provided path. If the path contains a menu, the | |
-- actual children of that menu are returned, otherwise the menu item itself is returned. | |
function MenuBar:findMenuItemsUI(path) | |
local menu = self:findMenuUI(path) | |
if menu and #menu == 1 then | |
return menu[1]:children() | |
end | |
return menu | |
end | |
--- cp.apple.finalcutpro.MenuBar:visitMenuItems(visitFn[, startPath]) -> nil | |
--- Method | |
--- Walks the menu tree, calling the `visitFn` on all the 'item' values - that is, | |
--- `AXMenuItem`s that don't have any sub-menus. | |
--- | |
--- The `visitFn` will be called on each menu item with the following parameters: | |
--- | |
--- ``` | |
--- function(path, menuItem) | |
--- ``` | |
--- | |
--- The `menuItem` is the AXMenuItem object, and the `path` is an array with the path to that | |
--- menu item. For example, if it is the "Copy" item in the "Edit" menu, the path will be | |
--- `{ "Edit" }`. | |
--- | |
--- Parameters: | |
--- * `visitFn` - The function called for each menu item. | |
--- * `startPath` - The path to the menu item to start at. | |
--- | |
--- Returns: | |
--- * Nothing | |
function MenuBar:visitMenuItems(visitFn, startPath) | |
local menu = nil | |
local path = startPath or {} | |
if #path > 0 then | |
menu = self:findMenuUI(path) | |
table.remove(path) | |
else | |
menu = self:UI() | |
end | |
if menu then | |
self:_visitMenuItems(visitFn, path, menu) | |
end | |
end | |
-- TODO: Add documentation | |
function MenuBar:_visitMenuItems(visitFn, path, menu) | |
local role = menu:attributeValue("AXRole") | |
local children = menu:attributeValue("AXChildren") | |
if role == "AXMenuBar" or role == "AXMenu" then | |
if children then | |
for _,item in ipairs(children) do | |
self:_visitMenuItems(visitFn, path, item) | |
end | |
end | |
elseif role == "AXMenuBarItem" or role == "AXMenuItem" then | |
local title = menu:attributeValue("AXTitle") | |
if #children == 1 then | |
-- add the title | |
table.insert(path, title) | |
-- log.df("_visitMenuItems: post insert: path = %s", hs.inspect(path)) | |
self:_visitMenuItems(visitFn, path, children[1]) | |
-- drop the extra title | |
-- log.df("_visitMenuItems: pre remove: path = %s", hs.inspect(path)) | |
table.remove(path) | |
-- log.df("_visitMenuItems: post remove: path = %s", hs.inspect(path)) | |
else | |
if title ~= nil and title ~= "" then | |
visitFn(path, menu) | |
end | |
end | |
end | |
end | |
function MenuBar:_loadMainMenu(languages) | |
languages = languages or self:app():getSupportedLanguages() | |
local menu = {} | |
for _,language in ipairs(languages) do | |
if language then | |
self:_loadMainMenuLanguage(language, menu) | |
else | |
log.wf("Received a nil language request.") | |
end | |
end | |
return menu | |
end | |
function MenuBar:_loadMainMenuLanguage(language, menu) | |
local menuPlist = plist.fileToTable(string.format("%s/Contents/Resources/%s.lproj/MainMenu.nib", self:app():getPath(), language)) | |
if menuPlist then | |
local menuArchive = archiver.unarchive(menuPlist) | |
-- Find the 'MainMenu' item | |
local mainMenu = nil | |
for _,item in ipairs(menuArchive["IB.objectdata"].NSObjectsKeys) do | |
if item.NSName == "_NSMainMenu" and item["$class"] and item["$class"]["$classname"] == "NSMenu" then | |
mainMenu = item | |
break | |
end | |
end | |
if mainMenu then | |
return self:_processMenu(mainMenu, language, menu) | |
else | |
log.ef("Unable to locate MainMenu in '%s.lproj/MainMenu.nib'.", language) | |
return nil | |
end | |
else | |
log.ef("Unable to load MainMenu.nib for specified language: %s", language) | |
return nil | |
end | |
end | |
function MenuBar:_processMenu(menuData, language, menu) | |
if not menuData then | |
return nil | |
end | |
-- process the menu items | |
menu = menu or {} | |
if menuData.NSMenuItems then | |
for i,itemData in ipairs(menuData.NSMenuItems) do | |
local item = menu[i] or {} | |
item[language] = itemData.NSTitle | |
item.separator = itemData.NSIsSeparator | |
-- Check if there is a submenu | |
if itemData.NSSubmenu then | |
item.submenu = MenuBar:_processMenu(itemData.NSSubmenu, language, item.submenu) | |
end | |
menu[i] = item | |
end | |
end | |
return menu | |
end | |
return MenuBar |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment