2010-12-14

QtCreator: создание расширения редакторов на примере QmlJSEditor

Возникла потребность в активном использовании extension'ов к QtScript, причем в js-виде. Как известно, они хранятся в файлах __init__.js, раскиданных по разным папкам внутри папки qtscriptextension. Поменять название файла, содержащего extension, возможности нет (оно зашито в исходниках Qt). Следовательно, в QtCreator мы видим кучу файлов с одинаковым названием (__init__.js), что не очень удобно и совсем не продуктивно. В качестве решения был реализован небольшой плагин, являющийся по сути надстройкой над QmlJSEditor. На его примере и будет рассказано как отнаследоваться от QmlJSEditor, чтобы не потерять имеющийся функционал.


Описание решения
Итак, какое решение нас устроит? Вполне достаточно чтобы в Open Documents и по Ctrl+Tab появлялись не просто __init__.js всей своей массой, а также было указано в какой директории лежит этот файл (например someext/__init__.js). Ничто не мешает конечно поменять количество папок в префиксе или приписать что-то свое.

MIME-тип
Итак, первое что нам необходимо сделать это прицепиться к нужным нам файлам, предоставляя стандартный QmlJSEditor для всех остальных js-файлов. Ну тут все просто, нам надо всего лишь задать подтип обычного javascript с конкретным паттерном файла (в нашем случае это как раз будет __init__.js).

<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
    <mime-type type="application/x-script-extension-javascript">
        <sub-class-of type="application/javascript"/>
        <comment>Qt Script Extension init file</comment>
        <glob pattern="__init__.js"/>
    </mime-type>
</mime-info>

Наследник IPlugin
Сначала приведу полный код метода initialize(), ну а потом уже разберем что в нем происходит.
bool ScriptExtensionsEditorPluginImpl::initialize(const QStringList &arguments, 
                                                  QString *errorString)
{

    Q_UNUSED(arguments)

    Core::ICore *core = Core::ICore::instance();
    if (!core->mimeDatabase()->addMimeTypes(
                QLatin1String(":/seeditor/SEEditor.mimetypes.xml"), 
                errorString))
        return false;

    m_modelManager = QmlJS::ModelManagerInterface::instance();

    m_editor = new ScriptExtensionsEditorFactory(this);
    addObject(m_editor);

    m_actionHandler = new TextEditor::TextEditorActionHandler(
                "BlackTass.SEEditor",
                TextEditor::TextEditorActionHandler::Format
                | TextEditor::TextEditorActionHandler::UnCommentSelection
                | TextEditor::TextEditorActionHandler::UnCollapseAll);
    m_actionHandler->initializeActions();


    QmlJSEditor::Internal::CodeCompletion *completion = 
            new QmlJSEditor::Internal::CodeCompletion(m_modelManager);
    addAutoReleasedObject(completion);


    // Set completion settings and keep them up to date
    TextEditor::TextEditorSettings *textEditorSettings = 
            TextEditor::TextEditorSettings::instance();
    completion->setCompletionSettings(
                textEditorSettings->completionSettings());
    connect(textEditorSettings, 
            SIGNAL(completionSettingsChanged(TextEditor::CompletionSettings)),
            completion, 
            SLOT(setCompletionSettings(TextEditor::CompletionSettings)));

    return true;
}
Итак, задаем xml для MIME, создаем фабрику наших редакторов и инициализируем action'ы. А вот дальше начинается работа с самым приятным, что есть в QmlJSeditor - автокомплит. К сожалению, класс, отвечающий за автокомплит в QmlJSEditor является внутренним и поэтому приходится прилинковывать его к нашему плагину. Для этого в про-файле надо добавить следующие строки:
SOURCES += \
    $$QTCREATOR_SOURCES/src/plugins/qmljseditor/qmljscodecompletion.cpp

HEADERS +=\
    $$QTCREATOR_SOURCES/src/plugins/qmljseditor/qmljscodecompletion.h
Ну и под конец настраиваем сигналы для отлова изменения настроек автокомплита.
Также нам необходимо сделать метод, который будет непосредственно настраивать редактор, назовем его initializeEditor():
void ScriptExtensionsEditorPluginImpl::initializeEditor(
    ScriptExtensionsEditor *editor)
{
    QTC_ASSERT(m_instance, /**/);

    m_actionHandler->setupActions(editor);

    TextEditor::TextEditorSettings::instance()->initializeEditor(editor);

    // auto completion
    connect(editor,
            SIGNAL(requestAutoCompletion(TextEditor::ITextEditable*, bool)),
            TextEditor::CompletionSupport::instance(),
            SLOT(autoComplete(TextEditor::ITextEditable*, bool)));

}

Он не делает ничего сверхъестественного, только самое необходимое: настраивает внешний вид самого редактора и подключает сигнал для вызова автокомплита. В самом QmlJSEditor в этом методе также настраивается QuickFix, но, так как пока что они существуют только для Qml, было решено их не использовать.

Непосредственно редактор
Для реализации редактора в QtCreator, как известно нужны минимум три класса: фабрика, сам редактор и editable (который отслеживает различные операции с файлами и взаимодействия с редактором). На фабрике особо останавливаться не буду, напомню только что в createEditor() необходимо вызвать initializeEditor() из экземпляра класса рассмотренного выше.
В editable необходимо не забыть возвращать правильный контекст (который должен содержать наш плагин и текстовый редактор), чтобы правильно работали все горячие клавиши:
ScriptExtensionsEditorEditable::ScriptExtensionsEditorEditable(
    ScriptExtensionsEditor *editor)
  : QmlJSEditor::QmlJSEditorEditable(editor)
{
    m_context.add("BlackTass.SEEditor");
    m_context.add(TextEditor::Constants::C_TEXTEDITOR);
}

Core::Context ScriptExtensionsEditorEditable::context() const
{
    return m_context;
}
В самом же редакторе мы подключимся к сигналу, который испускается при смене имени файла (и следовательно по этому сигналу меняются все обозначения открытого файла в различных отображениях):
Сам слот достаточно прост. Он проверяет является ли текущий файл extension'ом (а это очень легко проверить по пути файла, в нем должна быть папка qtscriptextension и сам файл должен называться __init__.js) и если подходит, то дописывает перед названием файла имя папки, в которой он лежит. После этого в очередь событий кладется изменение displayName (который и отвечает за надписи во всех нужных нам местах) для того чтобы наше изменение было гарантированно последним.
void ScriptExtensionsEditor::onTitleChanged(const QString &title)
{
    Q_UNUSED(title);
    QString fileName = baseTextDocument()->fileName();
    if (fileName.contains(QRegExp("qtscriptextension.*__init__\\.js$")))
    {
        QFileInfo fi(fileName);
        QString realTitle = fi.dir().dirName()+"/"+fi.fileName();
        QMetaObject::invokeMethod(this, "setDisplayName",
                                  Qt::QueuedConnection,
                                  Q_ARG(QString, realTitle));
    }

}

Итог
В итоге мы имеем небольшой плагин (суммарное количество кода в районе 350 строк), который не нарушает приятный функционал QmlJSEditor и дает возможность легче работать с extension'ами.
Внимание! Подобные плагины не будут работать в 2.0 и скорее всего не будут в 2.1. В них editable у QmlJSEditor являлся также внутренним классом и только потом его заэкспортили в библиотеку.

2 комментария:

  1. Делайте отступы в коде, а то он становится нечетабельным.
    А так все кул)

    ОтветитьУдалить
  2. Хехе, да они съедаются при копипасте из редактора. Постоянно забываю расставлять по новой.

    ОтветитьУдалить