Building OS X Applications Using JavaScript

Original author: Tyler Gaw
  • Transfer
Recently, OS X Yosemite introduced the ability to use JavaScript for automation. This opens up the possibility of accessing native * OS X frameworks from JavaScript. I rummaged around in this new world and collected several examples . In this post I will explain the basics and step by step show the process of creating a small application . JavaScript
session took place at WWDC 2014. It told you that you can now use JavaScript to automate applications instead of AppleScript. This in itself is exciting news. The ability to automate repetitive operations using AppleScript has been around for quite some time. Writing in AppleScript is not a pleasant experience, so using familiar syntax instead would be very good.
During this session, the speaker talked about the bridge with Objective-C. This is where the fun begins. The bridge allows you to import any Objective-C framework into a JS application. For example, if you want to write a GUI using standard OS X controls, you need to import Cocoa:
ObjC.import("Cocoa"); 

The Freymork Foundation does exactly what its name suggests. It allows you to assemble blocks for OS X applications. It has a huge set of classes and protocols. NSArray , NSURL , NSUserNotification , etc. You may not be familiar with all of them, but their names suggest what they are for. Because of its extreme importance, the framework is available by default, without the need to import into a new application.
As far as I can tell, you can do the same in JavaScript as in Objective-C or Swift.

Example


Note: for this example to work, you need Yosemite Developer Preview 7+. The
best way to learn something is to just take it and try to do something. I am going to show you the process of creating a small application that can display images from a computer.
You can download the full example from my repository .

Screen of the application I'm going to write.

In the application there will be: a window, a text block, an input field and a button. Well, or by class names: NSWindow , NSTextField , NSTextField and NSButton .
Clicking the “File Selection” button will open NSOpenPanelto select a file. We will configure the panel so that it does not allow the user to select files with an extension other than .jpg, .png and .gif.
After selecting the image, we will show it in the window. The window will adjust its size to the width and height of the image plus the height of the controls. We will also indicate the minimum window sizes so that the controls do not disappear.

Project setup


Open the Apple Script Editor app in Applictions> Utilities . This is not the best editor that I have tried, but now it is needed. There are a number of necessary features for building an OS X application on JS. Not sure what is going on under the hood, but he knows how to compile and run your scripts as applications. It also creates additional necessary things, such as the Info.plist file. It seems to me that there is an opportunity to force other editors to do the same, but I have not figured it out yet.
Create a new document through File> New or cmd + n . The first thing we need to do is save the document as an application. Save it via File> Save or cmd + s. Do not rush to save once. There are two settings that are necessary in order to run the project as an application.

Script Editor with the necessary settings

Change the format to “Application” and check the box “Do not close” **
Important note: you can change these settings: open the File menu and hold down the option button. This will open the “Save As ...” item. In the save dialog, you can make changes to the settings. But it’s better to do this right away when creating a project.
If you do not check the "Do not close" checkbox, then your application will open, and then immediately close. There is almost no documentation on this functionality on the Internet. I found out about this only after several hours beating my forehead on the keyboard.

Let's do something already!


Add the following lines to your script and run everything through Script> Run Application or cmd + r .
ObjC.import("Cocoa");  
$.NSLog("Hi everybody!"); 

Almost nothing happened. The only visible changes are in the menu bar and dock. The name of the application and the File and Edit items appeared in the menu bar. You can see that the application is running, since its icon is now in the dock.
Where is the line “Hi everybody!”? And what is the dollar sign, jQuery? Close the application via File> Quit or cmd + q and let's find out where this NSLog happened.
Open the console: Applications> Utilities> Console . Each application can write something to the console. It is not much different from Developer Tools in Chrome, Safari or Firefox. The main difference is that you are debugging applications instead of sites.
It has a lot of messages. Filter it by writing “applet” in the search bar in the upper right corner. Return to the Script Editor and run the application again through opt + cmd + r .

Have you seen !? The message "Hi everybody!" should appear in the console. If it is not there, close your application and run it again. Many times I forgot to close it and the code did not start.

What about the dollar sign?


The dollar sign is your access to the bridge in Objective-C. Every time you need to access an Objective-C class or constant, you need to use $ .foo or ObjC.foo . Later I will talk about other ways to use $ .
The Console application and NSLog are irreplaceable things, you will always use them for debugging. To learn how to log something other than strings, check out my NSLog example .

Create a window


Let's do something to interact with. Add your code:
ObjC.import("Cocoa");  
var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask; 
var windowHeight = 85; 
var windowWidth = 600;  
var ctrlsHeight = 80;  
var minWidth = 400; 
var minHeight = 340; 
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer( $.NSMakeRect(0, 0, windowWidth, windowHeight), styleMask, $.NSBackingStoreBuffered, false );  
window.center; 
window.title = "Choose and Display Image"; 
window.makeKeyAndOrderFront(window); 

Then run the application. opt + cmd + r . Now let's talk! With a small amount of code, we made an application that can be moved, minimized, and closed.

A simple NSWindow window created using JS

If you, like me, have never had to create applications with Objective-C or Cocoa, all this may look a little crazy. The length of method names seemed to me like that. I like descriptive names, but Cocoa goes too far.
However, this is JavaScript. The code looks like you are writing a website.
What happens in the first lines where we set the value of styleMask? Style masks are used to customize windows. Each option indicates what it adds; title, close button, minimize button. These options are constants. Use bitwise or ("|") to separate one setting from another.
There are many options. You can read about them all in the documentation . Try adding NSResizableWindowMask and see what happens.
You need to remember a few interesting things about syntax. $ .NSWindow.alloc invokes the alloc object NSWindow. Note that there are no parentheses after calling the method. In JavaScript, this gives you access to properties, not methods. How is that? In JS for OS X, brackets are only allowed if you pass parameters. Attempting to use parentheses without arguments will result in a runtime error. If something goes wrong, look in the console for errors.
Here is an example of a super-long method name:
initWithContentRectStyleMaskBackingDefer 

In the documentation for NSWindow, this method looks a little different:
initWithContentRect:styleMask:backing:defer:
In Objective-C, such windows are created in the following way:
NSWindow* window [[NSWindow alloc] 
    initWithContentRect: NSMakeRect(0, 0, windowWidth, windowHeight) 
    styleMask: styleMask, 
    backing: NSBackingStoreBuffered 
    defer: NO
];

Note the colons (":") in the original description of the method. To use the Objective-C method in JS, you need to remove them and replace the next letter with a capital letter. Square brackets ("[]") is a call to a class / object method. [NSWindow alloc] calls the alloc method of the NSWindow class . In JS, this is equivalent to NSWindow.alloc , plus, if necessary, brackets.
I think the rest of the code is quite simple. I will miss her detailed description. To understand what happens next, you will need a lot of time and have to read a lot of documentation, but you can handle it. If you see a window, then it’s already great. Let's do something else.

Add controls


We need a label, text box and button. We will use NSTextField and NSButton . Update your code and run the application again.
ObjC.import("Cocoa"); 
var styleMask = $.NSTitledWindowMask | $.NSClosableWindowMask | $.NSMiniaturizableWindowMask; 
var windowHeight = 85; 
var windowWidth = 600; 
var ctrlsHeight = 80; 
var minWidth = 400; 
var minHeight = 340; 
var window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer( $.NSMakeRect(0, 0, windowWidth, windowHeight), styleMask, $.NSBackingStoreBuffered, false ); 
var textFieldLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24)); 
textFieldLabel.stringValue = "Image: (jpg, png, or gif)"; 
textFieldLabel.drawsBackground = false; 
textFieldLabel.editable = false; 
textFieldLabel.bezeled = false; 
textFieldLabel.selectable = true; 
var textField = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 60), 205, 24));
textField.editable = false; 
var btn = $.NSButton.alloc.initWithFrame($.NSMakeRect(230, (windowHeight - 62), 150, 25)); 
btn.title = "Choose an Image..."; 
btn.bezelStyle = $.NSRoundedBezelStyle; 
btn.buttonType = $.NSMomentaryLightButton; 
window.contentView.addSubview(textFieldLabel); 
window.contentView.addSubview(textField); 
window.contentView.addSubview(btn); 
window.center; 
window.title = "Choose and Display Image"; 
window.makeKeyAndOrderFront(window); 

If everything went well, then now you have a window with controls. Nothing can be entered into the field, and the button does nothing, but wait a minute, we are already moving forward.

Window controls

What have we done here? textFieldLabel and textField are similar to each other. They are both instances of NSTextField . We created them the same way we created the window. When you see initWithFrame and NSMakeRect , then most likely a UI element is created here. NSMakeRect does what is rendered in its name. It creates a rectangle with the specified coordinates and dimensions; (x, y, width, height). The result is what Objective-C calls “structure”. In JS, the equivalent could be an object, a hash, or possibly a dictionary. Key-value pairs.
After creating the text fields, set each handful of properties to get the desired result. Cocoa has nothing like the html label element. So, let's do it by turning off the background and the ability to edit.
We will set the text box programmatically, simultaneously disabling editing. If we did not need this, then we would have managed on one line.
To create the button, we use NSButton . As with the text box, we need a structure. Select two properties: bezelStyle and buttonType. The values ​​of both are constants. These properties determine how the element will be drawn and what styles it will have. See the NSButton documentation for what else you can do with the button. I also have an example showing various styles and types of buttons in action.
The last of the new things we are doing here is adding elements to the window using addSubView . The first time I tried to do this using
window.addSubview(theView)

On other standard views that you create using NSView , this will work, but not with NSWindow instances . Not sure why, but elements need to be added to the contentView in windows . The documentation says: "The topmost available NSView in the window hierarchy." It worked for me.

Making the button work


Pressing the “Select Image” button should open a panel that displays files on the computer. Before that, let's warm up by adding a message to the console when you click on the button.
In JS, event handlers are attached to elements to handle clicks. Objective-C uses a slightly different concept. It uses what is called messaging ***. That is, you pass a message to the object containing the name of the method. The object must have information about what to do when it receives such a message. This may not be the most accurate description, but I understand it.
First, you need to set target and action on the button. Target is the object to which the message contained in the action needs to be sent.. If it’s not clear now, move on, you will understand everything when you see the code. Update the part of the script where the button is configured:
... 
btn.target = appDelegate; 
btn.action = "btnClickHandler"; 
...

appDelegate and btnClickHandler do not exist yet. We must make them. In the following code, order is important. I added comments to show where the new code is.
ObjC.import("Cocoa"); 
// Вот здесь 
ObjC.registerSubclass({ 
    name: "AppDelegate",
    methods: { 
        "btnClickHandler": { 
            types: ["void", ["id"]], implementation: function (sender) { $.NSLog("Clicked!"); 
            } 
        } 
    } 
}); 
var appDelegate = $.AppDelegate.alloc.init; 
// Вот до сюда 
// Далее то, что уже было 
var textFieldLabel = $.NSTextField.alloc.initWithFrame($.NSMakeRect(25, (windowHeight - 40), 200, 24)); 
textFieldLabel.stringValue = "Image: (jpg, png, or gif)"; 
... 

Launch the application, click on the button and look at the console. See the “Clicked!” Message when you click on a button? If so, it’s just a jerk, right? If not, check the code and errors in the console.

Inheritance


What is ObjC.registerSubclass ? Inheritance is a way to create a new class that inherits from another Objective-C class. Lyrical digression: here there is a possibility of using the wrong terminology by me. Fight me. registerSubclass takes one argument: a JS object containing the properties of the new object. Properties can be: name , superclass , protocols , properties and methods . I am 100% not sure if this is an exhaustive list, but this is described in release notes .
This is all well and good, but what have we done here? Since we did not specify superclass, we inherit from NSObject. This is the base class for most Objective-C classes. The name property will allow us in the future to access the new class through $ or ObjC .
$ .AppDelegate.alloc.init; creates an instance of our class AppDelegate . Once again, note that the brackets for the calls to the alloc and init methods are not used, since we do not pass arguments to them.

Heir methods


The method is created by assigning it any string name. For example, btnClickHandler . Pass it an object with types and implementation properties . I did not find the official documentation about what the types array should contain. Through trial and error, I realized that it looks something like this:
["return type", ["arg 1 type", "arg 2 type",...]] 

btnClickHandler returns nothing, so the first element will be void. It takes one parameter, the object sending the message. In our case, NSButton , which we called btn . A complete list of types is available here .
implementation is a regular function. In it you write JavaScript. You have all the same access to $ as outside the object. Also, variables declared outside the function are available to you.

A little note about using protocols


You can inherit subclasses from Cocoa protocols, but there are pitfalls. I managed to find out that if you use the protocols array , your script will simply crash without errors. I wrote an example and explanation , so if you want to work with them, read it.

Image selection and display


We are ready to open the panel, select an image and show it. Update function code
 btnClickHandler: 
... 
implementation: 
    function (sender) { 
        var panel = $.NSOpenPanel.openPanel; 
        panel.title = "Choose an Image"; 
        var allowedTypes = ["jpg", "png", "gif"]; 
        // NOTE: Мост из массива JS в массив NSArray
        panel.allowedFileTypes = $(allowedTypes); 
            if (panel.runModal == $.NSOKButton) { 
                // NOTE: panel.URLs - это NSArray, а не JS array 
                var imagePath = panel.URLs.objectAtIndex(0).path; 
                textField.stringValue = imagePath; 
                var img = $.NSImage.alloc.initByReferencingFile(imagePath); 
                var imgView = $.NSImageView.alloc.initWithFrame( $.NSMakeRect(0, windowHeight, img.size.width, img.size.height)); 
                window.setFrameDisplay( $.NSMakeRect( 0, 0, (img.size.width > minWidth) ? img.size.width : minWidth, ((img.size.height > minHeight) ? img.size.height : minHeight) + ctrlsHeight ), true ); 
                imgView.setImage(img); 
                window.contentView.addSubview(imgView); window.center; 
                } 
            } 

First, we create an instance of NSOpenPanel . You saw the panels in action, if you have ever selected a file or chosen where to save this file.
All we need from the application is to show images. The allowedFileTypes property allows us to determine which types of files the panel can choose. It takes a value of type NSArray . We create a JS array with valid types, but we still need to convert it to NSArray . This is done like this: $ (allowdTypes) . This is another way to use the bridge. We use this method to translate a JS value into Objective-C. The reverse operation is done like this: $ (ObjCThing) .js .
Open the panel withpanel.runModal . Code execution is paused. If you click Cancel or Open , the panel will return a value. If the Open button is pressed , the constant $ .NSOKButton will be returned .
The following remark about panel.URLs is very important. In JS, accessing array values ​​is as follows: array [0] . Because URLs is of type NSArray , square brackets cannot be used. Instead, use the objectAtIndex method . The result will be the same.
After receiving the image URL, you can create a new instance of NSImage. Since creating an image using a file URL is a widespread approach, there is a convenient method for this:
initByReferencingFile
NSImageView is created in the same way that we used to create other UI elements. imgView controls the display of the image.
We need to adjust the window size to the width and height of the image, while not leaving the lower limits of height / width. To resize the window, use setFrameDisplay .
Thus, we set the image for imageView and added it to the window. Since its width and height have changed, the window needs to be centered.
And here is our little application. Go ahead, open a couple of files. And yes, animated gifs will work too, so don't forget about them.

Savory News


So far, you have run the application on opt + cmd + r . But you launch a regular application by double-clicking on the icon.

Double-click on the icon to start the application. The

icon can be changed by replacing /Contents/Resources/applet.icns . To access the application resources, right-click on the icon and select “Show Package Contents”.

Why did I get so excited


Because I think there is great potential. That's why. When Yosemite comes out, anyone can sit down and write a native application. And they can do it using one of the most common programming languages. They don’t have to download or install anything. Even Xcode will not need to be installed if you do not want to. The entry threshold will be greatly reduced. This is unbelievable.
I know that developing applications for OS X is a much deeper process than writing a script on your knees. I have no illusions that JavaScript will become the de facto standard for starting development for Mac. I believe that this will help developers write small applications that will make development easier for themselves and other people. Do you have someone in the team who finds it difficult to work with the command line? Write a quick GUI for it. Need a way to quickly or visually create or modify configs? Make a small application for this.
Other languages ​​also have these features. Python and Ruby have access to the same native APIs and people make applications using them. However, using JavaScript for this is different. This turns everything upside down. As if the DIY principles of web development are knocking on the door of development under Desktop. Apple left the door unlocked, I go in.

Translator's notes:
* - native, the word is already becoming common, for example, to distinguish between HTML5 applications and traditional
** - Stay open after run handler, I'm not sure what the setting is called in the Russian version of Script Editor
*** - message passing

Also popular now: