Converting AppleScript to SwiftAutomation - macos

This AppleScript that I found here does what I want:
tell application "iTunes"
set matchtrack to tracks in playlist 1 whose persistent ID is "C70EA9CDC276CB6D"
if matchtrack is not {} then
return name of item 1 of matchtrack
else
return "no track found"
end if
end tell
That is, finds a track based on the persistent ID. I'm trying to get it to work in a MacOS Cocoa Swift application using the Swift Automation as an Apple Event Bridge. I can retrieve the value with:
let trackID = try iTunes.tracks[1].persistentID.get()
I've tried all sorts of statements. This one seems to show the most promise:
let trackRow = try iTunes.tracks[ITUItem.persistentID == "C70EA9CDC276CB6D"].get() as ITUItem
When I run it, I get the error:
Instance member 'persistentID' cannot be used on type 'ITUItem'
The framework is in beta at the best so it may be a bug. Any suggestions on what else to try? I'd ask the author, but I can't find a way to contact him.
Here is part of the sdef file that was generated by the app.
item n : an item
properties
class_ (type, r/o) : the class of the item
container (specifier, r/o) : the container of the item
id (integer, r/o) : the id of the item
index (integer, r/o) : The index of the item in internal application order.
name (text) : the name of the item
persistentID (text, r/o) : the id of the item as a hexadecimal string. This id does not change over time.
properties (record) : every property of the item
Track is a subclass of item.
Fix
When I first tried #matt's suggestion of ITUIts.persistentID, it wouldn't compile. I got the error:
Binary operator '==' cannot be applied to operands of type 'ITUItem' and 'String'
After some back and forth, I realized the problem was that I was missing an import:
import SwiftAutomation
I had it in originally and wasn't sure I needed it so I commented it out. A dozen other calls to it worked fine without it before I got to this one.

Use ITUIts and get rid of as ITUItem. So:
let trackRow = try itunes.tracks[ITUIts.persistentID == "C70EA9CDC276CB6D"].get()
// result will be something along these lines:
// [ITunes().sources.ID(66).libraryPlaylists.ID(93639).fileTracks.ID(95018)]
Herewith, a complete working test along with the results on my machine (of course your numbers will be different):
let itunes = ITunes()
let trackID = try itunes.tracks[1].persistentID.get() as String
print("track ID is", trackID)
// track ID is 689006177BB39343
let trackRows = try itunes.tracks[ITUIts.persistentID == trackID].get() as [ITUItem]
if let trackRow = trackRows.first {
print(trackRow)
// ITunes().sources.ID(66).libraryPlaylists.ID(93639).fileTracks.ID(95018)
}

Related

JXA: get containers of an element

Using JavaScript for automation in macOS, it's straightforward to get the elements of a container. But I can't figure out how to get the containers of an element.
In the Photos documentation, for example, an Album "contains mediaItems"; and a MediaItem is "contained by albums".
This works:
Application('Photos').albums[0].mediaItems()
This is what I want to write, but does not work:
Application('Photos').mediaItems[0].albums()
(resulting error:
Error: Can't get object. (-1728)
)
I've also tried to do something with the whose method, but I'm not quite sure how to write it:
Application('Photos').albums.whose({ /* what to put here? */ })
(Obviously I'd rather use the more direct route, if it exists, but if the proper way to do what I want is via whose, okay.)
I don’t think there’s a direct answer to this question. What you’re basically looking for, if I read you correctly, is, how can I use JXA to query a container based on its elements. That is, you want the answer to how to perform this AppleScript query in JavaScript:
tell application "Photos"
--get an arbitrary photo
set firstPhoto to the first media item
--get the albums that contain that photo
get the name of every album whose id of media items contains id of firstPhoto
end tell
This means going multiple levels deep, something like:
//this does not work
var firstPhoto = Application('Photos').mediaItems[0]();
var containingAlbums = Application('Photos').albums.whose({mediaItems: {_contains: firstPhoto}});
But according to the error this script generates, the albums object doesn’t even have a property called “mediaItems”.
Tantalizingly, if you were to run the following script you would see the ids of each of the albums that contains your photo:
//get a photo
var firstPhoto = Application('Photos').mediaItems[0]();
var firstPhotoId = firstPhoto.id();
albumQuery = Application('Photos').albums.mediaItems.where({id: {_equals: firstPhotoId}});
containingAlbums = []
for (var possibleAlbum of albumQuery[0]()) {
if (possibleAlbum != null) {
containingAlbums.push(possibleAlbum);
}
}
containingAlbums;
I see, for example,:
[Application("Photos").albums.byId("RLf9PUOxSLunpY5vFLLR6A").mediaItems.byId("68IM5jaiRDqIJhcKVBXo%w"),
Application("Photos").albums.byId("7QpA6wQrSEeIPyhu8xHlOw").mediaItems.byId("68IM5jaiRDqIJhcKVBXo%w"),
Application("Photos").albums.byId("SX8PbxO9S+a4%w4FvHH%Og").mediaItems.byId("68IM5jaiRDqIJhcKVBXo%w")]
But if I change the push line to containingAlbums.push(possibleAlbum.properties()); I see no property that will get the album name or even id back in any of the entries.
I asked a similar question about getting people from the same city in Contacts. The only solution I’ve been able to find is to get your media item and then loop through all albums.
//get an arbitrary photo
var firstPhoto = Application('Photos').mediaItems[0]();
var firstPhotoId = firstPhoto.id();
//loop through all albums and compile list of those that contain this photo
var containingAlbums = []
for (var possibleAlbum of Application('Photos').albums()) {
if (possibleAlbum.mediaItems.whose({id: {_equals: firstPhotoId}}).length) {
containingAlbums.push(possibleAlbum.name());
}
}
containingAlbums;
This is obviously not a satisfactory answer. I’m providing it in the hope that it may help you hack up a solution, and that someone will put up a better answer to prove me wrong.
In general, JXA support is spotty enough that if you don’t need one of its features that AppleScript doesn’t have, such as the ability to chain queries programmatically, it’s better to build the solution in AppleScript.

Add one spot to a value

Its possible to book appointments in my app and I get the queue line value from this code to display for the users in the app:
Text = $"Spot {_Class.Slots.WaitList}";
What im trying to get here is to show the queue that person has. The issue is that the value im getting from that line is the value that says how many people there are in the line. So if a person books an appointment the value has to increase from e.g 2 to 3.
EDIT:
The value im getting from this code I get from the database:
Text = $"Spot {_Class.Slots.WaitList}";
The value is 1 (just an example) but i need the value to be +1 every time.
I tried to do this:
Text = $"Spot {_Class.Slots.WaitList + "1"}";
But it added 10 instead of 1.
What's the type of WaitList field/property?
I don't see a reason why this wouldn't work:
Text = $"Spot {_Class.Slots.WaitList + 1}";

JavaEdit search function not locating visible element

I've got two library functions:
Function searchWindow(title)
Set searchWindow = Window("title:=" + title)
End Function
And
Function searchField(label)
Set searchField = JavaEdit("attached text:=" + label)
End Function
Here I'm testing them:
Environment.Loadfromfile("C:\UFTConstants\constants.ini")
Set loginFrame = searchWindow(Environment.Value("frameLogin"))
loginFrame.Click
Set userField = searchField("User ID / Ci-Usager")
userField.Set "test"
The first function works fine, and it's title property matches that of the application. However, the second will not find the text field, despite properties matching:
The error:
I've tried other properties as well, tagname, various class properties, as well as combinations of all three, and none are producing a find.
Any ideas?
First Update
As per request, full spy screencap:
The full line generated by the recording tool:
JavaWindow("Application Name").JavaDialog("Window Title").JavaEdit("User ID / Ci-Usager").Set "user"
However, when I try to re-create this programmatically, I get the same error, only for JavaWindow instead:
"Cannot identify the object [JavaWindow] of (class JavaWindow)..."
Possible Java set-up issue? This would not explain why recording can still locate Java objects, however.
Second Update
Here are the recognition properties:
I have ensured that all properties are set, still unable to locate.
Final Update
Ok, I've reduced the code to absolute barebones. No external constant file, no external library calls. I've copied the full extent of what is recorded in recording mode. I've printed each variable to ensure accuracy. I've included the full object hierarchy:
Set objWin = JavaWindow("label:=<redacted>")
objWin.SetTOProperty "to_class", "JavaWindow"
objWin.SetTOProperty "toolkit class", "javax.swing.JFrame"
MsgBox objWin.GetTOProperty("label")
MsgBox objWin.GetTOProperty("to_class")
MsgBox objWin.GetTOProperty("toolkit class")
Set objDialog = objWin.JavaDialog("label:=<redacted>")
objDialog.SetTOProperty "to_class", "JavaDialog"
objDialog.SetTOProperty "toolkit class", "<redacted>.LoginDialog"
MsgBox objDialog.GetTOProperty("label")
MsgBox objDialog.GetTOProperty("to_class")
MsgBox objDialog.GetTOProperty("toolkit class")
Set objEdit = objDialog.JavaEdit("attached text:=User ID / Ci-Usager")
objEdit.SetTOProperty "to_class", "JavaEdit"
objEdit.SetTOProperty "toolkit class", "javax.swing.JTextField"
MsgBox objEdit.GetTOProperty("attached text")
MsgBox objEdit.GetTOProperty("to_class")
MsgBox objEdit.GetTOProperty("toolkit class")
objEdit.Set "test"
Note the redacted text is to remove identifying elements from the code. They have been triple-checked on my side and are correct.
This still does not work.
However, recording the same does. What gives?
I think you have to mention the full hierarchy while working with the Javaedit field. Try re-writing the code for the function searchField as :
Function searchField(label)
Dim objFrame
Set objFrame = searchWindow(Environment.Value("frameLogin"))
Set searchField = objFrame.JavaEdit("attached text:=" + label) 'Javaedit should be the child of the login window. You had to mention the full hierarchy here
End Function

Cocoa Scripting: Use special string types like raw data

My app has some raw data content I want to be able to offer to AppleScript so that it can be at least looked at, if not even handled by saving it to a file or setting it to some other object that supports it.
Now, I don't understand which data type is used to accomplish that.
See this output from Script Editor, for instance:
tell application "Script Editor"
the clipboard as record
--> {Unicode text:"text",
«class BBLM»:«data BBLM6C6C756E»,
string:"text"}
end tell
How do I return these «data ...», which are apparently a combination of a 4-char-code and hex-string-encoded bytes of the actual data.
I've tried returning an NSData object containing the raw bytes data from my scriptable property, but that doesn't work.
Update
It appears it has to do with implementing scripting<type>Descriptor and scripting<type>WithDescriptor. I cannot find any documentation on this other than it being used in the Sketch sample code. I assume these will be invoked for the type if I happen to define such a custom type in my Sdef.
However: I will not know the types I want to send in advance, so I cannot pre-define them in the Sdef. I'm more in the situation similar to the clipboard: I have clipboard-like data I want to return, so I only know their 4-char-types at runtime. Which means I won't be asked through these handlers. There must be some other way to generically create and receive these types, the same way the clipboard implementation does it.
RE: "...implementing scripting<Key>Descriptor and scripting<Key>WithDescriptor. I cannot find any documentation on this..."
The first place to start is "Key-Value Coding and Cocoa Scripting" section in the Cocoa Scripting Guide (2008). There are a whole slew of these methods that embed the type in the method name. Many are also documented in the Foundation's NSScriptKeyValueCoding Protocol Reference page, but you have to read the "Discussion" section to find them. For instance, in:
- (id)valueWithUniqueID:(id)uniqueID inPropertyWithKey:(NSString *)key
the discussion says: "The method valueIn<Key>WithUniqueID: is invoked if it exists."
So in a Widgets class, you would implement valueInWidgetsWithUniqueID:
scripting<Key>Descriptor and scripting<Key>WithDescriptor are special conversion handlers that are used when you use a element in your application's .sdef, which is why they show up in Sketch to handle the typeRGBColor data type, a list of 3 integers. I can't find these documented outside of the Sketch code either, but I can confirm that
scriptingRGBColorDescriptor
is called by methods in:
NSObject(NSScriptAppleEventConversion)
NSAppleEventDescriptor(NSScriptConversion)
RE: "However: I will not know the types I want to send in advance, so I cannot pre-define them in the Sdef."
There is a way to solve that problem: you can return a special list structure known as a User-Field Record (typeUserField). This record includes alternating Key and Value descriptors, and does not require anything to be defined in the SDEF.
Here's an item I posted on the ASOC mailing list last year:
http://lists.apple.com/archives/applescriptobjc-dev/2015/Jan/msg00036.html
And here's the code (using AppleScript-ObjectiveC code) to build the typeUserField record from an NSDictionary.
# ASOC implementation of - (NSAppleEventDescriptor *)scriptingRecordDescriptor for an NSDictionary
# Creates an empty record descriptor and an empty list descriptor, then
# Iterates over the dictionary and inserts descriptors for each key and each value into the list descriptor
# Finally, populates the record descriptor with the type 'usrf' and the list descriptor
on makeUserRecordDescriptor(aDict)
log aDict
set recordDescriptor to aedClass's recordDescriptor()
set listDescriptor to aedClass's listDescriptor()
set typeUserField to 1970500198 -- 'usrf'
set itemIndex to 1 -- AS records are 1-based
repeat with aKey in aDict's allKeys()
set aVal to aDict's valueForKey_(aKey)
-- The values can be several different types. This code DOES NOT handle them all.
set isStringValue to aVal's isKindOfClass_(nssClass's |class|) = 1
set isNumericValue to aVal's isKindOfClass_(nsnClass's |class|) = 1
set isBooleanValue to aVal's className()'s containsString_("Boolean") = 1
-- Insert a descriptor for the key into the list descriptor
set anItem to aedClass's descriptorWithString_(aKey)
listDescriptor's insertDescriptor_atIndex_(anItem, itemIndex)
set itemIndex to itemIndex + 1
-- Insert a descriptor (of the correct type for the value) into the list descriptor
if isStringValue
set anItem to aedClass's descriptorWithString_(aVal)
else if isBooleanValue
set anItem to aedClass's descriptorWithBoolean_(aVal's boolValue())
else if isNumericValue
set intValue to aVal's intValue()
set fpValue to aVal's doubleValue()
if intValue = fpValue
set anItem to aedClass's descriptorWithInt32_(aVal's intValue())
else
set anItem to aedClass's descriptorWithString_(aVal's stringValue) # TODO: 'doub'
end
else
set anItem to aedClass's descriptorWithString_("Unhandled Data Type")
end
listDescriptor's insertDescriptor_atIndex_(anItem, itemIndex)
set itemIndex to itemIndex + 1
end
recordDescriptor's setDescriptor_forKeyword_(listDescriptor, typeUserField)
return recordDescriptor
end
The magic lies in using NSAppleEventDescriptor. It offers a lot of initializers. It's the one that eventually holds any value that gets passed back to the calling AppleScript (or JXA or whatever uses the Scripting engine).
Apparently, any value returned to the Cocoa Scripting layer, such as strings as NSString and numeric values as NSNumber, end up being assign to a NSAppleEventDescriptor object, and converted by that step to the AppleEvent-internal format.
So, if I want to return a string of bytes, e.g. stored in an NSData object, all I have to do is this from my property method:
-(id)returnSomeBytes {
return [NSAppleEventDescriptor descriptorWithDescriptorType:'Raw ', myNSDataObject];
}
This will end up in AppleScript as «data Raw ...».
I now also understand why the scripting engine won't automatically convert NSData for me: It needs a type code, which NSData doesn't inherit.
The reverse works just as well - any such raw data gets passed to my code as a NSAppleEventDescriptor, which I can then decode accordingly.

How to print validation error outside of field constructor in Play framework 2

How can I show a validation error for a form field outside of a field constructor in Play framework 2? Here is what I tried:
#eventForm.("name").error.message
And I get this error:
value message is not a member of Option[play.api.data.FormError]
I'm confused because in the api docs it says message is a member of FormError. Also this works fine for global errors:
#eventForm.globalError.message
You can get a better grasp of it checking Form's sourcecode here
Form defines an apply method:
def apply(key: String): Field = Field(
this,
key,
constraints.get(key).getOrElse(Nil),
formats.get(key),
errors.collect { case e if e.key == key => e },
data.get(key))
That, as said in the doc, returns any field, even if it doesn't exist. And a Field has an errors member which returns a Seq[FormError]:
So, you could do something like that (for the Seq[FormError]):
eventForm("name").errors.foreach { error =>
<div>#error.message</div>
}
Or (for the Option[FormError])
eventForm("name").error.map { error =>
<div>#error.message</div>
}
Or, you could use Form errors:
def errors(key: String): Seq[FormError] = errors.filter(_.key == key)
And get all errors of a given key. Like this (for the Seq[FormError]):
eventForm.errors("name").foreach { error =>
<div>#error.message</div>
}
Or (for the Option[FormError])
eventForm.error("name").map { error =>
<div>#error.message</div>
}
If you want more details, check the source code. It's well written and well commented.
Cheers!
EDIT:
As biesior commented: to show human readable pretty messages with different languages you have to check how play works I18N out here
To be thorough you're probably going to have to deal with I18N. It's not hard at all to get it all working.
After reading the documentation you may still find yourself a bit consufed. I'll give you a little push. Add a messages file to your conf folder and you can copy its content from here. That way you'll have more control over the default messages. Now, in your view, you should be able to do something like that:
eventForm.errors("name").foreach { error =>
<div>#Messages(error.message, error.args: _*)</div>
}
For instance, if error.message were error.invalid it would show the message previously defined in the conf/messages file Invalid value. args define some arguments that your error message may handle. For instance, if you were handling an error.min, an arg could be the minimum value required. In your message you just have to follow the {n} pattern, where n is the order of your argument.
Of course, you're able to define your own messages like that:
error.futureBirthday=Are you sure you're born in the future? Oowww hay, we got ourselves a time traveler!
And in your controller you could check your form like that (just one line of code to show you the feeling of it)
"year" -> number.verifying("error.furtureBirthday", number <= 2012) // 2012 being the current year
If you want to play around with languages, just follow the documentation.
Cheers, again!
As you said yourself, message is a member of FormError, but you have an Option[FormError]. You could use
eventForm("name").error.map(_.message).getOrElse("")
That gives you the message, if there is an error, and "" if there isn't.

Resources