GalaPlugin — JS/QML плагин для QtCreator

в 6:41, , рубрики: c++, open source, plugins, qt, QtCreator, метки: ,

После прочтения поста Использование панели режимов QtCreator + 2 плагина, у меня возникла идея попробовать создать плагин, способный расширять функциональность QtCreator'а с помощью JavaScript и QML. Появился проект GalaPlugin.

Вот небольшая демка того, что получилось.

Как это работает

Идея для этого плагина достаточно проста — при инициализации, в функции bool initialize(const QStringList &arguments, QString *errorString) мы ищем в папке плагина специальные «скриптовые» плагины. Это JavaScript файлы с расширением *.gala, в которых могут быть следующие конструкции:

  1. Обязательная функция initialize(), которая вызывается сразу после загрузки скрипта.
  2. Опциональная функция extensionsInitialized(), которая будет вызвана в соответствующей функции основного плагина (когда все плагины загружены).
  3. Опциональная функция aboutToShutdown(), которая будет вызвана перед закрытием QtCreator'а.
  4. Опциональная целочисленная переменная galaPluginOrder, которая используется для переупорядочивания скриптовых плагинов при загрузке.
  5. Опциональная булева переменная galaPluginDisable, с помощью которой можно игнорировать скриптовые плагины.
  6. Опциональная булева переменная galaPluginTrace, которая позволяет логировать все вызовы оберток (полезно при отладке).

Вот пример скриптового плагина SaveAllBttn.gala, который добавляет кнопку «Сохранить всё» на панель режимов

SaveAllBttn.gala

var galaPluginOrder = 1;
var galaPluginTrace = true;
function saveAllAction() {
    var docs = editorManager.documents();
    for (var i = 0; i < docs.length; ++i) {
        var doc = docs[i];
        if (doc.isModified()) {
            doc.save("", false);
        }
    }
}
function createSaveAllButton() {
    var bttn = galaAPI.createQObject("QPushButton", modeManager);
    bttn.flat = true;
    bttn.text = "Save All";
    bttn.focusPolicy = 0;
    bttn.styleSheet = "QPushButton {color: white; }";
    // disable button minimum width
    bttn.sizePolicy = galaAPI.sizePolicy(13, 0, 1);
    bttn.clicked.connect(saveAllAction);

    return bttn;
}
function initialize() {
    modeManager.addWidget(createSaveAllButton());
    galaAPI.debug("Success initialize");
}
function extensionsInitialized() {
    galaAPI.debug("Success extensionsInitialized");
}
function aboutToShutdown() {
    galaAPI.debug("Success aboutToShutdown");
}

Кроме того доступны еще 4 скриптовых плагина:

  • CloseAllBttn — добавляет на панель режимов кнопку закрытия всех документов
  • CloseAllToolMenu — добавляет пункт меню Tools->MyPlugin->Close All
  • Clock — добавляет анимированные цифровые часы на панель режимов
  • RelaxTracker — добавляет специальный QML объект, который через определенные промежутки времени меняет зеленый прямоугольник на красный, с моргающей надписью «Break» (идея из оригинальной статьи)
  • Weather — добавляет краткую информацию по погоде на панель режимов

Что бы воспользоваться QtCreator API из скриптинга, я создал обёртки вокруг необходимых классов (таких как Core::ICore, Core::Command, Core::ActionManager и других). Процесс создания обёрток почти механический: создаем класс наследник QObject, передаём и сохраняем в нём указатель на класс из QtCreator API, и все public методы исходного класса перевызываем в обёртке в секции public slots.

Вот небольшой пример:

class GModeManager : public GWrapper
{
    Q_OBJECT

public:
    GModeManager(QJSEngine* jsEngine)
        : GWrapper(jsEngine),
          m_owner(qobject_cast<Core::ModeManager*>(Core::ModeManager::instance()))
    {
        Q_ASSERT(m_owner);
    }
    ~GModeManager() {}

    Core::ModeManager* owner1() { return m_owner; }

public slots:
    QJSValue owner() { return m_jsEngine->toScriptValue(m_owner); }

    QJSValue currentMode() { return m_jsEngine->toScriptValue(m_owner->currentMode()); }
    QJSValue mode(QString id) { return m_jsEngine->toScriptValue(m_owner->mode(str2id(id))); }

    void addAction(QAction *action, int priority) { m_owner->addAction(action, priority); }
    void addProjectSelector(QAction *action) { m_owner->addProjectSelector(action); }
    void addWidget(QWidget *widget) { m_owner->addWidget(widget); }

    void activateMode(QString id) { m_owner->activateMode(str2id(id)); }
    void setFocusToCurrentMode() { m_owner->setFocusToCurrentMode(); }
    bool isModeSelectorVisible() { return m_owner->isModeSelectorVisible(); }

    void setModeSelectorVisible(bool visible) { m_owner->setModeSelectorVisible(visible); }

private:
    Core::ModeManager* m_owner;
};

Небольшая сложность с функциями, возвращающими список указателей на объекты: их нужно упаковывать в JavaScript Array значения. Вот пример реализации функции editorManager.documents():

QJSValue documents()
{
    QList<Core::IDocument *> documents = m_owner->documentModel()->openedDocuments();
    QJSValue array = m_jsEngine->newArray(documents.size());

    for (quint32 i = 0; i < (quint32)documents.size(); ++i)
    {
        array.setProperty(i, m_jsEngine->newQObject(new GDocument(m_jsEngine, documents[i])));
    }

    return array;
}

На данный момент в окружении JavaScript/QML можно пользоваться следующими глобальными объектами:

  1. core — представляет Core::ICore::instance()
  2. messageManager — представляет Core::MessageManager::instance()
  3. actionManager — представляет Core::ActionManager::instance()
  4. editorManager — представляет Core::EditorManager:instance()
  5. modeManager — представляет Core::ModeManager::instance()
  6. galaAPI — служит точкой доступа к вспомогательным полезным функциям

В ходе написания плагина я столкнулся со следующими проблемами.
Если слот возвращает указатель на QObject наследованный объект, то JavaScript окружение берёт владение этим объектом на себя. Это полезно, если слот создает обёртку и возвращает её в JS код. Например

GCommand *command(QString id) { return new GCommand(m_jsEngine, m_owner->command(str2id(id))); }

Если же слот возвращает внутренний объект QtCreator'а, то владеть им JS окружение не должно. В таких случаях нужно возвращать не указатель на объект, а QJSValue, в которое и завернуть указатель.

// возвращает QMenu*
QJSValue menu() const { return m_jsEngine->toScriptValue(static_cast<QObject*>(m_owner->menu())); }

Еще одна проблема при «пробросе» сигналов в JS заключается в параметрах по умолчанию и одинаковых именах методов.

Когда некоторый метод foo вызывается из JS, то в мета-системе объекта ищется метод с таким именем и вызывается первый найденный. Никакого сопоставления по количеству (и, тем более, типам) параметров нет. При этом, если сигнал имеет параметры по умолчанию, moc генерирует несколько мета-методов. Например, для сигнала void foo(int a, int b = 0, int c = 1); будут сгенерированы три мета-метода

void foo(int a);
void foo(int a, int b);
void foo(int a, int b, int c);

Причём именно в таком порядке — сначала самая короткая версия. Таким образом параметры по умолчанию в JS использовать не получается и необходимо передавать все параметры вручную. А методы с одинаковыми именами делать уникальными.

Заключение

Данный плагин позволяет расширять функциональность QtCreator'а очень легко. Я вижу основное использование скриптовых плагинов в создании визуальных элементов на пенели режимов, создание тулбаров и пунктов меню для часто используемых или специфичных команд. Возможность встраивать QML объекты даёт широкие возможности. Можно легко создать QML view, который будет следить за каким-либо web сервисом, будь то погода, курс валюты, новые статьи на любимом ресурсе, счёт в спортивных соревнованиях, статус сборки и т.п. Я не профессионал по QML, но в сети можно найти много интересных примеров.

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

Так же прошу помощи в компиляции плагина под разные платформы. С Windows беда, так же легко собирать, как и под Linux не получается. Под MacOS вообще нет возможности собирать.

P.S. для тех, кто прочитал до конца, ещё одно видео:

Автор: lexxmark

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js