Search code examples
applescript

Apple Script: Go back-function in menus


I'm not too versed in AppleScript, so maybe someone can help here:

I have several menus. Let's say, Menu A has three options, these three options lead to Menu B1, B2, B3, depending what options was chosen in Menu A. The B-menus lead to C-menus and so on. I am a bit bothered, that every time I "Cancel", I need to start from scratch. A "Go back"-button would be highly useful in this scenario.

I know that the choose from list-function does only allow Yes and no, so to say, but is it possible to somehow implement a back button?

My code looked like this (we're in Menu B):

if MenuB is false then error number -128 -- user canceled

Then I tried implementing the "Go back" function via:

if MenuB is false then set MenuA to (choose from list {"MenuB1", "MenuB2", "MenuB3"} with prompt "Menu A" default items "None" OK button name {"Go"} cancel button name {"Quit"})

However that creates a new menu and doesn't refer to the menu created before the Menu B script.

Tl;dr: So is it somehow possible to reference to existing menus based on choose from list without creating a new menu. Or is there a way to implement a "back"-button in menus via AppleScript?

Looking forward to your thoughts!

Code excerpt:

# Main Menu
set MainMenu to (choose from list {"Item1", "Item2", "Item3"} with prompt "Main Menu" default items "None" OK button name {"Go"} cancel button name {"Quit"})
if MainMenu is false then error number -128 -- user canceled

# Button 1: Item 1
if MainMenu contains "Item1" then
    
    # Sub-Button 1: Item 1
    set SubButton to (choose from list {"Create", "Rename"} with prompt "Settings" default items "None" OK button name {"Choose"} cancel button name {"Quit"})
    if SubButton is false then error number -128 -- user canceled
    
    # Sub-Sub-Button 1: Item 1
    if SubButton contains "Create" then
(...)

Solution

  • Without knowing the exact number of menus and how they interact, the easiest way would probably be to implement a stack, where items can be pushed onto or popped from the stack depending on the dialog results. Then it is just a matter of organizing and connecting the menus, as it is doubtful you will run out of stack space before running out of patience. Trying to navigate back and forth across nested or interconnected menus by just using a long sequence of if statements leads to the dark side.

    To make the menu lists a little more manageable, they can be put into a collection object such as a record, rather than hard coding the dialog statements, with the record keys being used to keep track of the current and previous lists. From each choose from list dialog, the current choice can be pushed onto the top of the stack, and the previous item popped from it if going back.

    A little bit of AppleScriptObjC is used in the following example to dynamically look up the record keys, since regular AppleScript doesn’t have a way to do that. The lists contain the various menu items, which are also used to look up the keys for other menu lists - surrounding the record keys with pipes allows them to have spaces and punctuation, so pretty much any text can be used as a key. Using menu item text as record keys also allows circling and jumping around (the example script also demonstrates this), so just be aware when laying out the menu connections. The ultimate end values are kept in a different record, and are single items (string or list). The final result is also used in a choice dialog for verification and to allow going back.

    Note that the funky comma placements are used to try to make the record formatting a little easier to read.

    use AppleScript version "2.4" -- Yosemite (10.10) or later
    use framework "Foundation"
    use scripting additions
    
    property stack : {"base"} -- LIFO, starting with key of the base menu
    property menus : ¬
       {base:{"menu A", "menu B", "menu C"} ¬
          , |menu A|:{"subMenu A1", "subMenu A2", "subMenu A3"} ¬
          , |menu B|:{"subMenu B1", "subMenu B2", "subMenu B3"} ¬
          , |menu C|:{"A key", "A little longer key", "The quick brown fox jumped over the lazy dog's key"} ¬
          , |subMenu A1|:{"Action1", "Action2", "Action3"} ¬
          , |subMenu A2|:{"Action4", "Action5", "Action6"} ¬
          , |subMenu A3|:{"Action7", "Action8", "Action9"} ¬
          , |subMenu B1|:{"menu A", "menu B", "menu C"} ¬
          , |subMenu B2|:{"subMenu A1", "subMenu A2", "subMenu A3"} ¬
          , |subMenu B3|:{"A key", "A little longer key", "The quick brown fox jumped over the lazy dog's key"} ¬
          , |A key|:{"Action1", "Action2", "Action3"} ¬
          , |A little longer key|:{"Action4", "Action5", "Action6"} ¬
          , |The quick brown fox jumped over the lazy dog's key|:{"Action7", "Action8", "Action9"}}
    property actions : ¬
       {Action1:"Action one", Action2:"Action two", Action3:"Action three", Action4:"Action four", Action5:"Action five", Action6:"Action six", Action7:"Action seven", Action8:"Action eight", Action9:"Action 9"}
    
    on run -- example
       set menuDict to current application's NSDictionary's dictionaryWithDictionary:menus
       set actionDict to current application's NSDictionary's dictionaryWithDictionary:actions
       set cancelName to "Quit"
       repeat
          set tos to first item of stack
          set choices to (menuDict's objectForKey:tos) -- get menu
          if choices is missing value then -- handle missing key
             log "menu key " & quoted form of tos & " not found, trying actions"
             set choices to (actionDict's objectForKey:tos) -- try actions
             if choices is missing value then log "actions key " & quoted form of tos & " not found"
          end if
          set theChoice to (choose from list (choices as list) default items {"msng"} cancel button name cancelName) -- select missing value
          if theChoice is false then
             if (count stack) is 1 then error number -128 -- cancel
             set stack to rest of stack -- pop
             if (count stack) is 1 then set cancelName to "Quit" -- last one
          else
             if (count (choices as list)) is 1 then exit repeat -- final action result selected
             set beginning of stack to theChoice as text -- push
             set cancelName to "Go Back"
          end if
       end repeat
       #showResult(stack, actionDict) -- uncomment to show the result
       runWorkflow(((actionDict's objectForKey:(first item of stack)) as text))
    end run
    
    to showResult(stack, dictionary) -- show the final result action
       set {tempTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, " > "}
       set keys to (reverse of stack) as text
       set AppleScript's text item delimiters to tempTID
       set value to quoted form of ((dictionary's objectForKey:(first item of stack)) as text)
       if value is quoted form of "missing value" then set value to value & return & return & "A menu or actions key was not found - see log."
       display dialog "Key sequence:  " & keys & return & "Final value:  " & value with title "Final Result" buttons "OK" default button 1
    end showResult
    
    to runWorkflow(workflowName) -- run an Automator workflow
       set basepath to POSIX path of (path to desktop) -- change as desired
       set workflowpath to basepath & workflowName -- POSIX path/name to add to the basePath
       set command to "/usr/bin/automator " & quoted form of workflowpath
       set output to (do shell script command)
       -- do whatever with the output
    end runAction