Больше года прошло с тех пор, как я увлекся программированием под платформу 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. Кто найдет в тексте эпическую опечатку, получит плюс в карму!
Автор: k06a