Создание сервиса для OS X
В OS X есть малозаметный пункт меню — Services. Обычно в нем собраны команды, выполняющие какую-либо специализированную короткую операцию, вроде создания заметки из выделенного текста. Попробуйте, например, в Safari выделить текст, а затем открыть меню Safari → Services.
Я тщательно искал, но не нашел в Интернете источника с исходными текстами, объясняющего, как сделать свой сервис. Пример SampleCMPPlugIn устарел с ног до головы, о чем имеется соответствующее предупреждение. В новой документации нет готового примера. Да и в целом хотелось бы видеть больше. Paul Oppenheim делится кодом добавления пункта в контекстное меню Finder средствами Automator, но Automator — не то средство, которое я хотел использовать. Еще находится статья на CocoaDev и заметка в блоге Michael Schade. Последние две также не содержат готовых примеров и написаны в XCode 3, в XCode 4 есть незначительные тонкости. К тому же, в статье Шейда, насколько я могу судить, ошибка в предоставленном XML из plist. С тем, как написать сервис для OS X в XCode 4 и разберемся.
Создаем новый проект. File → New → New Project, Mac OS X → Framework & Library → Bundle. Используем Core Foundation. ARC не используем. Проект я назвал UppercaseService.
Первый шаг в том, чтобы изменить расширение нашего бандла. Из UppercaseService.bundle он должен стать UppercaseService.service. Идем в настройки таргета на вкладку Build Settings. Там в секции Packaging находим ключ Wrapper extension и меняем его значение на service. Не закрываем настройки. В секции Linking ключу Math-O-Type задаем значение Executable.
К бандлу мы еще вернемся. Сейчас нужно создать код, который будет выполняться нашим сервисом. File → New → New Target → Mac OS X → Application → Command Line Tool. Называл UppercaseExecutable. Поля оставил по-умолчанию.
Все еще никакого кода, одна только подготовка. Нужно как-то сообщить UppercaseService, что он должен выполнять свежесозданный UppercaseExecutable. В XCode 3 создавался Custom Executable. В XCode 4 это делается через редактирование схемы для таргета. Выбираем Edit Scheme, убеждаемся, что выбран UppercaseService, на вкладке Info в выпадающем списке Executable выбираем наш UppercaseExecutable.
Забегу вперед. Если бы мы не переключили Math-O-Type для UppercaseService в Executable, то несмотря на заданный для выполнения таргет, программа бы не работала. В процессе отладки рекомендую смотреть в системную консоль (Console это не Terminal). Для непереключенного Math-O-Type и готовой программы мы бы увидели сообщения примерно такого вида:
07.04.12 16:40:42,498 com.apple.appkit.xpc.sandboxedServiceRunner: 2012-04-07 16:40:42.497 SandboxedServiceRunner[16875:4093] Application ru.chernyy.UppercaseService never opened its Services port before the timeout. 07.04.12 19:51:12,697 [0x0-0xa4fa4f].ru.chernyy.UppercaseService: /Users/chernyy/Library/Services/UppercaseService.service/Contents/MacOS/UppercaseService: /Users/chernyy/Library/Services/UppercaseService.service/Contents/MacOS/UppercaseService: cannot execute binary file 07.04.12 19:51:12,697 com.apple.launchd.peruser.501: ([0x0-0xa4fa4f].ru.chernyy.UppercaseService[21137]) Exited with code: 126
Наконец, к коду. Позволю себе ускориться и не быть таким подробным. К таргету UppercaseExecutable добавил обычный Objective-C класс, назовем его путь даже UppercaseService. Этот класс будет отвечать за работу сервиса. В нашем простейшем случае у него будет всего один метод. Имя метода запомните, пригодится дальше.
#import <Foundation/Foundation.h> #import <AppKit/AppKit.h> @interface UppercaseService : NSObject { } - (void)capitalizeIt:(NSPasteboard *)pasteboard userData:(NSString *)userData error:(NSString **)error; @end
#import "UppercaseService.h" @implementation UppercaseService - (void)capitalizeIt:(NSPasteboard *)pasteboard userData:(NSString *)userData error:(NSString **)error { NSArray *types = [pasteboard types]; NSString *incomingString = [pasteboard stringForType:NSStringPboardType]; BOOL isPasteboardConstainsString = [types containsObject:NSStringPboardType]; BOOL isIncomingStringCorrect = (incomingString != nil); if (isPasteboardConstainsString == NO) { *error = @"Pasteboard doesn't contain a string"; return; } if (isIncomingStringCorrect == NO) { *error = @"Couldn't uppercase string"; return; } types = [NSArrayarrayWithObject:NSStringPboardType]; NSString *outcomingString = [incomingString uppercaseString]; [pasteboard clearContents]; [pasteboard declareTypes:types owner:nil]; [pasteboard writeObjects:[NSArrayarrayWithObject:outcomingString]]; } @end
Если выбросить проверки, то останется всего пару строк работы с NSPasteboard. А вся полезная работа сервиса выполняется вызовом одного только метода uppercaseString. У меня сразу появилась мысль сделать сервис, который будет правильно форматировать текст (кавычки, тире, неразрывные пробелы…). Можете взять себе в качестве домашнего задания. Полезная функция сервиса есть, но сам сервис еще никак не общается с внешним миром. Как-то так должен выглядеть main.m:
#import <Foundation/Foundation.h> #import <AppKit/AppKit.h> #import "UppercaseService.h" int main (int argc, constchar * argv[]) { @autoreleasepool { UppercaseService *uppercaseService = [[UppercaseService alloc] init]; NSRegisterServicesProvider(uppercaseService, @"UppercaseService"); NS_DURING [[NSRunLoopcurrentRunLoop] run]; NS_HANDLER NS_ENDHANDLER [uppercaseService release]; } return 0; }
#define NS_DURING @try { #define NS_HANDLER } @catch (NSException *localException) { #define NS_ENDHANDLER }
Специально разместил описание констант для тех, кто был озадачен строками с префиксом NS_. Убедитесь, никакой магии нет. Описание функции NSRegisterServicesProvider совпадает с ее именем. Первый параметр — объект сервиса, второй — уникальное имя сервиса. Функция доступна в рамках AppKit.framework, начиная с версии ОС 10.0, этот фреймворк должен быть добавлен к проекту. Замечу также, что необходимо для всех файлов верно выставить Target Membership.
Последний штрих. Придется вернуться к бандлу сервиса и немного изменить Info.plist. Правой кнопкой мыши на UppercaseService-Info.plist → Open As → Source Code. Все можно сделать и из обычного редактора, но я считаю, так нагляднее. Нужно добавить несколько значений. У меня они добавлены после ключа копирайта. Последние два закрывающих тега я оставил, чтобы лишний раз напомнить, все это единый plist. Программа работает только в фоне.
... <key>LSBackgroundOnly</key> <string>1</string> <key>NSServices</key> <array> <dict> <key>NSKeyEquivalent</key> <dict> <key>default</key> <string>U</string> </dict> <key>NSMenuItem</key> <dict> <key>default</key> <string>Uppercase Text</string> </dict> <key>NSMessage</key> <string>capitalizeIt</string> <key>NSPortName</key> <string>UppercaseService</string> <key>NSReturnTypes</key> <array> <string>NSStringPboardType</string> </array> <key>NSSendTypes</key> <array> <string>NSStringPboardType</string> </array> </dict> </array> </dict> </plist>
Можно собирать сервис. Если что-то не работает, первым делом проверьте правильно ли вы включили файлы в таргеты, добавили ли фреймворк AppKit.
Пусть у нас есть собранный сервис. В Project Navigator найдите Products, а в нем UppercaseService.service. Щелчок правой кнопкой мыши → Show in Finder. В другом окне откройте ~/Library/Services (Cmd + Shift + G в помощь). Скорее всего, каталог не существует. Создайте его, а затем скопируйте туда UppercaseService.service.
Еще полшага. В Терминале нужно выполнить команду, чтобы обновить список доступных сервисов для работы с буфером. Обратите внимание, я выставил фильтр по названию рассматриваемого сервиса, ваш может называться иначе.
imac-chernyy:~ chernyy$ /System/Library/CoreServices/pbs -dump_pboard | grep UppercaseService
Для полного счастья можно проверить через System Preferences → Keyboard → Services, есть ли созданный сервис в секции Text, включить его, назначить сочетание клавиш. Запустим TextEdit, наберем какой-нибудь текст. TextEdit → Services → Uppercase Service или заданное сочетание клавиш.
Исходные тексты UppercaseService. Вы можете использовать их без каких-либо ограничений. Традиционно: за возможный моральный или материальный ущерб я ответственности не несу :)
Комментарии
Алексей Секачев
Саш, это ж Эгея?:) Поделись темой, а то я так и не смог напилить ничего приличного из нее, а у тебя круто и симпатично тут:)
P.S. Крутой блог!
Александр Черный
Нет, Леша, это ни разу не Эгея :) Это мой собственный дизайн и код.