Первая программа для OS X — менеджер буфера обмена

от автора

Больше года прошло с тех пор, как я увлекся программированием под платформу iOS. Наконец-то я нашел свободное время попробовать свои силы на платформе OS X. Если вы давно испытываете интерес к платформе OS X, но никак не соберетесь начать, эта статья для вас! Под катом подробное описание процесса создания приложения — менеджера буфера обмена. Все исходники можно найти на github.com/k06a/Clipshare


Подготавливаем проект к работе

Не долго думая, я создал в Xcode 5 проект типа Cocoa Application с именем «Clipshare». В свежесозданном проекте, мы можем наблюдать следующие файлы:

Файлы ABAppDelegate.h и ABAppDelegate.m относятся к классу делегата приложения и в них мы как раз будем писать весь наш код. В файле MainMenu.xib мы будем рисовать и настраивать графический интерфейс приложения.

Хорошенько подумав, решаем что наше приложение должно быть безоконным, и должно висеть в строке статуса (возле часиков). Первое что нам потребуется, удалить из файла MainMenu.xib стандартную строку меню и объект окна — они нам не потребуются. Далее создаем объект меню с 2 пунктами: разделителем и пунктом выхода из приложения. Это делается простым перетаскиванием объектов из библиотеки на холст. Для работы пункта меню Quit достаточно с зажатой клавишей Ctrl протянуть от пункта меню до объекта приложения Application синюю нить мышкой:

После того как мы отпустим клавишу мыши, появится окошко со списком селекторов (методов), доступных для соединения. Среди представленных методов, нам подходит -terminate:

Теперь необходимо суметь обратиться к нашему меню из кода. Для того чтобы настроить его отображение в статусной строке OS X и много еще для чего. Откроем режим ассистанта, когда в окне Xcode видно сразу 2 докумена, в первом оставим — графический интерфейс, а во втором выберем исходный код приложения: ABAppDelegate.h. И также как и в прошлый раз с зажатой клавишей Ctrl протянем синюю нить от меню, но теперь к исходному коду. Протянуть нужно в секцию кода @interface в файле ABAppDelegate.h (свойство window я из этого файла я уже стер за ненадобностью):

Как только мы отпустим зажатую клавишу мыши, выскочит диалоговое окно с настройками создаваемого Outlet (свойство в коде, ссылающееся на объект графического интерфейса). Остается только указать имя свойства, например «menu»:

Теперь переключаемся на исходный код приложения. В файле ABAppDelegate.m у нас имеется всего 1 метод и тот пустой:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {    // ... }

Внутри этого метода нем предстоит вписать код, в первую очередь выполняющий привязку нашего меню к строке статуса OS X:

self.statusBar = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; self.statusBar.title = @"CS"; self.statusBar.menu = self.menu; self.statusBar.highlightMode = YES;

Для работоспособности этого кода необходимо также объявить свойство statusBar в секции @interface:

@property (strong, nonatomic) NSStatusItem *statusBar;

Чтобы уже можно было проверить наше приложение, нужно всего лишь добавить 1 ключик со значением YES в файл Clipshare-Info.plist, он позволит нашему приложению работать без окна:

Запускаем наше приложение и наблюдаем его в строке статуса OS X и даже можем выйти из него по Cmd-Q или просто ткнув в единственный пункт меню:

Внедряем основную логику

Предлагаю проверять содержимое буфера обмена по таймеру каждые полсекунды и в зависимости от того, что мы в нем найдем — выполнять те или иные действия. Судя по всему, нет другого способа узнать об изменениях в буфере обмена, а жаль ( stackoverflow.com/a/5033480/440168 ). Ок, настраиваем таймер. Создаем объект таймера, указываем ему какой метод и как часто он должен вызывать и добавляем таймер в цикл обработки сообщений:

NSTimer * timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

Теперь циклом обработки сообщений по таймеру будет происходить вызов селектора (метода) -timerFire: у объекта self. Не забудем этот метод реализовать:

- (void)timerFire:(id)sender {     NSPasteboard * pboard = [NSPasteboard generalPasteboard];     NSPasteboardItem * pboardItem = [[pboard pasteboardItems] lastObject];     NSString * text = [pboardItem stringForType:NSPasteboardTypeString];      // ... }

В теле метода, мы обращаемся к главному буферу обмена, к последнему объекту в нем содержащемуся и пытаемся извлечь из него текстовые данные. Предлагаю запоминать изменения буфера обмена в массиве предыдущих значений, а также хранить параллельный массив ( ru.wikipedia.org/wiki/Параллельный_массив ) дат и времени соответствующих изменений:

@property (strong, nonatomic) NSMutableArray * texts; @property (strong, nonatomic) NSMutableArray * times;

Не забываем в начале работы программы инициализировать оба свойства пустыми пассивами:

self.texts = [NSMutableArray array]; self.times = [NSMutableArray array];

Теперь реализуем следующую логику: текст обнаруженный в буфере обмена мы поищем в массиве с предыдущими значеними и если такой НЕ найдется — добавим его в начало массива и добавим новый пункт в начало меню:

NSInteger index = [self.texts indexOfObject:text];  // ...  NSMenuItem * menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(menuItemSelect:) keyEquivalent:@""]; [self.menu insertItem:menuItem atIndex:0]; [self.texts insertObject:text atIndex:0]; [self.times insertObject:[NSDate date] atIndex:0];

Если такой текст уже есть в массиве с предыдущими значениями, будем ставить галочку у соответствующего пункта нашего меню и ничего более. Для этого заведем новое свойство selectedIndex типа BOOL:

if (index != NSNotFound) {     self.selectedIndex = index;     [self updateItemTitlesAndStates];     return; }

Метод updateItemTitlesAndStates занимается обходом всех пунктов меню и обновлением их названий (в названиях указано время, прошедшее с момента копирования в буфер) и выставлением галочки слева от нужного пункта selectedIndex:

- (void)updateItemTitlesAndStates {     for (int i = 0; i < self.menu.itemArray.count-2; i++)     {         NSDate * time = self.times[i];         NSString * text = self.texts[i];         NSMenuItem * menuItem = self.menu.itemArray[i];                  NSString * timeStr = nil;         NSTimeInterval secs = MAX(0,[[NSDate date] timeIntervalSinceDate:time]);         if (secs < 60)             timeStr = [NSString stringWithFormat:@"%ds",(int)(secs)];         else if (secs < 60*60)             timeStr = [NSString stringWithFormat:@"%dm",(int)(secs/60)];         else if (secs < 60*60*24)             timeStr = [NSString stringWithFormat:@"%dh",(int)(secs/60/60)];         else if (secs < 60*60*24*7)             timeStr = [NSString stringWithFormat:@"%dd",(int)(secs/60/60/24)];         else if (secs < 60*60*24*365.75)             timeStr = [NSString stringWithFormat:@"%dw",(int)(secs/60/60/24/7)];         else if (secs < 60*60*24*365.75*3)             timeStr = [NSString stringWithFormat:@"%dM",(int)(secs/60/60/24/30.5)];         else if (secs < 60*60*24*365.75*100)             timeStr = [NSString stringWithFormat:@"%dy",(int)(secs/60/60/24/365.75)];         else             timeStr = @"..";                  menuItem.title = [NSString stringWithFormat:@"(%@) \"%@%@\"", timeStr,                           [text substringToIndex:MIN(MaxVisibleChars,text.length)],                           (text.length <= MaxVisibleChars) ? @"" : @"..."];         menuItem.state = (i == self.selectedIndex) ? NSOnState : NSOffState;         menuItem.keyEquivalent = [@(i+1) description];     } }

Ну и в случае добавления нового пункта нужно еще удалять старые пункты, если их слишком много:

while (self.menu.itemArray.count >= MaxVisibleItems+2) {     [self.menu removeItemAtIndex:self.menu.itemArray.count-3];     [self.texts removeLastObject];     [self.times removeLastObject]; }

Осталось обработать клики по пунктам меню:

- (void)menuItemSelect:(id)sender {     NSInteger index = [self.menu.itemArray indexOfObject:sender];          NSPasteboard * pboard = [NSPasteboard generalPasteboard];     [pboard clearContents];     NSPasteboardItem * pboardItem = [[NSPasteboardItem alloc] init];     [pboardItem setString:self.texts[index] forType:NSPasteboardTypeString];     [pboard writeObjects:@[pboardItem]]; }

Вот теперь приложение, работает как мы хотели! Когда в буфер обмена попадает текст, который уже был там недавно, галочка перескакивает вниз на нужный пункт. То же происходит, если мы кликаем по этому пунктусами:


Приложение рассчитано только на текстовые данные, и не работает должным образом с картинками, файлами и прочими вещами попадающими в буфер обмена, но я сам им пользуюсь уже пару дней. Это первое приложение написанное мной за пару часов — которое я реально использую. Спасибо тем, кто прочитал и просмотрел все картинки, надеюсь вам понравилось!

За магические числа и параллельные массивы в коде, прошу не пинать! Как было проще и быстрее так и написал. Кто любит по фэн-шую всё — жду ваш пул-реквест!

Исходники приложения найти можно здесь: github.com/k06a/Clipshare
Бинарник можно скачать с моего дропбокса: dl.dropboxusercontent.com/u/8629267/Clipshare.app.zip

P.S. Если не будете скупиться на плюсы и комментарии, в следующей статье создадим приложение под iOS и будем синхронизировать буфер обмена между устройствами!

P.P.S. Кто найдет в тексте эпическую опечатку, получит плюс в карму!

ссылка на оригинал статьи http://habrahabr.ru/post/197348/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *