«Съешь меня»… нет, не так… «Выполни меня»!

    У меня периодические возникают разные потребности решения мелких насущных задач в Mac OS X. Для этих целей я обычно делаю небольшие программы, которые «закрывают» потребность частным образом. Но иногда хочется, чтоб программа была универсальной, и ей могли воспользоваться другие люди при необходимости (например «Переlator»). Так получилось и в этот раз…

    Я люблю, когда Dock отображается всегда на экране. Но при запуске Симулятора iOS постоянно приходилось включать автоматические скрытие, чтобы симулятор полностью умещался на экране. Появилась задача — автоматизировать этот процесс. За пару дней набросал универсальную программу, с помощью которой можно задать AppleScript на определённое действие любой программы: «Программа запущена», «Программа завершена», «Программа активирована», «Программа деактивирована» и пр.




    Этот топик я разделю на две части. Одна для пользователей, которые просто хотят пользоваться программой (или ознакомиться). Вторая для начинающих разработчиков – я опишу схему работы программы и предоставлю исходный код. Несмотря на кажущуюся простоту самой программы, исходный код покрывает множество нюансов из различных тематик, которые могут сэкономить существенное количество времени в будущем.

    Загрузить программу можно по ссылке (только для 10.6+) (в 18.51 я обновил программу и исходные коды благодаря баг-репорту. Исправил маленький баг, из-за которого помощник не получал сообщения после удаления программы из списка. Издержки кустарного тестирования...).

    Программа очень проста в использовании (если имеются навыки работы с AppleScript). Это не отдельное приложение, а панель для «Системных настроек». Два раза щёлкаете по нему мышкой и получаете новую панель «Выполни меня» в «Системных настроек». В левой части находится список программ. А в правой скрипты, заданные для выбранной программы.

    При нажатии на кнопку "+" откроется список запущенных программ (имеющих идентификатор – Bundle identifier), из которого мы можем выбрать нужную программу для добавления (если требуемая программа не запущена, то её нужно предварительно запустить).


    Всё, что осталось, это задать скрипты на те действия, которые требуются. Например, включать режим автоматического скрытия Dock:
    tell application "System Events" to set the autohide of the dock preferences to true

    Или запустить какую-то программу:
    tell application "iTunes" to activate

    Или выполнить какой-нибудь консольный скрипт:
    do shell script "…"

    Возможности фактически ничем не ограничены — AppleScript позволяет сделать очень многое.

    На самом деле программа состоит из двух частей. Панель системных настроек. И специальная скрытая программа помощник – «Выполни меня (помощник)», которая автоматически прописывается в автозагрузку. Она настолько мизерная, что фактически не потребляет системных ресурсов, но именно она отвечает за выполнение скриптов прописанных в панели настроек.

    Чтобы удалить программу, просто щёлкните правой кнопкой мыши (или Ctrl + щелчок мышью / на тачпаде) на названии панели «Выполни меня» и выберите «Удалить панель».


    ДЛЯ НАЧИНАЮЩИХ РАЗРАБОТЧИКОВ

    Вот ссылка на исходный код проекта.
    Вот вариант на github.

    Программа состоит из двух частей — панели настроек и программы помощника. Для каждой из них я сделал отдельный проект (в данном случае мне так было удобнее). При необходимости два проекта можно объединить в один с двумя таргетами.

    Создание панели для «Системных настроек» мало чем отличается от создания обычной программы. В Xcode уже есть шаблон для этого модуля System Plug-in > Preference Pane. В котором уже добавлен класс на базе NSPreferencePane и интерфейсный (xib) файл к нему. Каждый решает сам, как тестировать этот модуль. Например, можно делать всё в виде обычной программы и лишь на финальном этапе переносить всё в Preference Pane.

    Важно понимать, что это не самостоятельная программа, а модуль для «Системных настроек». Это означает, что все методы, которые привязаны к основному bundle не будут корректно выполнены (т.к. основной bundle — это «Системные настройки»).

    Мы не можем пользоваться макросом следующего вида:
    NSLocalizedString(NSString *key, NSString *comment)

    Нужно пользоваться:
    NSLocalizedStringFromTableInBundle(NSString *key, NSString *tableName, NSBundle *bundle, NSString *comment)

    Или мы не можем пользоваться методом NSImage:
    + (id)imageNamed:(NSString *)name

    Нужно пользоваться, например:
    - (id)initWithContentsOfFile:(NSString *)filename

    И т.д.

    У объекта класса NSPreferencePane есть метод:
    - (NSBundle *)bundle

    Его и нужно использоваться.

    В проекте используются две кнопки "+" и "-" для редактирования списка программ.

    Инициализация кнопки удаления банальная и простая:
    removeButton = [[NSButton alloc] initWithFrame:NSMakeRect(43192222)];<br/>
    [removeButton setButtonType:NSMomentaryChangeButton];<br/>
    [removeButton setImage:buttonImage];<br/>
    [removeButton setImagePosition:NSImageOnly];<br/>
    [removeButton setBordered:NO];<br/>
    [removeButton setTarget:self];<br/>
    [removeButton setAction:@selector(removeButtonAction:)];<br/>
    [[self mainView] addSubview:removeButton];

    А вот кнопка добавления уже сложнее. Для неё необходимо ввести два подкласса NSPopUpButton и NSPopUpButtonCell. Формально для кнопки с выпадающим списком служит класс NSPopUpButton. Но в голом виде он нам не подходит. Во-первых, не позволяет задать статическую картинку для кнопки (точнее позволяет, но реальный размер кнопки не соответствует реальному размеру статической картинки, что в нашем случае неприемлемо, т.к. у нас две кнопки расположены рядом) — это мы обходим с помощью подкласса NSPopUpButtonCell. Во-вторых, NSPopUpButton не позволяет динамически изменить содержимое меню в момент её нажатия (у нас меню должно формироваться именно в момент нажатия на кнопку) – это мы обходим с помощью подкласса NSPopUpButton.

    Чтобы кнопка имела размер один в один с заданной картинкой, создаём класс PopUpCell:
    @interface PopUpCell : NSPopUpButtonCell<br/>
    {<br/>
        NSButtonCell *buttonCell;<br/>
    }<br/>
     <br/>
    - (id)initWithimage:(NSImage *)image;<br/>
     <br/>
    @end<br/>
     <br/>
    …<br/>
     <br/>
    @implementation PopUpCell<br/>
     <br/>
    - (id)initWithimage:(NSImage *)image<br/>
    {<br/>
        self = [super initTextCell:@"" pullsDown:YES];<br/>
     <br/>
        buttonCell = [[NSButtonCell alloc] initImageCell:image];<br/>
        [buttonCell setButtonType:NSPushOnPushOffButton];<br/>
        [buttonCell setImagePosition:NSImageOnly];<br/>
        [buttonCell setImageDimsWhenDisabled:YES];<br/>
        [buttonCell setBordered:NO];<br/>
     <br/>
        return self;<br/>
    }<br/>
     <br/>
    ...<br/>
     <br/>
    - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView<br/>
    {<br/>
        [buttonCell drawWithFrame:cellFrame inView:controlView];<br/>
    }<br/>
     <br/>
    - (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView<br/>
    {<br/>
        [buttonCell highlight:flag withFrame:cellFrame inView:controlView];<br/>
    }<br/>
     <br/>
    @end

    Что означает весь этот код? Всё просто — мы создаём объект класса NSButtonCell и отрисовываем его вместо NSPopUpButtonCell. Т.е. программа рисует NSButtonCell (размер которой может соответствовать один в один с заданной картинкой) вместо NSPopUpButtonCell, но функционально это NSPopUpButton.

    Теперь меню… С динамическим меню поступим следующим образом – добавим делегата в NSPopUpButton, который будет выдавать нам меню в тот момент, когда нажата левая кнопка мыши на кнопке.

    Создаём подкласс NSPopUpButton:
    @protocol PopUpDelegate <NSObject><br/>
    @optional<br/>
    - (NSMenu *)menuForPopUp;<br/>
    @end<br/>
     <br/>
     <br/>
    @interface PopUpButton : NSPopUpButton<br/>
    {<br/>
        id<PopUpDelegate> delegate;    <br/>
    }<br/>
     <br/>
    @property (assign) id delegate;<br/>
     <br/>
    @end<br/>
     

    Таким образом мы объявили протокол делегата, который имеет лишь один метод:
    - (NSMenu *)menuForPopUp<br/>
     

    Реализация:
    @implementation PopUpButton<br/>
     <br/>
    @synthesize delegate;<br/>
     <br/>
    - (void)mouseDown:(NSEvent *)event<br/>
    {<br/>
        if([delegate respondsToSelector:@selector(menuForPopUp)])<br/>
        {<br/>
            [self setMenu:[delegate menuForPopUp]];<br/>
        }<br/>
     <br/>
        [super mouseDown:event];<br/>
    }<br/>
     <br/>
    @end<br/>
     


    При нажатии мышкой, перед тем, как событие начнёт обрабатываться объектом класса NSPopUpButton, мы устанавливаем меню делегата этому объекту.

    Теперь можно добавлять нашу кнопку "+" на нашу панель:
    addButton = [[PopUpButton alloc] initWithFrame:NSMakeRect(20192322) pullsDown:YES];<br/>
    addButton.delegate = self;<br/>
    [addButton setCell:[[[PopUpCell alloc] initWithimage:buttonImage] autorelease]];<br/>
    [addButton setMenu:[self menuForPopUp]];<br/>
    [[self mainView] addSubview:addButton];

    Обратите внимание на «addButton.delegate = self». Мы назначили наш основной класс делегатом к addButton. Для этого мы дополнительно реализуем метод:
    - (NSMenu *)menuForPopUp<br/>
    {<br/>
        ...<br/>
    }

    в нашем основном классе.

    Как получить список запущенных программ для меню?

    У объекта класса NSWorkspace есть метод:
    - (NSArray *)runningApplications

    который даст нам массив всех запущенных программ (с полной информацией по ним: название, идентификатор, иконка и пр.).
    NSArray *apps = [[NSWorkspace sharedWorkspace] runningApplications];

    Из этого объекта apps и будет формироваться (см. исходный код) меню для кнопки добавления.

    Наш объект NSTableView отображает ячейки с картинкой, заголовком и подзаголовком. Стандартной ячейки такого типа нет, её необходимо сделать самостоятельно.

    Объявляем класс AppCell:
    @interface AppCell : NSTextFieldCell<br/>
    {<br/>
        NSImage *image;<br/>
        NSString *title;<br/>
        NSString *subtitle;<br/>
    }<br/>
    ...<br/>
    @end<br/>
     

    Самое главное — это переназначить метод отрисовки ячейки:
    - (void)drawInteriorWithFrame:(NSRect)inCellFrame inView:(NSView*)inView<br/>
    {<br/>
        //рисуем image<br/>
        //рисуем title<br/>
        //рисуем subtitle<br/>
        …<br/>
    }<br/>
     

    Класс ячейки готов, осталось только добавить его в наш объект NSTableView. Существует два способа.

    1). Если количество объектов в таблице небольшое, то можно воспользоваться методом делагата (которым является наш основной класс NSPreferencePane) к NSTableView:
    - (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row<br/>
    {<br/>
        AppCell* cell = [[[AppCell alloc] init] autorelease];<br/>
        [cell setEditable:NO];<br/>
        cell.title = [[tableDataSource objectAtIndex:row] objectForKey:@"name"];<br/>
        cell.subtitle = [[tableDataSource objectAtIndex:row] objectForKey:@"ID"];<br/>
        cell.image = [[tableDataSource objectAtIndex:row] objectForKey:@"icon"];<br/>
     <br/>
        return cell;<br/>
    }

    2). Если объектов много, то можно задать универсальную ячейку для всего столбца:
    NSTableColumn* column = [[appTable tableColumns] objectAtIndex:0];
    [column setDataCell:[[[AppCell alloc] init] autorelease]];

    и проставлять необходимые значения в методе делегата к NSTableView:
    - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex<br/>
    {<br/>
        AppCell* cell = (AppCell *)aCell;<br/>
        [cell setEditable:NO];<br/>
        cell.title = [[tableDataSource objectAtIndex:row] objectForKey:@"name"];<br/>
        cell.subtitle = [[tableDataSource objectAtIndex:row] objectForKey:@"ID"];<br/>
        cell.image = [[tableDataSource objectAtIndex:row] objectForKey:@"icon"];<br/>
    }


    Данные (файл со списком программ и скриптами и иконки) сохраняются в соответствующей папке Application Support (пользовательская Библиотека). Получить путь к этой папке можно (и нужно) следующим образом:
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); <br/>
    NSString *appSupportPath = [paths objectAtIndex:0];<br/>
    NSString *prefDir = [appSupportPath stringByAppendingPathComponent:@"info.yuriev.OnAppBehaviour"];

    Иконки мы будем хранить в формате PNG. Есть несколько способов получение PNG данных из NSImage, я приведу один из них (самый универсальный), который используется в программе. Для класса NSImage мы введём новый метод. Вот как выглядит его реализация:
    @implementation NSImage (PNGExport)<br/>
     <br/>
    - (NSData *)PNGData<br/>
    {<br/>
        [self lockFocus];<br/>
        NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(00[self size].width, [self size].height)];<br/>
        [self unlockFocus];<br/>
     <br/>
        NSData *PNGData = [rep representationUsingType:NSPNGFileType properties:nil];<br/>
        [rep release];<br/>
     <br/>
        return PNGData;<br/>
    }<br/>
     <br/>
    @end

    Сохранение данных происходит автоматически, когда изменяется ячейка в таблица. Или через 3 секунды после того, как пользователь изменил какой-либо скрипт. Чтобы это сделать, мы воспользуется методом делегата к NSTextView и отложенным выполнением:
    - (void)textDidChange:(NSNotification *)aNotification<br/>
    {<br/>
        [NSObject cancelPreviousPerformRequestsWithTarget:self];<br/>
        [self performSelector:@selector(saveCurrentData) withObject:nil afterDelay:3.0];<br/>
     <br/>
        saved = NO;<br/>
    }

    Если пользователь изменил какие-либо данные, то через 3 секунды произойдёт автоматическое сохранение. Если в этот период пользователь продолжает редактировать данные, то предыдущий запрос на сохранение отменяется и создаётся новый. Подобный механизм удобно использовать, например, при поиске (чтобы поиск срабатывал лишь через некоторое время после окончания редактирования).

    После сохранения извещаем помощника, что данные изменены:
    NSString *observedObject = @"info.yuriev.OnAppBehaviourHelper";<br/>
    NSDistributedNotificationCenter *center = [NSDistributedNotificationCenter defaultCenter];<br/>
    [center postNotificationName:@"OABReloadScripts" object:observedObject userInfo:nil deliverImmediately:YES];

    Наша программа умеет самостоятельно запускать помощника (если он не запущен) и умеет добавлять его в автозагрузку. Запуск реализовать очень просто (наш помощник находится внутри bundle):
    [[NSWorkspace sharedWorkspace] launchApplicationAtURL:[NSURL fileURLWithPath:helperPath] options:NSWorkspaceLaunchDefault configuration:nil error:NULL];

    За управление элементами автозапуска программ (Login Items) отвечает LaunchServices framework. Он написан на Си. Мы сделаем удобную Objective-C обёртку для наших задач. Объявим класс LoginItems:
    @interface LoginItems : NSObject<br/>
    {<br/>
     <br/>
    }<br/>
     <br/>
    + (void)addApplication:(NSString *)path;<br/>
    + (void)removeApplication:(NSString *)path;<br/>
    + (BOOL)findApplication:(NSString *)path;<br/>
     <br/>
    @end

    Это методы класса. Они не привязаны к какому-либо объекту, их можно вызывать просто [LoginItems addApplication:path].

    Полную реализацию этих методов можно посмотреть в исходных кодах. Вот как, для примера, реализован метод добавления:
    + (void)addApplication:(NSString *)path<br/>
    {<br/>
        LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);<br/>
     <br/>
        if (loginItemsRef)<br/>
        {<br/>
            LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULLNULL(CFURLRef)[NSURL fileURLWithPath:path]NULLNULL);<br/>
            if (itemRef) CFRelease(itemRef);<br/>
            CFRelease(loginItemsRef); <br/>
        } <br/>
    }

    Вот в принципе и вся наша панель.

    Теперь помощник… он очень просто. Т.к. помощник не должен быть виден пользователю, в файле описания программы выставляем ключ LSBackgroundOnly в YES. Это означает, что программа не будет отображаться в доке, в окне Force Quit, не будет отображать меню и пр.

    Самая главная часть инициализация помощника – это получения сообщений от панели, что настройки изменены (для их перезагрузки), и получение сообщений NSWorkspace о состоянии программ:
    NSNotificationCenter *notificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];<br/>
     <br/>
    [notificationCenter addObserver:self selector:@selector(didLaunchApplication:) name:NSWorkspaceDidLaunchApplicationNotification object:nil];<br/>
    [notificationCenter addObserver:self selector:@selector(didTerminateApplication:) name:NSWorkspaceDidTerminateApplicationNotification object:nil];<br/>
    [notificationCenter addObserver:self selector:@selector(didHideApplication:) name:NSWorkspaceDidHideApplicationNotification object:nil];<br/>
    [notificationCenter addObserver:self selector:@selector(didUnhideApplication:) name:NSWorkspaceDidUnhideApplicationNotification object:nil];<br/>
    [notificationCenter addObserver:self selector:@selector(didActivateApplication:) name:NSWorkspaceDidActivateApplicationNotification object:nil];<br/>
    [notificationCenter addObserver:self selector:@selector(didDeactivateApplication:) name:NSWorkspaceDidDeactivateApplicationNotification object:nil];<br/>
     <br/>
    NSString *observedObject = @"info.yuriev.OnAppBehaviourHelper";<br/>
    NSDistributedNotificationCenter *dNotificationCenter = [NSDistributedNotificationCenter defaultCenter];<br/>
     <br/>
    [dNotificationCenter addObserver: self selector: @selector(loadPreferences:) name:@"OABReloadScripts" object:observedObject];

    И основной метод, который выполняет AppleScript, если программа и её действие соответствует настройкам:
    - (void)preformScriptOnApp:(NSRunningApplication *)app forKey:(NSString *)key<br/>
    {<br/>
        if ((!app) || (![app bundleIdentifier])) return;<br/>
     <br/>
        NSString *bundleID = [app bundleIdentifier];<br/>
     <br/>
        for (int i = 0; i < [preferences count]; i++)<br/>
        {<br/>
            if ([[[preferences objectAtIndex:i] objectForKey:@"ID"] isEqualToString:bundleID])<br/>
            {<br/>
                NSString *script = [[preferences objectAtIndex:i] objectForKey:key];<br/>
     <br/>
                if (script && ([script length] > 0))<br/>
                {<br/>
                    NSAppleScript *AScript = [[NSAppleScript alloc] initWithSource:script];<br/>
                    [AScript executeAndReturnError:NULL];<br/>
                    [AScript release];<br/>
                }<br/>
     <br/>
                break;<br/>
            }<br/>
        }<br/>
     <br/>
    }

    Вот такой небольшой проект, а интересного внутри очень много. Надеюсь, что кому-то этот материал окажется полезен. Я же свою пользу уже получил – Dock автоматически скрывается при активации Симулятора iOS :).

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 26

      +5
      Это настолько прекрасно, что даже очень прекрасно. Юрий, вы молодец.

      Убежал ставить и радоваться.
        0
        С одной стороны понятно, что ваш проект решает целый класс задач, вместо решения одной конкретной. Но для этой самой одной конкретной чем не угодил Cmd+Alt+D?
          +3
          Для моего частного случая… Способов скрыть Dock несколько. Но всё это дополнительные действия, которые необходимо делать при запуске Симулятора. А запускаться он может по несколько раз в минуту (когда идёт процесс тестирования). Проще (лично для меня) автоматизировать это действие и забыть, чем постоянно скрывать док вручную (что утомило).
            0
            это дополнительные действия, которые необходимо делать при запуске Симулятора

            не обязательно, можно и автоматизировать — в Info.plist симулятора добавьте ключ LSUIPresentationMode с значением 4. При его активации будет скрываться не только док, но и меню! Подробности где-то в мануалах по созданию приложений для киосков.
              0
              Мне знаком этот параметр. Нет такого параметра LSUIPresentationMode (4 — это скрытие всего), чтобы скрывался только Dock.

              В любом случае, редактировать Info.plist файл какой-либо программы — это самое последнее дело.

                0
                Ну я не сказал бы что это какой-то криминал, у меня к примеру скрипт автоматически его прописывает для любой программы которая попадает в /Applications, чтобы меню не мешалось, в вот док вообще отключен и не используется, ну разве что при загрузке системы чтобы Finder загрузился. Системе от этого как говорится ни тепло, ни холодно.
          0
          спасибо за проделанную работу, отличная статья.
            0
            Спасибо за отзывы и ценный баг-репорт :). Исправил мелкий баг в панели (после удаления какой-либо программы из списка, информация не сообщалась помощнику). Исправленная версия (бинарник и исходники) лежат на прежних местах (время обновления 18:51). Издержки кустарного тестирования ;(.
              0
              Просьба выложить исходники на гитхаб или ещё куда — для удобства ;)
              За программу и туториал — спасибо.
              +3
              а можно по-больше юзкейсов? понимаю что круто, но что-то туго сегодня с фантазией :)
                0
                Например, развлекаловка — установить картинку рабочего стола по настроение при запуске какой-то конкретной программы
                tell application "Finder"
                set desktop picture to {"Macintosh HD:Library:Desktop Pictures:Jaguar Aqua Blue.jpg"} as alias
                end tell
                  +1
                  Или отлавливаете момент запуска какой-то скрытой программы (очередного «помощника»). Можно установить скрипт на её запуск:
                  display dialog "Что за ...?"
                    0
                    Что-то никак не могу заставить работать, панелька добавилась, процесс висит, добавляю адиум и действие при открытии
                    display dialog «Что за ...?»

                    и ничего :)
                    что я делаю не так?

                    10.6.7, Air, английский язык системы
                      0
                      Всё так. Это я немного переборщил. Т.к. AppleScript выполняет помощник и он LSBackgroundOnly (работает только в фоне без возможности отображения UI), то элементы интерфейса он не отображает.

                      Эта команда привязывает диалоговое окно к программе, которая её исполняет. Соответственно, оно не будет показано.

                      Если хотите поэкспериментировать, то можете в файле ~/Library/PreferencePanes/OnAppBehaviour.prefPane/Contents/Resources/OnAppBehaviourHelper.app/Contents/Info.plist (можно просто открыть его в текстовом редакторе) заменить LSBackgroundOnly на LSUIElement. В этом случае помощник сможет отображать UI (в частности без проблем покажет диалоговое окно).
                        0
                        Вместо display dialog «Что за ...?» напишите say "Chto za?"
                          0
                          ага, так работает, спасибо.
                      0
                      При запуске менеджера баз данных пробрасывать туннель на сервер, при закрытии убирать.

                      Можно на старт онлайн-игры или другой требовательной ко всем ресурсам железа штуки повесить выключение ненужных торрентов и фотошопов.
                        0
                        Ага, недавно как раз где-то проскакивала статья, как через посылку SIGSTOP и SIGCONT определённым процессам распределять нагрузку.
                        +1
                        Можно сделать еще такую штуку, как уже тут написали… При запуске определённой программы снижать приоритет какой-нибудь другой тяжелой программы. При закрытии возвращать всё обратно. Как-то так (проверить не могу — под Windows сейчас):

                        (снижаем приоритет до минимума программы AppName)
                        tell application «System Events» to set unixID to unix id of process «AppName»
                        do shell script («renice +20 » & unixID)

                        (возвращаем базовый приоритет программы AppName)
                        tell application «System Events» to set unixID to unix id of process «AppName»
                        do shell script («renice 0 » & unixID)
                        +2
                        Вот книжка по AppleScript для начинающих на русском языке (если нужно).
                          0
                          Вот это да! Такая отличная программа не должна быть бесплатной!
                            0
                            Я выложил исходный код, размещайте платную программу в Mac App Store под своей учётной записью — я не против :).

                            P.S. Только в Mac App Store нельзя выкладывать Preference Pane, придётся переделать под обычную программу :)
                            0
                            Юрий на вашем месте я бы разместил эту программу в Mac App Store по цене 0,99$
                              +1
                              Я предлагаю вам встать на моё место. Размещайте в Mac App Store по любой цене (исходники свободный — пользуйтесь в любых целях). Всё, что заработаете, полностью ваше :).
                                0
                                Вы переоцениваете Mac App Store, там мелкие продажи. К примеру чтобы выбиться в топ 10 даже среди бесплатных приложений достаточно иметь в день сотню-две закачек (личный опыт), платное приложение подобного уровня (имеется ввиду не из разряда must have) принесет вам дай бог сотню долларов, через месяц.

                              Only users with full accounts can post comments. Log in, please.