Swift - Applescript makes the "Pages" Application unresponsive

Good day, I have an app that works on Ventura but it doesn’t seem to work for Sonoma.

It makes the “Pages” application unresponsive and it doesn’t export a pdf. I removed the finalizer part and it does export the PDF.

I have tried to view and add logs and console but it goes through everything and the logs/ console does not have an error and goes through successfully, but the Pages application is still unresponsive and no output. I tried to make it

I think this is the part where it makes the pages unresponsive:

Applescript:

    on clickEveryMenuItemWithNameContains:doesContain nameNotContains:notContain menuName:menuName -- (NSString, NSString, NSString) -> ()
        --Get every menu item from menuName menu
        --That does not contain notContain
        --And contains doesContain in name
        --Then log its name and finally click it
        set doesContain to doesContain as string
        set notContain to notContain as string
        set menuName to menuName as string
        myLog("ClickEveryMenuItem with \"" & doesContain & "\" and not \"" & notContain & "\" in " & menuName)
        tell application "System Events" to tell process (applicationName as string)
            tell menu 1 of menu bar item menuName of menu bar 1
                tell every menu item whose ((enabled is true) and (name contains doesContain) and not (name contains notContain))
                    my myLog("Click <" & my convertListToString(name, "><") & "> of " & menuName)
                    click
                end tell
            end tell
        end tell
    end clickEveryMenuItemWithNameContains

    on clickEverySubmenuItemWithNameContains:doesContain nameNotContains:notContain submenuName:submenuName menuName:menuName -- (NSString, NSString, NSString, NSString) -> ()
        --Get every menu item from subnameMenu of menuName menu
        --That does not contain notContain
        --And contains doesContain in name
        --Then log its name and finally click it
        set doesContain to doesContain as string
        set notContain to notContain as string
        set submenuName to submenuName as string
        set menuName to menuName as string
        myLog("ClickEverySubmenuItem with \"" & doesContain & "\" and not \"" & notContain & "\" in " & submenuName & " in " & menuName)
        tell application "System Events" to tell process (applicationName as string)
            tell menu 1 of menu item submenuName of menu 1 of menu bar item menuName of menu bar 1
                tell every menu item whose ((enabled is true) and (name contains doesContain) and not (name contains notContain))
                    my myLog("Click <" & my convertListToString(name, "><") & "> of " & menuName)
                    click
                end tell
            end tell
        end tell
    end clickEverySubmenuItemWithNameContains

    on clickMenuItemByName:itemName menuName:menuName -- (NSString, NSString) -> ()
        --Click menu item of name itemName
        --That exists in menu bar 1 menu menuName
        set itemName to itemName as string
        set menuName to menuName as string
        myLog("ClickMenuItemByName " & itemName & " in " & menuName)
        tell application "System Events" to tell process (applicationName as string)
            click menu item itemName of menu 1 of menu bar item menuName of menu bar 1
        end tell
    end clickMenuItemByName

    on clickSubmenuItemByName:itemName submenuName:submenuName menuName:menuName -- (NSString, NSString) -> ()
        --Click menu item of name itemName
        --That exists in submenu submenuName
        --Of menu bar 1 menu menuName
        set itemName to itemName as string
        set submenuName to submenuName as string
        set menuName to menuName as string
        myLog("ClickSubmenuItemByName " & itemName & " in " & menuName)
        tell application "System Events" to tell process (applicationName as string)
            click menu item itemName of menu 1 of menu item submenuName of menu 1 of menu bar item menuName of menu bar 1
        end tell
    end clickSubmenuItemByName

and this is the Swift part

   func finalizeFrontmost(showCommentsPane : Bool = false) {
        NSLog("[pagesUtil] finalize Frontmost")
        genericBridge.bringToFront()
        genericBridge.maximizeWindow()
        //Set 100% zoom
        genericBridge.clickSubmenuItemByName("Actual Size".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
        //Fit page to window
        genericBridge.clickSubmenuItemByName("Fit Page".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
        
        //"Hide Rulers" in Keynote didn't hide so we're back to 2x workaround
        for _ in 0..<2 {
            self.refreshAllMenus()
            
            //Set view to document only
            genericBridge.clickMenuItemByName("Document only".NSLocalized(), menuName: "View".NSLocalized())
            //Click every "Hide"-able thing
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Find".NSLocalized(), menuName: "Edit".NSLocalized())
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Spelling and Grammar".NSLocalized(), menuName: "Edit".NSLocalized())
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Substitutions".NSLocalized(), menuName: "Edit".NSLocalized())
            genericBridge.clickEveryMenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "Toolbar".NSLocalized(), menuName: "View".NSLocalized())
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Inspector".NSLocalized(), menuName: "View".NSLocalized())
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Guides".NSLocalized(), menuName: "View".NSLocalized())
            genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Comments & Changes".NSLocalized(), menuName: "View".NSLocalized())
        }
        //Untick Check Spelling and Grammar while typing
        genericBridge.untickSubmenuItemByName("Check Spelling While Typing".NSLocalized(), submenuName: "Spelling and Grammar".NSLocalized(), menuName: "Edit".NSLocalized())
        //Set 100% zoom
        //genericBridge.clickSubmenuItemByName("Actual Size".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
        genericBridge.setZoomByToolbar("100%".NSLocalized())
        sleep(1) //waiting for animation
        //Zoom window to fit slide
        genericBridge.clickMenuItemByName("Zoom".NSLocalized(), menuName: "Window".NSLocalized())
        sleep(1) //waiting for animation
        //Fit width to window
        genericBridge.clickSubmenuItemByName("Fit Width".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
        sleep(1) //waiting for animation
        //Go to first page, but first go to last page
        genericBridge.clickSubmenuItemByName("Last Page".NSLocalized(), submenuName: "Go To".NSLocalized(), menuName: "View".NSLocalized())
        genericBridge.clickSubmenuItemByName("First Page".NSLocalized(), submenuName: "Go To".NSLocalized(), menuName: "View".NSLocalized())
        sleep(1)
        // Show comments pane
        if (showCommentsPane) {
            genericBridge.clickMenuItemByName("Show Comments Pane".NSLocalized(), menuName: "View".NSLocalized())
            sleep(1) //waiting for animation
        }
        //Save
        genericBridge.clickMenuItemByName("Save".NSLocalized(), menuName: "File".NSLocalized())
    }
    
    func refreshAllMenus() {
        // Click through every needed menu. It's a workaround
        // for some dumb refreshing issue with System Events.
        // Basically System Events does not update the value
        // of buttons unless the (sub)menu is opened so right
        // after change there is non-updated value there
        
        NSLog("[pagesUtil] refresh All Menus")
        //first get strings from Localized
        let file: NSString = "File".NSLocalized()
        let edit: NSString = "Edit".NSLocalized()
        let view: NSString = "View".NSLocalized()
        let window: NSString = "Window".NSLocalized()
        //Refresh main menus
        genericBridge.refreshMenuBarMenu(file)
        genericBridge.refreshMenuBarMenu(edit)
        genericBridge.refreshMenuBarMenu(view)
        genericBridge.refreshMenuBarMenu(window)
        
        //Refresh submenus
        genericBridge.refreshMenuBarSubmenu("Find".NSLocalized(), menuName: edit)
        genericBridge.refreshMenuBarSubmenu("Spelling and Grammar".NSLocalized(), menuName: edit)
        genericBridge.refreshMenuBarSubmenu("Substitutions".NSLocalized(), menuName: edit)
        
        
        genericBridge.refreshMenuBarSubmenu("Inspector".NSLocalized(), menuName: view)
        genericBridge.refreshMenuBarSubmenu("Guides".NSLocalized(), menuName: view)
        genericBridge.refreshMenuBarSubmenu("Comments & Changes".NSLocalized(), menuName: view)
        genericBridge.refreshMenuBarSubmenu("Zoom".NSLocalized(), menuName: view)
    }

and my trial Async

func finalizeFrontmost(showCommentsPane: Bool = false, completion: @escaping () -> Void) {
    NSLog("[pagesUtil] finalize Frontmost")
    
    genericBridge.bringToFront()
    waitAndLogAsync("After bringing to front") { [self] in
        genericBridge.maximizeWindow()
        self.waitAndLogAsync("After maximizing window") { [self] in
            genericBridge.clickSubmenuItemByName("Actual Size".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
            self.waitAndLogAsync("After setting actual size zoom") { [self] in
                genericBridge.clickSubmenuItemByName("Fit Page".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
                self.waitAndLogAsync("After fitting page to window") { [self] in
                        
                    for _ in 0..<2 {
                        self.refreshAllMenus()

                        self.genericBridge.clickMenuItemByName("Document only".NSLocalized(), menuName: "View".NSLocalized())
                        self.waitAndLogAsync("After setting view to document only", seconds: 5)
                        self.genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Find".NSLocalized(), menuName: "Edit".NSLocalized())
                        self.genericBridge.clickEverySubmenuItem(nameContains: "Hide".NSLocalized(), nameNotContains: "", submenuName: "Spelling and Grammar".NSLocalized(), menuName: "Edit".NSLocalized())
                        // Add similar waitAndLogAsync statements for other actions
                    }
                    self.genericBridge.untickSubmenuItemByName("Check Spelling While Typing".NSLocalized(), submenuName: "Spelling and Grammar".NSLocalized(), menuName: "Edit".NSLocalized())
                    self.waitAndLogAsync("After unticking 'Check Spelling While Typing'") { [self] in
                        self.genericBridge.setZoomByToolbar("100%".NSLocalized())
                        self.waitAndLogAsync("After setting zoom to 100%") { [self] in
                            self.genericBridge.clickMenuItemByName("Zoom".NSLocalized(), menuName: "Window".NSLocalized())
                            self.waitAndLogAsync("After clicking 'Zoom' in Window menu") { [self] in
                                self.genericBridge.clickSubmenuItemByName("Fit Width".NSLocalized(), submenuName: "Zoom".NSLocalized(), menuName: "View".NSLocalized())
                                self.waitAndLogAsync("After fitting width to window") { [self] in
                                    self.genericBridge.clickSubmenuItemByName("Last Page".NSLocalized(), submenuName: "Go To".NSLocalized(), menuName: "View".NSLocalized())
                                    self.genericBridge.clickSubmenuItemByName("First Page".NSLocalized(), submenuName: "Go To".NSLocalized(), menuName: "View".NSLocalized())
                                    self.waitAndLogAsync("After going to first page") { [self] in
                                        if showCommentsPane {
                                            self.genericBridge.clickMenuItemByName("Show Comments Pane".NSLocalized(), menuName: "View".NSLocalized())
                                            self.waitAndLogAsync("After showing comments pane") { [self] in
                                                self.genericBridge.clickMenuItemByName("Save".NSLocalized(), menuName: "File".NSLocalized())
                                                self.waitAndLogAsync("After saving") { [self] in
                                                    completion() // Notify completion of the entire operation
                                                }
                                            }
                                        } else {
                                            self.genericBridge.clickMenuItemByName("Save".NSLocalized(), menuName: "File".NSLocalized())
                                            self.waitAndLogAsync("After saving") { [self] in
                                                completion() // Notify completion of the entire operation
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

    func waitAndLogAsync(_ message: String, seconds: Double = 1, completion: @escaping () -> Void) {
        NSLog(message)
        DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
            completion()
        }
    }

    func refreshAllMenus() {
        NSLog("[pagesUtil] refresh All Menus")

        // Perform menu refreshing asynchronously
        DispatchQueue.global().async {
            let file: NSString = "File".NSLocalized()
            let edit: NSString = "Edit".NSLocalized()
            let view: NSString = "View".NSLocalized()
            let window: NSString = "Window".NSLocalized()

            // Refresh main menus
            self.genericBridge.refreshMenuBarMenu(file)
            self.genericBridge.refreshMenuBarMenu(edit)
            self.genericBridge.refreshMenuBarMenu(view)
            self.genericBridge.refreshMenuBarMenu(window)

            // Refresh submenus
            self.genericBridge.refreshMenuBarSubmenu("Find".NSLocalized(), menuName: edit)
            self.genericBridge.refreshMenuBarSubmenu("Spelling and Grammar".NSLocalized(), menuName: edit)
            self.genericBridge.refreshMenuBarSubmenu("Substitutions".NSLocalized(), menuName: edit)

            self.genericBridge.refreshMenuBarSubmenu("Inspector".NSLocalized(), menuName: view)
            self.genericBridge.refreshMenuBarSubmenu("Guides".NSLocalized(), menuName: view)
            self.genericBridge.refreshMenuBarSubmenu("Comments & Changes".NSLocalized(), menuName: view)
            self.genericBridge.refreshMenuBarSubmenu("Zoom".NSLocalized(), menuName: view)

            NSLog("[pagesUtil] Refresh completed asynchronously")
        }
        
        // Optionally, introduce a delay if needed
        Thread.sleep(forTimeInterval: 5) // Adjust the duration as necessary
    }

I would like to ask for help on what changes should be made for it to work on Sonoma. This was a premade script of another dev I need to make work, but I am new to AppleScript and Swift. Thank you

I was focused on the Finalizer but it seems that the problem is within RefreshAllMenus, but the coding doesn’t seem to work without it?

If you want to export a PDF there is an AppleScript command in Pages.app.
GUI scripting is not reliable. Any change of the view hierarchy can break the script.

On the Swift side basically you must update any UI on the main thread. At least replace

DispatchQueue.global().async {

with

DispatchQueue.main.async {

1 Like

I’m not sure why GUI scripting gets a bad rap.
I get it very reliable because I always put tests in my scripts to check for items I’m looking for, in case they moved.

Robert, I meant not reliable with regard to software updates which modify the UI hierarchy can break the script, and those modifications are not documented at all.