As part of an automation workflow I'd like to be able to store a reference to an applescript object inside an NSMutableDictionary. This is done, in effect, to be able to use variable variable names. Keeping an NSAppleScript instance alive, I'd then be able to refer back to previous results and continue from where I left off.
Concretely, I'd like to store a Presentation object (Microsoft PowerPoint) inside an NSMutableDictionary.
use framework "Foundation"
property memory : null
set memory to current application's NSMutableDictionary's dictionary()
-- function to get a ref to a presentation based on either a NAME or a PATH
on getPresentation(desc)
tell application "Microsoft PowerPoint"
if (item 1 of desc) is "" then
return item 1 of (get presentations whose path is (item 2 of desc))
else
return item 1 of (get presentations whose name is (item 1 of desc))
end if
end tell
end getPresentation
-- Fetch a currently open presentation (just an example)
-- this exists if you open a new presentation in PPT (no saving) and enter as title test
set pres to getPresentation({"test [Autosaved]", null})
memory's setObject:pres forKey:"presentation"
This throws the following error:
error "Can’t make «class pptP» 2 of application \"Microsoft PowerPoint\" into the expected type." number -1700 from «class pptP» 2
I'd like to be able to store the object pres for future use, refering to it from the containing Objective-C code (through NSAppleScript, calling methods on pres through wrappers as needed). I can tell the applescript to store the result of some handler at some address and use it later on.
I've tried using a reference to pres to solve the issue, but no luck.
Is there a way to solve this? Perhaps a wrapping class around pres that allows it to be stored inside the NSMutableDictionary? Perhaps I can roll my own Objective-C code that would work on pres?
Any help would be much appreciated!
AppleScript’s reference type is not bridged to ObjC so cannot cross the bridge directly. You can wrap it in a script object (as long as that object inherits from NSObject) and pass that over, though in past experience I found creating and using large numbers of bridged script objects also causes ASOC to crash.
Easiest solution is to keep AS references on the AS side, and store them a native data structure such as a key-value list, binary tree, or hash list. (e.g. My old Objects library provides reasonably fast DictionaryCollection (hash list) objects created for exactly this reason.)
Related
I am facing issues in performing certain actions based on the value in the excel file cell data.
Actions like if value is "NORMAL" then click Container type = Normal (radio button)
Similarly the Unit Container Value
Following is my code:
I am getting this error while performing action .WebElement("Container_Type_Normal").Click
Your error is because you can't start a line with . unless your within a with - and i can't see a with in your function. (i also don't recommend using a with as the can cause needless confusion)
The .webelement object is a catch-all type of object that is the child of other web objects or .page. You need this to be a full and valid path to the object, by this i mean start with browser().page().
You have a couple of options:
You can either make this a full path to your object based on the object repository:
Browser("<<OR Browser name>>").Page("<<OR Page name>>").WebElement("<<Your webelement name>>".click
For this, look at your OR and insert your names.
Or, option 2, you can use descriptive programming:
Browser("CreationTime:=0").Page("index:=0").WebElement("text:=" & fieldValue,"index:=0").click
That will take the browser that was created first (Creation time 0), the only page it has and the first (index 0) web element that contains your text (text is field value).
I'm assuming that you only have one browser, and that the first element that contains the text you want is what you want to click. You may need to add more layers too this or
A good approach is to mirror what is OR or use the object spy to ensure these properties are correct.
I’m experimenting with scripting a batch of OmniFocus tasks in JXA and running into some big speed issues. I don't think the problem is specific to OmniFocus or JXA; rather I think this is a more general misunderstanding of how getting objects works - I'm expecting it to work like a single SQL query that loads all objects in memory but instead it seems to do each operation on demand.
Here’s a simple example - let’s get the names of all uncompleted tasks (which are stored in a SQLite DB on the backend):
var tasks = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})
var totalTasks = tasks.length
for (var i = 0; i < totalTasks; i++) {
tasks[i].name()
}
[Finished in 46.68s]
Actually getting the list of 900 tasks takes ~7 seconds - already slow - but then looping and reading basic properties takes another 40 seconds, presumably because it's hitting the DB for each one. (Also, tasks doesn't behave like an array - it seems to be recomputed every time it's accessed.)
Is there any way to do this quickly - to read a batch of objects and all their properties into memory at once?
Introduction
With AppleEvents, the IPC technology that JavaScript for Automation (JXA) is built upon, the way you request information from another application is by sending it an "object specifier," which works a little bit like dot notation for accessing object properties, and a little bit like a SQL or GraphQL query.
The receiving application evaluates the object specifier and determines which objects, if any, it refers to. It then returns a value representing the referred-to objects. The returned value may be a list of values, if the referred-to object was a collection of objects. The object specifier may also refer to properties of objects. The values returned may be strings, or numbers, or even new object specifiers.
Object specifiers
An example of a fully-qualified object specifier written in AppleScript is:
a reference to the name of the first window of application "Safari"
In JXA, that same object specifier would be expressed:
Application("Safari").windows[0].name
To send an IPC request to Safari to ask it to evaluate this object specifier and respond with a value, you can invoke the .get() function on an object specifier:
Application("Safari").windows[0].name.get()
As a shorthand for the .get() function, you can invoke the object specifier directly:
Application("Safari").windows[0].name()
A single request is sent to Safari, and a single value (a string in this case) is returned.
In this way, object specifiers work a little bit like dot notation for accessing object properties. But object specifiers are much more powerful than that.
Collections
You can effectively perform maps or comprehensions over collections. In AppleScript this looks like:
get the name of every window of Application "Safari"
In JXA it looks like:
Application("Safari").windows.name.get()
Even though this requests multiple values, it requires only a single request to be sent to Safari, which then iterates over its own windows, collecting the name of each one, and then sends back a single list value containing all of the name strings. No matter how many windows Safari has open, this statement only results in a single request/response.
For-loop anti-pattern
Contrast that approach to the for-loop anti-pattern:
var nameOfEveryWindow = []
var everyWindowSpecifier = Application("Safari").windows
var numberOfWindows = everyWindowSpecifier.length
for (var i = 0; i < numberOfWindows; i++) {
var windowNameSpecifier = everyWindowSpecifier[i].name
var windowName = windowNameSpecifier.get()
nameOfEveryWindow.push(windowName)
}
This approach may take much longer, as it requires length+1 number of requests to get the collection of names.
(Note that the length property of collection object specifiers is handled specially, because collection object specifiers in JXA attempt to behave like native JavaScript Arrays. No .get() invocation is needed (or allowed) on the length property.)
Filtering, and why your code example is slow
The really interesting part of AppleEvents is the so-called "whose clause". This allows you provide criteria with which to filter the objects from which the values will be returned from.
In the code you included in your question, tasks is an object specifier that refers to a collection of objects that have been filtered to only include uncompleted tasks using a whose clause. Note that this is still just reference at this point; until you call .get() on the object specifier, it's just a pointer to something, not the thing itself.
The code you included then implements the for-loop anti-pattern, which is probably why your observed performance is so slow. You are sending length+1 requests to OmniFocus. Each invocation of .name() results in another AppleEvent.
Furthermore, you're asking OmniFocus to re-filter the collection of tasks every time, because the object specifier you're sending each time contains a whose clause.
Try this instead:
var taskNames = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false}).name.get()
This should send a single request to OmniFocus, and return an array of the names of each uncompleted task.
Another approach to try would be to ask OmniFocus to evaluate the "whose clause" once, and return an array of object specifiers:
var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
Iterating over the returned array of object specifies and invoking .name.get() on each one would likely be faster than your original approach.
Answer
While JXA can get arrays of single properties of collections of objects, it appears that due to an oversight on the part of the authors, JXA doesn't support getting all of the properties of all of the objects in a collection.
So, to answer you actual question, with JXA, there is not a way to read a batch of objects and all their properties into memory at once.
That said, AppleScript does support it:
tell app "OmniFocus" to get the properties of every flattened task of default document whose completed is false
With JXA, you have to fall back to the for-loop anti-pattern if you really want all of the properties of the objects, but we can avoid evaluating the whose clause more than once by pulling its evaluation outside of the for loop:
var tasks = []
var taskSpecifiers = Application('OmniFocus').defaultDocument.flattenedTasks.whose({completed: false})()
var totalTasks = taskSpecifiers.length
for (var i = 0; i < totalTasks; i++) {
tasks[i] = taskSpecifiers[i].properties()
}
Finally, it should be noted that AppleScript also lets you request specific sets of properties:
get the {name, zoomable} of every window of application "Safari"
But there is no way with JXA to send a single request for multiple properties of an object, or collection of objects.
Try something like:
tell app "OmniFocus"
tell default document
get name of every flattened task whose completed is false
end tell
end tell
Apple event IPC is not OOP, it’s RPC + simple first-class relational queries. AppleScript obfuscates this, and JXA not only obfuscates it even worse but cripples it too; but once you learn to see through the faux-OO syntactic nonsense it makes a lot more sense. This and this may give a bit more insight.
[ETA: Omni recently implemented its own embedded JavaScriptCore-based scripting support in its apps; if JS is your thing you might find that a better bet.]
A previous programmer has created a form with a control array, containing the following controls:-
Command1(0)
Command1(1)
Command1(2)
and I am attempting to replace them with
cmdMeaningfulName
cmdOtherMeaningfulName
cmdThirdMeaningfulName
So far I have managed to rename the controls. This leaves me, however, with a set of controls:-
cmdMeaningfulName(0)
cmdOtherMeaningfulName(1)
cmdThirdMeaningfulName(2)
I can fiddle with the Index properties to get:-
cmdMeaningfulName(0)
cmdOtherMeaningfulName(0)
cmdThirdMeaningfulName(0)
but this still leaves a control array, resulting in methods like
cmdMeaningfulName(Index As Integer)
being generated (or required). Later - these methods don't actually compile, being reported as
Member already exists in a object module from which this object module derives.
when it clearly doesn't.
How does one remove the index entirely? I have tried editing the .frm by hand and no trace of any index can be found there.
On the form, select the control, then go to the properties window (F4). You can then select the index property and clear it. The control is then no longer an element of an array. This also means that any event handlers (_click, etc) are no longer hooked up, so you'll need to copy/re-implement those.
In my scriptable app, one of the properties is of a named record type, and that record type has been declared in the sdef as well (named "custom record").
I can get the record like this:
get owner of anElement
--> {pool:"test", position:2}
I can also successfully test for it like this:
set target to {pool:"test", position:2}
if owner of anElement = target then
-- found!
But I cannot use it in a whose clause:
get allElements whose owner = target
--> {}
I also cannot use missing value in the test:
get allElements whose owner = missing value
--> error number -1700 from missing value to custom record
Is that expected behavior with AppleScript, i.e. is it unable to handle records in whose clauses?
Or am I doing something wrong? I have so far not implemented any coercion handlers nor special record handlers because nothing has indicated that I'd need them.
Also, please see my related question: Cocoa Scripting: Returning "null" vs. "missing value"
Short answer : It's expected behavior.
The whose clause works only for element reference types (classes which have an object specifier), but not for record types and custom lists.
Even the selection property of the Finder cannot be filtered by a whose clause.
In my project I need to be able to tell the difference between documents created by the user and those restored at application launch by restoreStateWithCoder because there are some thing s that need to be done for new documents, but not restored ones. How can I do this?
How about subclassing "NSDocument" and using that subclass for your document?
Then, you can catch "restoreStateWithCoder" as it happens and set a unique flag (e.g. a BOOL property) for those documents that are restored from disk and not created fresh via "File -> New" command.
You can also attempt to "method swizzle" "restoreStateWithCoder", but you have to decide what property to set in which object.
[Answering this for Swift, but the general idea works for Objective-C as well]
When a document is brand new, you generally get a call to the following function:
convenience init(type tyepName: String) throws
You could set a flag in that function (say needSpecialHandling = true, a variable which is originally initialised to false) to say whether you need some special handling for such cases.
Then in the makeWindowControllers() function you use that variable to trigger invoking the special code (if true) the same way you invoked it possibly in the windowControllerDidLoadNib function.