Недавно в OS X Yosemite была представлена возможность использования JavaScript для автоматизации. Это открывает возможность получать доступ к нативным* фреймворкам OS X из JavaScript. Я тут покопался в этом новом мире и собрал несколько примеров. В этом посте я объясню основы и шаг за шагом покажу процесс создания небольшого приложения.
На WWDC 2014 проходила сессия по автоматизации с помощью JavaScript. На ней рассказывали, что вы теперь можете использовать JavaScript для автоматизации приложений вместо AppleScript. Это само по себе увлекательная новость. Возможность автоматизировать повторяющиеся операции с помощью AppleScript существует уже довольно давно. Писать на AppleScript — не самое приятное занятие, так что использовать вместо него знакомый синтаксис было бы очень хорошо.
Во время этой сессии докладчик рассказал о мосте с Objective-C. Вот здесь начинается самое интересное. Мост позволяет вам импортировать любой Objective-C фреймоврк в JS приложение. Например, если вы хотите написать GUI используя стандартные элементы управления OS X, вам нужно импортировать Cocoa:
ObjC.import("Cocoa");
Фрейморк Foundation делает именно то, что предполагает его название. Он позволяет собирать блоки для приложений OS X. В нем есть огромный набор классов и протоколов. NSArray, NSURL, NSUserNotification, и т.д. Может быть, вы и не знакомы со всеми ними, но их названия подсказывают, для чего они служат. Из-за своей крайней важности, фреймворк доступен по умолчанию, без необходимости импорта в новое приложение.
Насколько я могу судить, вы можете сделать на JavaScript все то же самое, что и на Objective-C или Swift.
Пример
Внимание: для работы этого примера вам необходима Yosemite Developer Preview 7+
Лучший способ чему-то научиться — это просто взять и попробовать что-то сделать. Я собираюсь показать вам процесс создания небольшого приложения, которое может показывать изображения с компьютера.
Вы можете скачать полный пример из моего репозитория.
Скрин приложения, которое я собираюсь написать.
В приложении будет: окно, текстовый блок, поле ввода и кнопка. Ну, или по названиям классов: NSWindow, NSTextField, NSTextField и NSButton.
Клик по кнопке «Выбор файла» откроет NSOpenPanel для выбора файла. Мы настроим панель таким образом, что она не даст пользователю выбирать файлы с расширением, отличным от .jpg, .png и .gif.
После выбора изображения мы покажем его в окне. Окно будет подгонять свой размер под ширину и высоту изображения плюс высота элементов управления. Мы так же укажем минимальные размеры окна чтобы элементы управления у нас не пропали.
Настройка проекта
Откройте приложение Apple Script Editor в Applictions > Utilities. Это не самый лучший редактор из тех, что я пробовал, но сейчас он необходим. Там есть ряд необходимых фич для сборки приложения OS X на JS. Не уверен, что там происходит под капотом, но он умеет компилировать и запускать ваши скрипты как приложения. Он так же создает дополнительные нужные вещи, как например файл Info.plist. Мне кажется, есть возможность заставить и другие редакторы делать то же, но я еще не разбирался с этим.
Создайте новый документ через File > New или cmd + n. Первое, что нам надо сделать — сохранить документ как приложение. Сохраните его через File > Save или cmd + s. Не спешите разу сохранять. Там есть две настройки, которые необходимы для того, чтобы запускать проект как приложение.
Script Editor с необходимыми настройками
Измените формат на «Application» и отметьте галочку «Не закрывать»**
Важное замечание: эти настройки можно поменять: откройте меню File и удерживайте кнопку option. Это откроет пункт «Save As...». В диалоге сохранения можно вносить изменения в настройки. Но лучше сделать это сразу при создании проекта.
Если вы не отметите флажок «Не закрывать», то ваше приложение откроется, а затем сразу же закроется. В интернете почти нет документации по этому функционалу. Я узнал об этом только после того, как несколько часов бился лбом в клавиатуру.
Давайте уже что-то сделаем!
Добавьте в ваш скрипт следующие строчки и запустите все через Script > Run Application или cmd + r.
ObjC.import("Cocoa");
$.NSLog("Hi everybody!");
Почти ничего не произошло. Единственные видимые изменения — в строке меню и доке. В строке меню появилось название приложения и пункты File и Edit. Можете видеть, что приложение запущено, так как его иконка теперь в доке.
Где же строчка «Hi everybody!»? И что за знак доллара, jQuery? Закройте приложение через File > Quit или cmd + q и давайте выясним, где же произошло это NSLog.
Откройте консоль: Applications > Utilities > Console. Каждое приложение может писать что-то в консоль. Она не намного отличается от Developer Tools в Chrome, Safari или Firefox. Основное отличие в том, что вы производите отладку приложений вместо сайтов.
В ней много сообщений. Отфильтруйте ее написав «applet» в строке поиска в правом верхнем углу. Вернитесь в Script Editor и запустите приложение снова через opt + cmd + r.
Видели!? Сообщение «Hi everybody!» должно появиться в консоли. Если его там нет, закройте ваше приложение и запустите снова. Я много раз забывал его закрыть и код не запускался.
Что насчет знака доллара?
Знак доллара — это и есть ваш доступ к мосту в Objective-C. Каждый раз когда вам необходимо получить доступ к классу или константе Objective-C, вам нужно использовать $.foo или ObjC.foo. Позже я расскажу и про другие способы использовать $.
Приложение «Консоль» и NSLog — незаменимые вещи, вы всегда будете использовать их для отладки. Чтобы узнать, как логгировать что-то кроме строк, изучите мой пример с NSLog.
Создаем окно
Давайте сделаем что-нибудь, с чем можно взаимодействовать. Допишите ваш код:
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);
Затем запустите приложение. opt + cmd + r. Вот теперь поговорим! Небольшим количеством кода мы сделали приложение, которое можно двигать, сворачивать и закрывать.
Простое окно NSWindow, созданное с помощью JS
Если вам, как и мне, никогда не приходилось создавать приложения с Objective-C или Cocoa, все это может выглядеть слегка бредово. Мне такими показалалсь длина названий методов. Мне нравятся описательные имена, но Cocoa заходит уж слишком далеко.
Тем не менее, это JavaScript. Код выглядит так, как будто вы пишете сайт.
Что же происходит в первых строчках, где мы устанавливаем значение styleMask? Маски стилей используются для настройки окон. Каждая опция говорит о том, что она добавляет; заголовок, кнопку закрытия, кнопку сворачивания. Эти опции — константы. Используйте побитовое или ("|") для отделения одной настройки от другой.
Опций много. Можете прочитать о них всех в документации. Попробуйте добавить NSResizableWindowMask и посмотреть, что произойдет.
Вам нужно запомнить несколько любопытных вещей касательно синтаксиса. $.NSWindow.alloc вызывает метод alloc объекта NSWindow. Обратите внимание, что после вызова метода нет скобок. В JavaScript таким образом получается доступ к свойствам, а не методам. Как так выходит? В JS для OS X скобки разрешено применять только в том случае, если вы передаете параметры. Попытка использовать скобки без аргументов приведет к runtime error. Если что-то идет не так, как задумано, смотрите в консоль на наличие ошибок.
Вот пример супер-длинного названия метода:
initWithContentRectStyleMaskBackingDefer
В документации к NSWindow этот метод выглядит несколько иначе:
initWithContentRect:styleMask:backing:defer:
В Objective-C такие окна создаются следующим способом:
NSWindow* window [[NSWindow alloc]
initWithContentRect: NSMakeRect(0, 0, windowWidth, windowHeight)
styleMask: styleMask,
backing: NSBackingStoreBuffered
defer: NO
];
Обратите внимание на двоеточия (":") в исходном описании метода. Для использования Objective-C метода в JS нужно их убрать и заменить следующую букву на заглавную. Квадратные скобки ("[]") — это вызов метода класса/объекта. [NSWindow alloc] вызывает метод alloc класса NSWindow. В JS это эквивалентно NSWindow.alloc, плюс, если надо, скобки.
Думаю, оставшаяся часть кода достаточно проста. Я пропущу подробное ее описание. Чтобы разобраться с тем, чт происходит дальше, вам понадобится много времени и придется много читать документацию, но вы справитесь. Если у вас показывается окно, то уже здорово. Давайте сделаем что-нибудь еще.
Добавляем элементы управления
Нам нужны метка (label), текстовое поле и кнопка. Мы будем использовать NSTextField и NSButton. Обновите свой код и снова запустите приложение.
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);
Если все прошло успешно, то теперь у вас есть окно с элементами управления. В поле ничего нельзя ввести, а кнопка ничего не делает, но постойте-ка, мы уже продвигаемся.
Элементы управления в окне
Что мы здесь сделали? textFieldLabel и textField похожи между собой. Они оба экземпляры NSTextField. Мы создали их так же, как создавали окно. Когда вы видите initWithFrame и NSMakeRect, то скорее всего, здесь создается элемент UI. NSMakeRect делает то, что вынесено в его название. Он создает прямоугольник с указанными координатами и размерами; (x, y, width, height). В результате создается то, что в Objective-C называется «структура». В JS эквивалентом может быть объект, хэш или, возможно, словарь. Пары ключ-значение.
После создания текстовых полей установим каждому горстку свойств чтобы получить желаемый результат. В Cocoa нет ничего похожего на html элемент label. Значит, сделаем свой отключив фон и возможность редактирования.
Мы установим текстовое поле программно, попутно отключив редактирование. Если бы нам это не было нужно, то мы бы обошлись одной строкой.
Для создания кнопки мы используем NSButton. Так же, как и в случае с текстовым полем, нам необходима структура. Выделим два свойства: bezelStyle и buttonType. Значения обоих — константы. Эти свойства определяют, как элемент будет нарисован и какие стили он будет иметь. См. документацию по NSButton чтобы узнать, что еще можно сделать с кнопкой. У меня так же есть пример, где показаны различные стили и типы кнопок в действии.
Последнее из нового, что мы здесь делаем — это добавление элементов в окно с помощью addSubView. В первый раз я попытался сделать это используя
window.addSubview(theView)
На других стандартных представлениях, которые вы создаете с помощью NSView, это сработает, но не с экземплярами NSWindow. Не уверен почему, но в окна элементы нужно добавлять в contentView. В документации сказано: «Самый верхний доступный объект NSView в иерархии окна». У меня сработало.
Заставляем кнопку работать
Нажатие на кнопку «Выбрать изображение», должно приводить к открытию панели, отображающей файлы на компьютере. Перед этим давайте разогреемся, добавив вывод сообщения в консоль при нажатии на кнопку.
В JS к элементам привязываются обработчики событий для обработки нажатий. Objective-C использует несколько другую концепцию. Он использует то, что называется «передача сообщений»***. То есть вы передаете объекту сообщение, содержащее название метода. Объект должен иметь информацию о том, что ему делать, когда он получает такое сообщение. Может, это и не самое точное описание, но я понимаю так.
Первое — нужно установить target и action у кнопки. Target — это объект, которому нужно послать сообщение, содержащееся в action. Если сейчас не понятно, двигайтесь дальше, вы все поймете, когда увидите код. Обновите часть скрипта, где производится настройка кнопки:
...
btn.target = appDelegate;
btn.action = "btnClickHandler";
...
appDelegate и btnClickHandler пока не существуют. Надо их сделать. В следующем коде важен порядок. Я добавил комментарии, чтобы показать, где новый код.
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)";
...
Запустите приложение, нажмите на кнопку и смотрите в консоль. Видите сообщение «Clicked!», когда нажимаете на кнопку? Если да, то это же просто отпад, да? Если нет, то проверьте код и ошибки в консоли.
Наследование
А что за ObjC.registerSubclass? Наследование — это способ создать новый класс, который наследуется от другого класса Objective-C. Лирическое отступление: здесь есть вероятность использования мной неправильной терминологии. Боритесь со мной. registerSubclass принимает один аргумент: JS объект, содержащий свойства нового объекта. Свойства могут быть: name, superclass, protocols, properties и methods. Я на 100% не уверен, что это исчерпывающий список, но так это описано в release notes.
Это все хорошо и прекрасно, но что мы здесь сделали? Так как мы не указали superclass, наследуемся от NSObject. Это базовый класс для большинства классов Objective-C. Свойство name позволит нам в будущем обращаться к новому классу через $ или ObjC.
$.AppDelegate.alloc.init; создает экземпляр нашего класса AppDelegate. Еще раз, обратите внимание, что скобки у вызовов методов alloc и init не используются, так как мы не передаем им аргументов.
Методы наследника
Метод создается тем, что вы назначаете ему любое строковое имя. Например, btnClickHandler. Передайте ему объект со свойствами types и implementation. Официальной документации по поводу того, что должен содержать массив types я не нашел. Методом проб и ошибок я понял, что он выглядит как-то так:
["return type", ["arg 1 type", "arg 2 type",...]]
btnClickHandler ничего не возвращает, так что первый элемент будет void. Он принимает один параметр, объект, отправляющий сообщение. В нашем случае NSButton, который мы назвали btn. Полный список типов доступен здесь.
implementation это обычная функция. В ней вы пишете JavaScript. У вас есть все тот же доступ к $, как и вне объекта. Так же вам доступны переменные, объявленные вне функции.
Небольшое замечание об использовании протоколов
Вы можете наследовать подклассы от протоколов Cocoa, но есть подводные камни. Мне удалось выяснить, что если использовать массив protocols, ваш скрипт просто вылетит без ошибок. Я написал пример и объяснение, так что если хотите с ними работать — почитайте.
Выбор и показ изображений
Мы готовы открыть панель, выбрать изображение и показать его. Обновите код функции
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;
}
}
Во-первых, мы создаем экземпляр NSOpenPanel. Вы видели панели в действии, если когда-либо выбирали файл или выбирали куда этот файл сохранить.
Все, что нам от приложения нужно — это показывать изображения. Свойство allowedFileTypes позволяет нам определить, какие типы файлов панель сможет выбирать. Оно принимает значение типа NSArray. Мы создаем JS массив с допустимыми типами, но нужно еще его сконвертировать в NSArray. Это делается так: $(allowdTypes). Это еще один способ использования моста. Мы используем этот способ для того, чтобы перевести JS значение в Objective-C. Обратная операция производится так: $(ObjCThing).js.
Открываем панель с помощью panel.runModal. Выполнение кода при этом приостанавливается. Если нажать Cancel или Open, панель вернет значение. Если нажата кнопка Open, вернется константа $.NSOKButton.
Следующее замечание по поводу panel.URLs очень важно. В JS доступ к значениям массива произодится так: array[0]. Из-за того, что URLs имеет тип NSArray, использовать квадратные скобки нельзя. Вместо этого нужно использовать метод objectAtIndex. Результат будет тот же.
После получения URL изображения можно создавать новый экземпляр NSImage. Так как создание изображения по URL файла — широко распространенный подход, для этого есть удобный метод:
initByReferencingFile
NSImageView создается тем же самым способом, который мы использовали для создания других UI элементов. imgView управляет показом изображения.
Нам необходимо подгонять размер окна под ширину и высоту изображения, при этом не выходя за нижние пределы высоты/ширины. Для изменения размера окна используем setFrameDisplay.
Таким образом, мы установили изображение для imageView и добавили его в окно. Так как его ширина и высота изменились, окно нужно центрировать.
И вот такое наше маленькое приложение. Вперед, откройте парочку файлов. И да, анимированные гифки тоже будут работать, так что не забудьте про них.
Пикантные новости
До сих пор вы запускали приложение по opt + cmd + r. Но обычное приложение вы запускаете двойным щелчком по иконке.
Дважды кликните по иконке, чтобы запустить приложение
Иконку можно изменить заменой /Contents/Resources/applet.icns. Чтобы получить доступ к ресурсам приложения, щелкните правой кнопкой по иконке и выберите пункт «Show Package Contents».
Почему меня это так взбудоражило
Потому что я думаю здесь большой потенциал. Вот почему. Когда Yosemite выйдет, кто угодно сможет сесть и написать нативное приложение. И смогут они это сделать используя один из наиболее распространенных языков программирования. Им не придется ничего скачивать или устанавливать. Даже XCode не нужно будет устанавливать, если не хочется. Порог вхождения будет сильно понижен. Это невероятно.
Я знаю, что разработка приложений для OS X — это гораздо более глубокий процесс, нежели написание скрипта на коленке. У меня нет иллюзий по поводу того, что JavaScript станет де-факто стандартом для начала разработки под Mac. Я считаю, что это поможет разработчикам писать небольшие приложения, которые сделают разработку проще для себя и других людей. У вас есть в команде человек, которому сложно работать с командной строкой? Напишите для него GUI по-быстрому. Нужно способ быстро, в визуальном режиме создавать или изменять конфиги? Сделайте для этого маленькое приложение.
Эти возможности есть и в других языках. В Python и Ruby есть доступ к тем же самым нативным API и люди делают приложения с их использованием. Однако использование для этого JavaScript — это другое. Это переворачивает все с ног на голову. Как будто DIY-принципы web-разработки стучатся в дверь разработки под Десктоп. Apple оставили дверь незапертой, я захожу.
Примечания переводчика:
* — native, слово уже становится общеупотребимым, например, для отличия HTML5-приложений и традиционных
** — Stay open after run handler, не уверен как именно называется настройка в русскоязычной версии Script Editor
*** — message passing
Автор: john_samilin