Search code examples
applescript-objc

Progress Bar with Applescript


Is there any way to make a progress bar that is loading as a shell script or shell command runs?

I currently use the following:

property theWindow : missing value
property parent : class "NSObject"
property customView : missing value
property myProgressBar : missing value

on hardwareData()

    set alert to current application's NSAlert's alloc's init()
        tell alert
            its setAccessoryView:customView
            its setMessageText:"Loading Informations"
            its setInformativeText: "Please Wait..."
            its setAlertStyle:1
            its addButtonWithTitle:"Cancel"
            its setShowsSuppressionButton:false
            its beginSheetModalForWindow:theWindow modalDelegate:me didEndSelector:(missing value) contextInfo:(missing value)
        end tell

delay 0.02
set hardwareData to do shell script "system_profiler SPHardwareDataType"

(*This loop loads the progress bar but only after the
shell command was executed and not at run time.*)
set c to 0
        repeat 100 times
            set c to c + 1
            delay 0.06
            tell myProgressBar to setDoubleValue_(c)
            if c > 99 then
                exit repeat
            end if
        end repeat
end hardwareData

I think this is a fake progress bar once which does not execute together with the shell script.


Solution

  • The user interface will be blocked if you don't give the system time to handle events - by using a tight repeat loop, for example. If a loop is needed, you can periodically call a handler to update the UI, or manually handle system events. For a third party scriptable progress indicator background application there is also the SKProgressBar from one of the MacScripter regulars.

    If you are planning to use a shell script, note that it will also block the user interface if it takes time to complete, and may not provide feedback that you can use for its progress. Anything that will take time to to complete should be performed with an asynchronous background task, but AppleScriptObjC is a bit limited in that regard. NSTask provides a way to perform background tasks with notifications, so you might want to check that out, as its use and arranging your app around the notifications is another subject.

    You should start using an Objective-C category that provides access to the new block-based alert methods, but to keep using the old deprecated sheet method you will need to use action handlers for any buttons (such as a cancel) you want to add. The following Xcode project (just create a blank AppleScriptObjC project and copy to the AppDelegate file) uses your counter to simulate the progress:

    # AppDelegate.applescript
    
    script AppDelegate
        property parent : class "NSObject"
        property theWindow : missing value
        property alert : missing value  -- this will be the alert
        property myProgressBar : missing value -- this will be the progress indicator
        property alertCancel : false -- this will be a flag to indicate cancel
    
        to makeButton(title, x, y) -- make a button at the {x, y} position
            tell (current application's NSButton's buttonWithTitle:title target:me action:"buttonAction:")
                its setFrame:{{x, y}, {120, 24}}
                its setRefusesFirstResponder:true -- no highlight
                return it
            end tell
        end makeButton
    
        on buttonAction:sender -- perform the alert
            if alert is missing value then tell current application's NSAlert's alloc's init()
                set my alert to it
                its setMessageText:"Loading Informations"
                its setInformativeText:"Please Wait..."
                set cancelButton to its addButtonWithTitle:"Cancel"
                cancelButton's setTarget:me
                cancelButton's setAction:"cancelButton:"
                its setAccessoryView:(my makeIndicator())
            end tell
            set my alertCancel to false -- reset
            myProgressBar's setDoubleValue:0
            alert's beginSheetModalForWindow:theWindow modalDelegate:me didEndSelector:(missing value) contextInfo:(missing value)
            doStuff()
        end buttonAction:
    
        on cancelButton:sender -- mark alert as cancelled
            set my alertCancel to true
            current application's NSApp's endSheet:(alert's |window|)
        end cancelButton:
    
        to makeIndicator() -- make and return a progress indicator
            alert's layout()
            set theSize to second item of ((alert's |window|'s frame) as list)
            set width to (first item of theSize) - 125 -- match alert width
            tell (current application's NSProgressIndicator's alloc's initWithFrame:{{0, 0}, {width, 22}})
                set myProgressBar to it
                set its indeterminate to false
                set its maxValue to 100
                return it
            end tell
        end makeIndicator
    
        on doStuff() -- the main progress loop
            set c to 0
            repeat 100 times
                set c to c + 1
                delay 0.06 -- do something
                tell myProgressBar to setDoubleValue:c
                fetchEvents()
                if c > 99 or alertCancel then exit repeat
            end repeat
            current application's NSApp's endSheet:(alert's |window|)
        end doStuff
    
        on fetchEvents() -- handle user events
            repeat -- forever
                tell current application's NSApp to set theEvent to its nextEventMatchingMask:(current application's NSEventMaskAny) untilDate:(missing value) inMode:(current application's NSDefaultRunLoopMode) dequeue:true
                if theEvent is missing value then return -- none left
                tell current application's NSApp to sendEvent:theEvent -- pass it on
            end repeat
        end fetchEvents
    
        ##################################################
        #   Delegate methods
        ##################################################
    
        on applicationWillFinishLaunching:aNotification
            theWindow's contentView's addSubview:makeButton("Show Alert", 180, 30)
        end applicationWillFinishLaunching:
    
        on applicationShouldTerminate:sender
            return current application's NSTerminateNow
        end applicationShouldTerminate:
    
    end script