📜 ⬆️ ⬇️

Creating OS X Applications with JavaScript

Recently, OS X Yosemite introduced the ability to use JavaScript for automation. This makes it possible to access native * OS X frameworks from JavaScript. I rummaged around in this new world and gathered a few examples . In this post I will explain the basics and step by step will show the process of creating a small application. .
At WWDC 2014, there was an automation session using JavaScript . It said that you can now use JavaScript to automate applications instead of AppleScript. This is an exciting news in itself. The ability to automate repetitive operations using AppleScript has been around for quite some time. Writing in AppleScript is not the most pleasant thing, so using the familiar syntax instead would be very good.
During this session, the speaker spoke about the bridge with Objective-C. This is where the fun begins. 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 FreeMork 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. Maybe you are not 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 all the same in JavaScript as in Objective-C or Swift.

Example


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

Screen application that I am going to write.

The application will have a window, a text box, an input field and a button. Well, or by class names: NSWindow , NSTextField , NSTextField and NSButton .
Clicking on the “Select File” button will open NSOpenPanel to select a file. We will configure the panel in such a way that it will not allow the user to select files with an extension other than .jpg, .png and .gif.
After selecting the image, we 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 size so that the controls do not disappear.
')

Project Setup


Open the Apple Script Editor application in Applictions> Utilities . This is not the best editor I've tried, but now it is needed. There are a number of necessary features for building an OS X application on JS. Not sure what's going on under the hood, but he can compile and run your scripts as applications. It also creates additional necessary things, such as the Info.plist file. It seems to me that it is possible to force other editors to do the same, but I have not figured it out yet.
Create a new document via 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 required in order to run the project as an application.

Script Editor with the necessary settings

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

Let's do something already!


Add the following lines to your script and run everything via 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 appeared in the menu bar and the items File and Edit. You can see that the application is running, as its icon is now in the dock.
Where is the line "Hi everybody!"? And what's 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 the Developer Tools in Chrome, Safari or Firefox. The main difference is that you debug applications instead of sites.
There are a lot of messages in it. Filter it by writing “applet” in the search bar in the upper right corner. Return to the Script Editor and run the application again via 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. I forgot to close it many times 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 indispensable things, you will always use them for debugging. To learn how to log something other than strings, study 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! We made a small amount of code an application that can be moved, collapsed and closed.

Simple NSWindow window created with JS

If you, like me, have never had to build applications with Objective-C or Cocoa, all of this may look a little crazy. I was so shown the length of the names of the methods. I like descriptive names, but Cocoa goes too far.
However, this is javascript. The code looks like you are writing a site.
What happens in the first lines where we set the styleMask value? Style masks are used to customize windows. Each option says that 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 curious things about syntax. $ .NSWindow.alloc calls the alloc method of an NSWindow object. Note that after calling the method there are no brackets. In JavaScript, this gives access to properties, not methods. How is it going? In JS for OS X, brackets are allowed only if you pass parameters. Attempting to use parentheses with no arguments will result in a runtime error. If something goes wrong as intended, look at the console for errors.
Here is an example of a super-long method name:
 initWithContentRectStyleMaskBackingDefer 

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

Note the colons (":") in the original method description. To use the Objective-C method in JS, you need to remove them and replace the next letter with a capital letter. The square brackets ("[]") are 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 pretty simple. I will miss her detailed description. To deal with 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 have a window showing, it's already great. Let's do something else.

Adding controls


We need a label, a text field and a 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 you now have a window with controls. Nothing can be entered in the field, and the button does nothing, but wait a minute, we are already moving.

Window controls

What did we do here? textFieldLabel and textField are similar. 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) . As a result, it creates what is called “structure” in Objective-C. In JS, the equivalent can be an object, a hash, or perhaps a dictionary. Pairs key value.
After creating the text boxes, set each handful of properties to get the desired result. Cocoa doesn't have anything like the html label element. So, let's make our own by turning off the background and the ability to edit.
We will set the text field programmatically, simultaneously disabling editing. If we didn’t need this, we would be treated in one line.
To create a button we use NSButton . As with the text field, we need 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 to find out what else you can do with the button. I also have an example where various styles and types of buttons are shown in action.
The last of the new things we are doing here is adding items to the window using addSubView . The first time I tried to do it using
 window.addSubview(theView) 

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

Making the button work


Clicking on the button “Select image” should lead to the opening of the panel displaying files on the computer. Before that, let's warm up by adding a message to the console when the button is pressed.
In JS, event handlers are attached to elements for handling clicks. Objective-C uses a slightly different concept. It uses what is called "messaging" ***. That is, you send 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. Maybe this is not the most accurate description, but I understand that.
The first is to set the target and the action of the button. Target is the object to which you want to send the message contained in action . If it is 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 in the console. See the “Clicked!” Message when you click a button? If yes, then this is just a bailout, right? If not, check the code and errors in the console.

Inheritance


And what about ObjC.registerSubclass ? Inheritance is a way to create a new class that inherits from another Objective-C class. Lyrical digression: there is a chance that I use the wrong terminology. 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'm not 100% sure that this is an exhaustive list, but this is how it is described in release notes .
That's all well and good, but what have we done here? Since we did not specify a superclass, we inherit from NSObject . This is the base class for most Objective-C classes. The name property will allow us to access a new class in the future through $ or ObjC .
$ .AppDelegate.alloc.init; creates an instance of our AppDelegate class. Again, note that the parentheses of 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 any string name to it. For example, btnClickHandler . Pass an object with the types and implementation properties to it. I did not find official documentation on what the array of types 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 full list of types is available here .
implementation is a common feature. 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 quick 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 an explanation , so if you want to work with them, read.

Select and display images


We are ready to open the panel, select the 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 ever selected a file or chose 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 on the 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 the JS value in Objective-C. The reverse operation is as follows: $ (ObjCThing) .js .
Open the panel with panel.runModal . The execution of the code is suspended. If you click Cancel or Open , the panel returns the value. If the Open button is pressed, the $ .NSOKButton constant will return.
The following note about panel.URLs is very important. In JS, access to the array values ​​is as follows: array [0] . Because URLs are of type NSArray , you cannot use square brackets. 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 by a file URL is a widespread approach, there is a convenient method for this:
initByReferencingFile
NSImageView is created in the same way we used to create other UI elements. imgView manages image display.
We need to adjust the window size to the width and height of the image, while not going beyond the lower limits of the height / width. To resize a window, use setFrameDisplay .
So we set the image for imageView and add 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


Until now, you have started the application using opt + cmd + r . But you launch a regular application by double clicking on the icon.

Double click on the icon to launch the application.

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

Why did it get me so excited


Because I think there is a lot of 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 will not have to download or install anything. Even XCode will not need to be installed if you do not want to. The threshold of entry will be greatly reduced. This is incredible.
I know that developing applications for OS X is a much deeper process than writing a script on my knee. I have no illusions about the fact that JavaScript will become the de facto standard for starting development for Mac. I believe this will help developers write small applications that will make development easier for themselves and other people. Do you have a person in the team who find it difficult to work with the command line? Write a GUI for him quickly. Need a way to quickly, visually create or change configs? Make a small application for this.
These features are in other languages. In Python and Ruby, you have access to the same native API and people make applications using them. However, using javascript for this is different. It turns everything upside down. It’s as if the DIY principles of web development are knocking on the development door under Desktop. Apple left the door unlocked, I enter.

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

Source: https://habr.com/ru/post/239635/


All Articles