diff --git a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h index 5a65d8c8e0e..792666ec364 100644 --- a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h +++ b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h @@ -72,6 +72,7 @@ const char layoutGridLayoutCommandId[] = "LayoutGridLayout"; const char layoutFillWidthCommandId[] = "LayoutFillWidth"; const char layoutFillHeightCommandId[] = "LayoutFillHeight"; const char goIntoComponentCommandId[] = "GoIntoComponent"; +const char mergeTemplateCommandId[] = "MergeTemplate"; const char goToImplementationCommandId[] = "GoToImplementation"; const char addSignalHandlerCommandId[] = "AddSignalHandler"; const char moveToComponentCommandId[] = "MoveToComponent"; @@ -116,6 +117,7 @@ const char resetSizeDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", const char resetPositionDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Reset Position"); const char goIntoComponentDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Go into Component"); +const char mergeTemplateDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Merge File With Template"); const char goToImplementationDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Go to Implementation"); const char addSignalHandlerDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Add New Signal Handler"); const char moveToComponentDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Move Component into Separate File"); diff --git a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp index 3bc71e05abb..2dbf8b00b44 100644 --- a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp @@ -1180,6 +1180,16 @@ void DesignerActionManager::createDefaultDesignerActions() &singleSelection, &singleSelection)); + addDesignerAction(new ModelNodeContextMenuAction( + mergeTemplateCommandId, + mergeTemplateDisplayName, + {}, + rootCategory, + {}, + 30, + &mergeWithTemplate, + &SelectionContextFunctors::always)); + addDesignerAction(new ActionGroup( "", genericToolBarCategory, diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp index 4fc5a53a5c4..24ea9cc47bd 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include @@ -66,11 +67,15 @@ #include #include +#include #include +#include #include +#include #include #include +#include #include #include @@ -1305,6 +1310,176 @@ void addCustomFlowEffect(const SelectionContext &selectionContext) }); } +static QString fromCamelCase(const QString &s) +{ + static QRegularExpression regExp1 {"(.)([A-Z][a-z]+)"}; + static QRegularExpression regExp2 {"([a-z0-9])([A-Z])"}; + + QString result = s; + result.replace(regExp1, "\\1 \\2"); + result.replace(regExp2, "\\1 \\2"); + + return result; +} + +QString getTemplateDialog(const Utils::FilePath &projectPath) +{ + + const Utils::FilePath templatesPath = projectPath.pathAppended("templates"); + + const QStringList templateFiles = QDir(templatesPath.toString()).entryList({"*.qml"}); + + QStringList names; + + for (const QString &name : templateFiles) { + QString cleanS = name; + cleanS.remove(".qml"); + names.append(fromCamelCase(cleanS)); + } + + QDialog *dialog = new QDialog(Core::ICore::dialogParent()); + dialog->setMinimumWidth(480); + dialog->setModal(true); + + dialog->setWindowTitle(QCoreApplication::translate("TemplateMerge","Merge With Template")); + + auto mainLayout = new QGridLayout(dialog); + + auto comboBox = new QComboBox; + + comboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + + for (const QString &templateName : names) + comboBox->addItem(templateName); + + QString templateFile; + + auto setTemplate = [comboBox, &templateFile](const QString &newFile) { + if (comboBox->findText(newFile) < 0) + comboBox->addItem(newFile); + + comboBox->setCurrentText(newFile); + templateFile = newFile; + }; + + QPushButton *browseButton = new QPushButton(QCoreApplication::translate("TemplateMerge", "&Browse..."), dialog); + + mainLayout->addWidget(new QLabel(QCoreApplication::translate("TemplateMerge", "Template:")), 0, 0); + mainLayout->addWidget(comboBox, 1, 0, 1, 3); + mainLayout->addWidget(browseButton, 1, 3, 1 , 1); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok + | QDialogButtonBox::Cancel); + mainLayout->addWidget(buttonBox, 2, 2, 1, 2); + + QObject::connect(browseButton, &QPushButton::clicked, dialog, [setTemplate, &projectPath]() { + + const QString newFile = QFileDialog::getOpenFileName(Core::ICore::dialogParent(), + QCoreApplication::translate("TemplateMerge", "Browse Template"), + projectPath.toString(), + "*.qml"); + if (!newFile.isEmpty()) + setTemplate(newFile); + }); + + QObject::connect(buttonBox, &QDialogButtonBox::accepted, dialog, [dialog](){ + dialog->accept(); + dialog->deleteLater(); + }); + + QString result; + + QObject::connect(buttonBox, &QDialogButtonBox::rejected, dialog, [dialog](){ + dialog->reject(); + dialog->deleteLater(); + }); + + QObject::connect(dialog, &QDialog::accepted, [&result, comboBox](){ + result = comboBox->currentText(); + }); + + dialog->exec(); + + if (!result.isEmpty() && !QFileInfo(result).exists()) { + result = templateFiles.at(names.indexOf(result)); + result = templatesPath.pathAppended(result).toString(); + } + + return result; +} + +void styleMerge(const SelectionContext &selectionContext, const QString &templateFile) +{ + Model *parentModel = selectionContext.view()->model(); + + QTC_ASSERT(parentModel, return); + + QScopedPointer templateModel(Model::create("QtQuick.Item", 2, 1, parentModel)); + Q_ASSERT(templateModel.data()); + + templateModel->setFileUrl(QUrl::fromLocalFile(templateFile)); + + QPlainTextEdit textEditTemplate; + Utils::FileReader reader; + + QTC_ASSERT(reader.fetch(templateFile), return); + QString qmlTemplateString = QString::fromUtf8(reader.data()); + QString imports; + + for (const Import &import : parentModel->imports()) + imports += QStringLiteral("import ") + import.toString(true) + QLatin1Char(';') + QLatin1Char('\n'); + + textEditTemplate.setPlainText(imports + qmlTemplateString); + NotIndentingTextEditModifier textModifierTemplate(&textEditTemplate); + + QScopedPointer templateRewriterView(new RewriterView(RewriterView::Amend, nullptr)); + templateRewriterView->setTextModifier(&textModifierTemplate); + templateModel->attachView(templateRewriterView.data()); + templateRewriterView->setCheckSemanticErrors(false); + + ModelNode templateRootNode = templateRewriterView->rootModelNode(); + QTC_ASSERT(templateRootNode.isValid(), return); + + QScopedPointer styleModel(Model::create("QtQuick.Item", 2, 1, parentModel)); + Q_ASSERT(styleModel.data()); + + styleModel->setFileUrl(QUrl::fromLocalFile(templateFile)); + + QPlainTextEdit textEditStyle; + RewriterView *parentRewriterView = selectionContext.view()->model()->rewriterView(); + QTC_ASSERT(parentRewriterView, return); + textEditStyle.setPlainText(parentRewriterView->textModifierContent()); + NotIndentingTextEditModifier textModifierStyle(&textEditStyle); + + QScopedPointer styleRewriterView(new RewriterView(RewriterView::Amend, nullptr)); + styleRewriterView->setTextModifier(&textModifierStyle); + styleModel->attachView(styleRewriterView.data()); + + StylesheetMerger merger(templateRewriterView.data(), styleRewriterView.data()); + + try { + merger.merge(); + } catch (Exception &e) { + e.showException(); + } + + try { + parentRewriterView->textModifier()->textDocument()->setPlainText(templateRewriterView->textModifierContent()); + } catch (Exception &e) { + e.showException(); + } +} + +void mergeWithTemplate(const SelectionContext &selectionContext) +{ + const Utils::FilePath projectPath = Utils::FilePath::fromString(baseDirectory(selectionContext.view()->model()->fileUrl())); + + const QString templateFile = getTemplateDialog(projectPath); + + if (QFileInfo(templateFile).exists()) + styleMerge(selectionContext, templateFile); +} + } // namespace Mode } //QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h index a3048e34ca6..afd8416bf9f 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h @@ -81,6 +81,7 @@ void addCustomFlowEffect(const SelectionContext &selectionState); void setFlowStartItem(const SelectionContext &selectionContext); void addToGroupItem(const SelectionContext &selectionContext); void selectFlowEffect(const SelectionContext &selectionContext); +void mergeWithTemplate(const SelectionContext &selectionContext); } // namespace ModelNodeOperationso } //QmlDesigner