• Александр Черный
  • Блог
  • Проекты
  • О себе
  • RSS
13 апреля 2012

Создание сервиса для 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.

Смена Wrapper Extension и Math-O-Type

К бандлу мы еще вернемся. Сейчас нужно создать код, который будет выполняться нашим сервисом. 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.

Редатирование схема таргета, другой таргет на исполнение (Custom Executable)

Забегу вперед. Если бы мы не переключили 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 или заданное сочетание клавиш.

Uppercase Service «до» и «после»

Исходные тексты UppercaseService. Вы можете использовать их без каких-либо ограничений. Традиционно: за возможный моральный или материальный ущерб я ответственности не несу :)

os x   сервис   objective-c   

Комментарии

Алексей Секачев

Саш, это ж Эгея?:) Поделись темой, а то я так и не смог напилить ничего приличного из нее, а у тебя круто и симпатично тут:)

P.S. Крутой блог!

Александр Черный

Нет, Леша, это ни разу не Эгея :) Это мой собственный дизайн и код.

Ваш комментарий


(не будет опубликован)


© Александр Черный, 2009–2026

Служебный вход

Работает на YAPSE, β