GalaPlugin — JS/QML плагин для 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. для тех, кто прочитал до конца, ещё одно видео:

    Similar posts

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

    More
    Ads

    Comments 4

      0
      Предоставление возможности написания плагинов (или скриптов, облегчающих жизнь) для программы на скриптовом языке — это как новый уровень! С чем и поздравляю свою любимую IDE!
        0
        Было бы круто на js писать правила для автоматического рефакторинга или же умные сниппеты.
          0
          Сейчас планирую написать более полезные скриптовые плагины. Один для Jenkins, типа этого QtCreator-Jenkins-Plugin, только на QML. И еще для JIRA какой-нибудь небольшой нотификатор. Пока основная проблема со сборкой под windows.

          Было бы круто на js писать правила для автоматического рефакторинга или же умные сниппеты.

          Если можно, опишите, как это должно выглядеть и работать. Можно будет попробовать сделать.

          0
          Я обновил проект для QtCreator 3.2, скомпилированные версии можно взять отсюда. Также есть видео.

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