Cocoa and AppleScript / Apple Events - objective-c

I'm using Cocoa’s [[NSWorkspace sharedWorkspace] launchApplicationAtURL… with the NSWorkspaceLaunchNewInstance option to spawn new instances of an AppleScriptable application (Adobe Acrobat), and I want to be able to trigger different Apple Events (do script, quit, save etc…) for each instance.
So far I’ve tried to AppleScript ”System Events” and tell commands based on a new process’ id but for some reason the commands aren't executed by the target process.
I'm getting the process id as [NSRunningApplication processIdentifier] and use that to compile an applescript by [[[NSAppleScript alloc] initWithSource: AppleScript] executeAndReturnError: nil]. The string representation of the AppleScript is something like the following:
tell application "System Events"
tell (process id [insert process id here]) to do script "this.preflight(Preflight.getProfileByName('Magazine Ads'),false,false);"
end tell
I'm suspecting that the processIdentifier returned by NSRunningApplication is different from the process id used by ”System Events”, but I'm stuck and don't know where to look to get any further. I need a pointers on how to trigger AppleScriptable methods of specific application process from Cocoa, given that
there can be several instances of the same application running and
each process that I want to communicate with will be created within the scope of my code
(Running new processes of Adobe Acrobat is necessary to allow the user to do other work while a preflight is running.)
Edit: The process id returned by cocoa and AppleScript are different:
tell application "System Events" to set process_id to id of every process whose name contains "AdobeAcrobat"
returned {5584211,…}, while at the same time
[NSRunningApplication processIdentifier]
returned 8722
Edit 2: The AppleEvent object does make it possible to address a process with a certain process id but I haven't been able to figure out how to apply it to an AppleScript object.
pid_t process_id = …;
NSAppleEventDescriptor* appleevent = [[NSAppleEventDescriptor alloc] initWithDescriptorType:typeKernelProcessID bytes:&process_id length:sizeof(pid)];
I still haven't figured out how NSAppleEventDescriptor be used to access AppleScriptable methods of a process with the given process_id. Any pointers to resources and possibly an example of this would be a perfectly answer to my question.

This does not work because this command is for Acrobat.
You must use the Processes suite commands ("System Events") when you want to simulate (click or the keyboard), get or set (value, property or attribute) on the `GUI elements of the process.
Here is the syntax for using the command do script
tell application id "process.id.here" to do script "this.preflight(Preflight.getProfileByName('Magazine Ads'),false,false);"
Edit :
I thought it was the bundle identifier.
It's not possible to execute a do script by "System Events".
But it is possible, if you have this script in a menu of the application.
You need to find the correct application ID, try unix ID.
To do what you want, put the application in the foreground and run the script
using terms from application "Adobe Acrobat"
tell application (path to frontmost application)
do script "this.preflight(Preflight.getProfileByName('Magazine Ads'),false,false);"
end tell
end using terms from
The Processes suite : Here is a sample script that click on a menu.
tell application "System Events"
tell (first process whose unix id is (processIdHere as integer))
set frontmost to true
click menu item "Crop to TrimBox*" of menu "Document" of menu bar 1
end tell
end tell

Related

Create a new PSD file and copy/paste new layer with AppleScript

I try to make a code assembly to create my own applescript to create an image in Photoshop but unfortunately I have an error and I can't find where it comes from.
The first part of the script works, the random image is well copied to the clipboard but the rest is problematic.
-- Select random image
tell application "Finder"
set bg_img to file (random number from 1 to (count files of folder "HD:Works:Club:LAYERS:0-BG")) of folder "HD:Works:Club:LAYERS:0-BG"
end tell
tell application "Preview"
activate
open bg_img
end tell
-- Select and copy image to clipboard
tell application "System Events"
tell process "Preview"
keystroke "a" using command down
keystroke "c" using command down
end tell
end tell
-- Quit Preview Application
tell application "Preview" to quit
tell application "Adobe Photoshop 2022"
-- Create a new document
set docRef to make new document with properties {width:1200, height:1200, resolution:72}
tell docRef
-- Unlock the background layer and fill it with gray color
set background layer of layer 1 of docRef to false
fill selection with contents {class:RGB color, red:200, green:200, blue:200}
end tell
end tell
-- Paste image to new layer
tell application "System Events"
tell process "Adobe Photoshop 2022"
set newLayer to make art layer with properties {name:"LayerA"}
keystroke "v" using command down
end tell
end tell
Ok, a compilation error; I got it now. Try this:
-- Paste image to new layer
tell application "Adobe Photoshop 2022"
activate
set newLayer to make art layer with properties {name:"LayerA"}
end tell
tell application "System Events"
tell process "Adobe Photoshop 2022"
keystroke "v" using command down
end tell
end tell
Remember, you use a tell application "NAME" … end tell block to 1. tell AppleScript which app to send commands to, and 2. the app from which to get the custom terminology (keywords) for commands and objects.
Thus your tell application "System Events"…end tell block uses SE terminology and commands. The tell process "Photoshop"…end tell block inside it is still addressed to System Events. process is an SE-defined keyword for an SE-specific object. Likewise keystroke is an SE-specific command.
To make a new art layer in Photoshop, you have to put that command inside its own tell application block which is targeted at PS.
Yeah, AppleScript is often confusing. And System Events is 10× more confusing than that, thanks to it being not-obvious what is GUI Scripting and what isn’t. Good luck.
--
p.s. I’ve a feeling the tell process block is totally redundant, and is only necessary when using GUI Scripting to manipulate an app’s windows. IIRC the keystroke command doesn’t know or care what app receives those keys, in which case what’s important is that you activate Photoshop before you start “pressing” keys. I’ll leave it to you to determine if you can discard the tell process.
p.p.s. AppleScriptable apps usually don’t include cut and paste commands, usually ’cos they already provide far better ways of getting and setting content. Photoshop, however, does. So if you’ve not already tried it, try inserting a paste command at the end of your “tell Photoshop” block, then comment out the “tell System Events” block, and see if that works for you. If so, you can get rid of that System Events block entirely!
p.p.p.s. It’s also worth investigating PS’s dictionary further, to see if it can open and place your “bg_file” directly. If so, you can eliminate the “tell Preview” and the rest of that System Events horridness too!!

Objective equivalent to AppleScript : Opening directory without bringing every finder window to from

There this AppleScript
tell application "Google Chrome" to set index of window 1 to 1
do shell script "open /Volumes"
Which opens a directory in Finder without bringing every onther Finder windows to the front.
Currently I'm using :
[[NSWorkspace sharedWorkspace] openURL:fileURL];
But it has the flaw to bring every Finder windows on the top of others.
Any idea how I could achieve the same behaviour as the AppleScript ?
You can always use NSAppleScript to run applescript code in Objective-C if Cocoa doesn't provide the functionality you want.
At a guess, -[NSWorkspace openURL:] also sends the application an activate event whereas the open process does not.
I'd recommend looking into the LaunchServices API. It's what both NSWorkspace and open use behind the scenes, but gives you more control than NSWorkspace's limited API.
--
p.s. If you do have to call out to open (or any other command line tool) from ObjC, you should use Cocoa's NSTask. (AppleScript's do shell script command is just its [crappy] equivalent of NSTask.)

Why would my application fail to "unhide" (show)

I'm investigating an issue on Mac OS X 10.8, and I am at the end of my wits. I'm not sure what to do next.
The application is 32 bit and has some Carbon calls in it.
Here is the problem: when I right-click the application icon in the dock, select the menu item "Hide", then, after the application has hidden, I select the "Show" menu item from the dock, and the problem occurs: the main document window does not appear (the palettes and menu do appear).
At this point, the "Show" menu item does not change to "Hide" even though the palettes have become visible.
I expect that the main document window becomes visible when I select "Show" from the application dock menu. Just like other Mac applications.
When it fails, I can make the main document window visible again if I use the App Exposé gesture on the Trackpad to show the document windows and select the main document window.
It works fine if I launch the application from the Terminal or from Xcode. The document window shows and the dock menu item for my application changes to "Hide" as expected. I launch the app from the terminal by navigating to the parent directory of the *.app, and typing ./MyApp.app/Contents/MacOS/MyApp.
It fails when I have launched by double-clicking the application icon in the Finder.
My log messages from the application delegate's unhide functions appear when the application is launched from the Terminal and Xcode, but not when launched from the Finder.
– applicationWillUnhide:
– applicationDidUnhide:
I have looked in the Console.app for any exceptions thrown (or any other messages). There are none.
Update:
To try and debug this, I launched from the Finder, and used Xcode to attach to the process.
I had suspected that an exception was being thrown and when I tested it using Xcode's "Exception Breakpoint" as well as putting a breakpoint on objc_exception_throw (just in case), it dd not break when I hide or "show" the application.
I then thought that I needed to prove that the NSApplicationWillUnhideNotification and NSApplicationDidUnhideNotification were being sent out. They are when I launch from Xcode or from the Terminal, but if I launch from the Finder, they are not.
I verified this by, after attaching Xcode to the application, putting a breakpoint via "Add Symbolic Breakpoint" for:
-[NSNotificationCenter postNotificationName:object:userInfo]
And then, I added a Debugger Command: "po * (id*) ($esp+12)" to print out the first parameter to that selector (the notification name).
Which I found in an answer posted here, in StackOverflow.
Using that, I can see the notifications that are posted after I choose the "Show" menu item. When I launch from Xcode/Terminal, I see the following notifications posted:
NSApplicationWillUpdateNotification, NSWindowDidUpdateNotification, NSApplicationDidUpdateNotification, ** NSApplicationWillUnhideNotification **, ..., ** NSApplicationDidUnhideNotification **, ..., NSApplicationWillBecomeActiveNotification, ...
NSApplicationWillUnhideNotification is posted in this situation.
When I launch from the Finder, I see the following notifications are posted:
NSApplicationWillUpdateNotification, NSWindowDidUpdateNotification, NSApplicationDidUpdateNotification, NSApplicationWillBecomeActiveNotification, ...
It does not send the NSApplicationWillUnhideNotification. Also, when I select "Show" from the Xcode-launched version, I see -[NSApplication _doUnhideWithoutActivation] in the backtrace. Putting a breakpoint for that function when I attach to the Finder-launched version does not result in a break when I select "Show".
Then, I thought to myself, perhaps the application thinks that it is not hidden.
I have a idle event handler, so from there I printed out the value of [[NSApplication sharedApplicaton] isHidden] while I Hide and "Show" the application.
For the problem situation, when the application is not hidden, it prints out NO for isHidden. When the application becomes hidden, it prints out YES for isHidden. When I select "Show" from the dock menu, it continues to print out NO for isHidden. It knows that it is hidden, but part of the application has been activated: the NSPanels and the NSMenuBar appear.
I can see the document window by entering into the application Exposé mode, and clicking the document window will make the window appear, but the dock menu item is still "Show" and isHidden is still YES.
The unhide mechanism works fine for a sample application, so I'm pretty sure that our code is doing something to shut this off.
I wonder what would be different between an application launched from the Terminal compared to an application launched from the Finder?
I had the application log the environment variables using [[NSProcessInfo processInfo] environment] and the only real difference I could see is that PWD exists in the variables for the Terminal application: I cannot see anything in our code that makes use of that.
I had the application log the command-line arguments via [[NSProcessInfo processInfo] arguments], and I do see something different in the Finder-launched version. Both the Terminal and Finder launched versions list the path of the binary as the first argument; the Finder also lists a second paramter, "-psn_0_89445704". I have read online that it is something that Mac OS X adds to command-line arguments for GUI applications and I see it added to the command-line arguments for other applications that Hide and Show properly from the Dock menu.
Do you have any other thoughts that may lead me further towards solving this mystery? Thanks for any help or suggestions!
After working with Apple Engineers in AppKit, a solution has been found.
In our application, we "flush" the event queue for various reasons via this method:
NSEvent* lastEvent = [NSEvent otherEventWithType:NSPeriodic
location:NSMakePoint(0.0, 0.0)
modifierFlags:0
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:1
context:NULL
subtype:0
data1:0
data2:0];
[[NSApplication sharedApplication] discardEventsMatchingMask:NSAnyEventMask beforeEvent:lastEvent];
The Mac OS X systems sends a "Show" event to the application on launch. Our flush function, which is called upon launch, effectively removes that event from the queue, but the core process part of Mac OS X has its own internal queue that keeps track of show and hide and other types of event types so that it doesn't send repeated messages. (I'll be investigating if this flush is really necessary)
The problem is that when discardEventsMatchingMask:NSAnyEventMask is called on every event, it clears out the events for the application, but doesn't respond to the core process's show event and so core process thinks that it doesn't need to send the show event again.
The solution to this particular problem is to be more selective in which events are cleared. With my new implementation, I do not clear events that will be sent by core process.
/* a bug in Apple's Core Process group forces me to isolate which events should be cleared as
show|hide|activate|deactivate messages get sent by Core Process, but are not _marked_ as
handled and so Core Process thinks that the "Show" event is still pending and will not send
another */
NSEvent* lastEvent = [NSEvent otherEventWithType:NSPeriodic
location:NSMakePoint(0.0, 0.0)
modifierFlags:0
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:1
context:NULL
subtype:0
data1:0
data2:0];
NSEventMask maskForEventsToDiscard = (NSPeriodic |
NSLeftMouseDown |
NSLeftMouseUp |
NSMouseMoved |
NSLeftMouseDragged |
NSRightMouseDragged |
NSMouseEntered |
NSMouseExited |
NSKeyDown |
NSOtherMouseDown |
NSOtherMouseUp |
NSOtherMouseDragged);
[[NSApplication sharedApplication] discardEventsMatchingMask:maskForEventsToDiscard
beforeEvent:lastEvent];
As the "show" event is not cleared on launch, show and hide work now!
A special thanks to KF of Apple!
This technique won't fit in a comment, but I might suggest getting up-close-and-personal with DTrace. I suggested in the comments above to subclass NSWindow and put NSLog statements in the -orderOut:, etc. methods. However, using DTrace for this would likely be far more effective - although, as you will see, it will still be useful to know the address of the objects you will be observing - the upside is that you won't litter your code with a bunch of NSLog statements.
The simplest script might be:
#pragma D option quiet
objc$target:NSWindow:-orderOut?:entry
{
printf( "%30s %10s %x %x\n", probemod, probefunc, arg0, arg1 );
}
and would be called with the process id of the application by doing something like:
sudo dtrace -s dtrace_window.d -p9434
In this particular case, arg0 will contain the address of the window. Unfortunately, it is apparently non-trivial to obtain the title of the window or even the contents of a NSString from within DTrace, but may be worth the effort. I do have a question here and here to see if anyone knows to do either one of these things. (If you can get the title of window, you can set up a map from the address of the window to a string.)
It would be easy to attach probes to any and all methods, functions, etc. you think might be involved so you can try to "follow the event" to resolve this problem.
So, ultimately, I would suggest to just keep adding DTrace probes until something provides the required hint resolve this problem.

Xcode cocoa - dynamically change the checkbox bool

I want to write test_application for MacOS which will show whether IM client is running. This test_application also will have ability to start and kill IM Messenger (checkbox on/off)
I understand how to run / kill application with the help of push-buttons and now i want to show status of IM (is it running or not) and show checkbox ON or OFF depending on it
I suppose, i need to use some system call like "ps -aux processname" and parse it or use some API from Cocoa - but i can't understand how to get that information to test_application and how to do it outside any method (i want test_application to load initial information at it start, so if i open test_application it looks whether IM Messenger is running and makes checkbox ON or OFF without any clicks)
You can have a look at GetBSDProcessList and the Process Manager Reference to get running processes and daemons.
You could also use NSAppleScript and some AppleScript to launch the application or NSTask along with ps -aux processname and grep

Why do AppleScript "tell" commands run a non-GUI instance of my GUI application in the background?

I'm writing a standard Cocoa application, and I've just started implementing AppleScript support for it. I've defined and implemented a property called testprop for my top-level application scripting class, and accessing it works just fine: if I launch an instance of my app and run the following AppleScript in Script Editor, I get the output I expect:
tell application "MyApp"
testprop
end tell
However if I run this very same AppleScript in the Script Editor when my app is not running, it returns the last known value for this property, and continues to return it for subsequent calls. I don't see an instance of my app getting started anywhere in the GUI.
After I noticed this, I ran "ps xawww | grep MyApp" in the shell, which told me that a process had been created using my app's main executable, with an argument that looks something like this: -psn_0_323663 (the number at the end changes each time this process is started -- I gather that it's the "process serial number" that AppleScript (among others) uses to keep track of and control processes).
What is going on here? How can I prevent this from happening (i.e. launch my app as a full, proper GUI-enabled instance when AppleScript "tell" commands for it are run)?
Edit:
The above seems to occur only on my laptop. When I try exactly the same thing on my Mac Mini (the OS version is the same on both: 10.5.8), I simply get an error message:
$ osascript -e "tell application \"MyApp\"" -e "testprop" -e "end tell"
26:40: execution error: The variable testprop is not defined. (-2753)
I don't think it's running a "non-GUI" instance, it just hides the app. You could add the line "activate" to the applescript to get the app to become active, in which case you'll see the windows and menu.