...

вторник, 20 мая 2014 г.

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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий