После прочтения поста Использование панели режимов QtCreator + 2 плагина, у меня возникла идея попробовать создать плагин, способный расширять функциональность QtCreator'а с помощью JavaScript и QML. Появился проект GalaPlugin.
Вот небольшая демка того, что получилось.
Как это работает
Идея для этого плагина достаточно проста — при инициализации, в функции bool initialize(const QStringList &arguments, QString *errorString)
мы ищем в папке плагина специальные «скриптовые» плагины. Это JavaScript файлы с расширением *.gala, в которых могут быть следующие конструкции:
- Обязательная функция
initialize()
, которая вызывается сразу после загрузки скрипта. - Опциональная функция
extensionsInitialized()
, которая будет вызвана в соответствующей функции основного плагина (когда все плагины загружены). - Опциональная функция
aboutToShutdown()
, которая будет вызвана перед закрытием QtCreator'а. - Опциональная целочисленная переменная
galaPluginOrder
, которая используется для переупорядочивания скриптовых плагинов при загрузке. - Опциональная булева переменная
galaPluginDisable
, с помощью которой можно игнорировать скриптовые плагины. - Опциональная булева переменная
galaPluginTrace
, которая позволяет логировать все вызовы оберток (полезно при отладке).
Вот пример скриптового плагина 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 можно пользоваться следующими глобальными объектами:
- core — представляет
Core::ICore::instance()
- messageManager — представляет
Core::MessageManager::instance()
- actionManager — представляет
Core::ActionManager::instance()
- editorManager — представляет
Core::EditorManager:instance()
- modeManager — представляет
Core::ModeManager::instance()
- 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