...

воскресенье, 13 октября 2013 г.

Первая программа для 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. Кто найдет в тексте эпическую опечатку, получит плюс в карму!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий