# Пишем простой плагин за 20 минут

## Введение

Из этого руководства вы узнаете, как написать простенький плагин (назовём его Auscrie — от "Auto Screenshoter") для LeechCraft, проилюстрировав, таким образом, базовые концепции написания плагинов. Наш плагин сможет снимать скриншот окна LeechCraft и либо сохранять его на диск, либо постить на imagebin. Процесс будет осуществляться с помощью кнопки на панели инструментов.

Вы узнаете:

- как создавать "пустые" плагины и собирать их;
- как добавлять интерфейс пользователя, созданный с помощью Qt Designer;
- о предпочтительных путях взаимодействия с сетью и HTTP в LeechCraft;
- как использовать сообщения LeechCraft для оповещения пользователя о событиях, произошедших в вашем плагине.

## Understanding plugins

Плагины, написанные на C++, являются всего лишь динамическими библиотеками, из которых экспортируется главный экземпляр плагина. Экспорт осуществляется с помощью Q_EXPORT_PLUGIN2. Плагин должен содержать реализацию интерфейса IInfo (для чего используется заголовочный файл `/src/interfaces/iinfo.h` в корне репозитория), чтобы распознаваться как плагин для LeechCraft.

Чтобы узнать больше о написании плагинов для приложений на C++/Qt, обратитесь к руководству "Как создавать плагины Qt".

Также можете обратиться к общему обзору LeechCraft, чтобы узнать больше о его архитектуре и плагинах.

## Пути

Для удобства, будем работать прямо в директории с исходниками, `/src/plugins/auscrie`. Обычно плагины разрабатываются отдельно от дерева сорцов (обратитесь к документации о трудовом процессе, чтобы узнать больше), но единственным отличием является то, что вам придётся соответствующим образом поменять пути. Например, при запуске CMake нужно будет убедиться, что `CMAKE_MODULE_PATH` указывает на директорию, содержащую `FindLeechCraft.cmake`. Переменные CMake устанавливаются с помощью опции командной строки `-D`, например:

    cmake -DCMAKE_MODULE_PATH=/some/path/to/LeechCraft/SDK

Это руководство ориентировано на *NIX. За деталями касательно сборки под Windows обратитесь к соответствующему руководству.

## Каркас

В репозитории LeechCraft есть удобный Python-скрипт, `/tools/scripts/genplugin.py`, генерирующий базовый `CMakeLists.txt` и файлы с объявлениями/определениями экземпляра плагина. Запускается он следующим образом:

    genplugin.py -a "Plugin Author" -p PluginNameWithoutSpaces -i Comma,Separated,List,Of,Base,Interfaces

При запуске с опцией `-h` будет выдана краткая справка.

Итак, создаём директорию `/src/plugins/auscrie`, переходим в неё и запускаем скрипт (заметьте, что в вашем случае путь к genplugin.py может быть другим):

    ../../../tools/scripts/genplugin.py -a "Your Name" -p Auscrie -i IToolBarEmbedder

Наследование от IToolBarEmbedder (`/src/interfaces/itoolbarembedder.h`) необходимо для того, чтобы поместить кнопку на панель инструментов.

Этот скрипт сгенерирует базовые файлы, но их будет достаточно для минимального работающего (точнее, загружающегося) плагина. Попробуем-ка собрать и запустить его! Для этого создадим директорию для сборки, запустим в ней `cmake`, потом `make`, чтобы собрать плагин, а затем `make install` от root, чтобы установить его. Из директории с исходниками выполните:

    		mkdir build
    		cd build
    		cmake ../
    		make
    		sudo make install

Здесь использована сборка вне дерева исходников. Вообще это предпочтительный способ, т.к. можно легко очистить дерево исходников, просто удалив директорию сборки, или содержать несколько сборок с разными конфигурациями.

Теперь запустим LeechCraft, откроем диалог Настройки и выберем вкладку Плагины. В списке должен присутствовать наш плагин. Если это не так, проверьте логи (`~/.leechcraft/warning.log`) и свяжитесь с нами.

## Базовые вещи

Итак, у нас уже есть базовый плагин. Заполним-ка пробелы.

Во-первых, нужно вписать в `GetInfo` какое-то описание типа "Простой плагин для снятия скриншотов".

Теперь нужно создать объект QAction, который будет снимать скриншот. Для начала объявим некоторые внутренние переменные и методы:

- `Proxy_` типа `ICoreProxy_ptr`, которая будет содержать указатель на прокси-объект ядра, передаваемый функции `Init`. Он нужен нам, т.к. через этот объект осуществляется всё взаимодействие с ядром;
- `QAction` *`ShotAction_`, который будет инициировать снятие скриншота;
- слот `makeScreenshot`, который будет запускаться по событию.

Эту инициализацию лучше производить в функции Init, так что давайте допишем туда вот такой код для нашего экшена:

    Proxy_ = proxy;

    Dialog_ = new ShooterDialog (Proxy_->GetMainWindow ());

    ShotAction_ = new QAction (Proxy_->GetIcon ("screenshot"),
            tr ("Make a screenshot"),
            this);
    connect (ShotAction_,
            SIGNAL (triggered ()),
            this,
            SLOT (makeScreenshot ()));

`ICoreProxy_ptr` используется для получения правильной иконки из текущей темы.Когда вы будете разрабатывать свои собственные плагины, вам придётся таскать иконки с собой, пока не попадёте в официальный репозиторий. Прокси также используется для получения указателя на главное окно. О `ShooterDialog` мы поговорим позже.

Теперь давайте заполним заглушку `GetActions`, чтобы она возвращала `ShotAction_`. `GetActions` будет выглядеть примерно так:

    QList<QAction*> Plugin::GetActions () const
    {
        QList<QAction*> result;
        result << ShotAction_;
        return result;
    }

Если сейчас скомпилировать и установить плагин, мы увидим иконку скриншоттера на панели инструментов, но она пока что ничего не делает.

## Инициация получения скриншота

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

    void Plugin::makeScreenshot ()
    {
        if (Dialog_->exec () != QDialog::Accepted)
            return;

        ShotAction_->setEnabled (false);
        QTimer::singleShot (Dialog_->GetTimeout () * 1000,
                this,
                SLOT (shoot ()));
    }

Мы создали Dialog_ в Init() для того, чтобы хранить его между вызовами makeScreenshot(). То, что диалог доступен из любой функции, имеет свои преимущества: например, нет никакой нужды в хранении параметров скриншота, вроде формата или качества, так как мы можем запросить их в любой момент

Написание диалога является довольно простой задачей для любого, кто когда-либо использовал Qt Designer, так что я не стану рассматривать это здесь. Впрочем, стоит рассказать о том, как добавлять формы в проекты, работающие с CMake. Нужно определить переменную, которая будет содержать список форм (в нашем случае это `FORMS`), добавить `.h`- и `.cpp`-файлы в список заголовочных файлов и исходников, вызвать функцию `QT4_WRAP_UI` для запуска на формах команды `uic`, а затем добавить результаты в список зависимостей плагина. Таким образом, середина `CMakeLists.txt` будет выглядеть так:

    SET (SRCS
        auscrie.cpp
        shooterdialog.cpp
        )
    SET (HEADERS
        auscrie.h
        shooterdialog.h
        )
    SET (FORMS
        shooterdialog.ui
        )
    QT4_WRAP_CPP (MOC_SRCS ${HEADERS})
    QT4_WRAP_UI (UIS_H ${FORMS})

    ADD_LIBRARY (leechcraft_auscrie SHARED
        ${COMPILED_TRANSLATIONS}
        ${SRCS}
        ${MOC_SRCS}
        ${UIS_H}
        )

## Скриншоты — это весело.

Давайте наконец взглянем на слот `shoot`.

Для получения доступа к окну мы используем объект `Proxy_`, с помощью которого ранее получали доступ к главному окну. Не забудьте подключить заголовочный файл `QMainWindow`, иначе приведение типов из `QMainWindow`* в `QWidget`* не сработает.

    void Plugin::shoot ()
    {
        ShotAction_->setEnabled (true);

        QWidget *mw = Proxy_->GetMainWindow ();
        QPixmap pm = QPixmap::grabWidget (mw);

        const char *fmt = qPrintable (Dialog_->GetFormat ());
        int quality = Dialog_->GetQuality ();

После этого у нас есть два варианта: либо сохранить файл на диск, либо отправить его на pastebin. Первый немного проще:

        switch (Dialog_->GetAction ())
        {
            case ShooterDialog::ASave:
            {
                QString path = Proxy_->GetSettingsManager ()->
                        Property ("PluginsStorage/Auscrie/SavePath",
                                QDir::currentPath () + "01." + Dialog_->GetFormat ())
                        .toString ();

                QString filename = QFileDialog::getSaveFileName (mw,
                        tr ("Save as"),
                        path,
                        tr ("%1 files (*.%1);;All files (*.*)")
                            .arg (Dialog_->GetFormat ()));

                if (!filename.isEmpty ())
                {
                    pm.save (filename, fmt, quality);
                    Proxy_->GetSettingsManager ()->
                    setProperty ("PluginsStorage/Auscrie/SavePath",
                            filename);
                }
            }
            break;

Здесь использован менеджер настроек из ядра, который, вобщем-то, является обёрткой вокруг `QSettings`. Клавиши, начинающиеся с PluginsStorage, могут быть использованы плагинами, ядро не будет использовать их в своих собственных задачах. Использование менеджера настороек ядра нормально для хранения пары настроек, но если вам нужно больше, особенно если вам нужен отдельный диалог, лучше добавить его в ваш плагин.

Если пользовать выбрал загрузку изображения на imagebin, следует вызвать отдельную функцию, Post(), которая обо всём позаботится:

            case ShooterDialog::AUpload:
            {
                QBuffer buf;
                pm.save (&buf,
                        fmt,
                        quality);
                Post (buf.data ());
            }
    		break;
        }
    }

Теперь нам также следует добавить в `CMakeLists.txt` следующие строки (прямо перед `INCLUDE (${QT_USE_FILE})`):

    SET (QT_USE_QTNETWORK TRUE)

Это даст нашему плагину возможность работать с сетью, делая видимыми включения из модуля QtNetwork и линкуя плагин с библиотекой QtNetwork. Нам она определённо понадобится, т.к. плагин использует QtNetwork (например, `QNetworkAccessManager` и `QNetworkReply`) для постинга скриншотов.

Так как сейчас нас не интересует имплементация загрузки скриншота на imagebin, мы перенесём этот код в отдельный класс `Poster`, и наша функция `Post` будет выглядеть очень просто:

    void Plugin::Post (const QByteArray& data)
    {
        Poster *p = new Poster (data,
                Dialog_->GetFormat (),
                Proxy_->GetNetworkAccessManager ());
        connect (p,
                SIGNAL (finished (QNetworkReply*)),
                this,
                SLOT (handleFinished (QNetworkReply*)));
        connect (p,
                SIGNAL (error (QNetworkReply*)),
                this,
                SLOT (handleError (QNetworkReply*)));
    }

Здесь мы ещё раз использовали `Proxy_`, на этот раз — для получения общего для всего приложения экземпляра `QNetworkAccessManager` с методом `GetNetworkAccessManager`. Использовать общий экземпляр `QNetworkAccessManager` всегда лучше, так как это позволяет получать доступ к общему кэшу и базе cookie, а также предоставляет возможность оптимизировать запросы путём повтороного использования соединений, например.

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

Считается нормальным создание `Poster` в куче, не заботясь об освобождении памяти. Она будет освобождена соответствующими слотами.

Мы подключаемся к сигналам класса `Poster` —

    					finished
    					QNetworkReply *reply

и

    					error
    					QNetworkReply *reply

— для того, чтобы получать извещения о том, что загрузка завершилась (а также чтобы узнать о возможных ошибках). `Poster` генерирует сигнал с параметром — `QNetworkReply`, который изначально излучил соответствующий сигнал.

Взглянем на

    					handleFinished
    					QNetworkReply *reply

(кстати, не забудьте объявить все представленные члены в определении класса):

    void Plugin::handleFinished (QNetworkReply *reply)
    {
        sender ()->deleteLater ();

        QString result = reply->readAll ();

        QRegExp re ("<p>You can find this at <a href='([^<]+)'>([^<]+)</a></p>");
        if (!re.exactMatch (result))
        {
            Entity e = Util::MakeNotification ("Auscrie",
                    tr ("Page parse failed"),
                    PWarning_);
            emit gotEntity (e);
            return;
        }

        QString pasteUrl = re.cap (1);

        Entity e = Util::MakeNotification ("Auscrie",
                tr ("Image pasted: %1, the URL was copied to the clipboard")
                    .arg (pasteUrl),
                PInfo_);

        QApplication::clipboard ()->setText (pasteUrl, QClipboard::Clipboard);
        QApplication::clipboard ()->setText (pasteUrl, QClipboard::Selection);

        emit gotEntity (e);
    }

В первую очередь мы указываем посылающему объекту (экземпляру `Poster`, созданному ранее) удалить себя, как только управление будет возвращено циклу обработки событий. Мы не можем просто написать что-то вроде `delete sender ();`, потому что объекты не могут быть удалены из их же обработчиков сигналов.

После этого мы с помощью `readAll` получаем страницу, которую вернул сервер, и пытаемся извлечь ссылку на наше свежезагруженное изображение с помощью довольно простого регулярного выражения. Если сделать это не удаётся, мы генерируем уведомление об ошибке с приоритетом Warning и прерываем обработку. Заметьте также, что в сигналах классов и слотов, использующих структуры данных LeechCraft, вы должны использовать полностью определённые имена со всеми пространствами имён, например

    					void gotEntity
    					const LeechCraft::Entity& entity

В противном случае система метаобъектов Qt не распознает их.

Здесь также используется система сообщений LeechCraft. С её помощью можно отправлять пользователю оповещения о событиях, происходящих в плагине, вроде ошибок или всяких информационных сообщений. Для создания оповещения используется функция `LeechCraft::Util::MakeNotification`, которая может быть добавлена с помощью `#include <plugininterface/util.h>`. Эта функция принимает три параметра: заголовок, тело и приоритет оповещения. Более детально оповещения и сообщения LeechCraft обсуждаются в документе Обзор.

## Заключение

Вобщем-то, это всё. Теперь у нас есть работающий, полезный плагин. Конечно же, есть вещи, которые стоило бы добавить: например, можно было бы хранить историю всех запощенных скриншотов или показывать красивенький прогрессбар, демонстрирующий загрузку изображения. Также вам наверняка захочется добавить поддержку локализаций, но нашей целью здесь было привыкнуть к концепции плагинов LeechCraft.