forked from qt-creator/qt-creator
QmlDesigner: Add bundle importer
Bundle importer can be used for importing QML components from a bundle that contains various components. Task-number: QDS-7499 Change-Id: Ic8a122215a52912c993fba62a3bbc7cc068619db Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
@@ -336,6 +336,7 @@ extend_qtc_plugin(QmlDesigner
|
||||
materialbrowserview.cpp materialbrowserview.h
|
||||
materialbrowserwidget.cpp materialbrowserwidget.h
|
||||
materialbrowsermodel.cpp materialbrowsermodel.h
|
||||
bundleimporter.cpp bundleimporter.h
|
||||
)
|
||||
|
||||
extend_qtc_plugin(QmlDesigner
|
||||
|
@@ -0,0 +1,235 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "bundleimporter.h"
|
||||
|
||||
#include "import.h"
|
||||
#include "model.h"
|
||||
#include "qmldesignerconstants.h"
|
||||
#include "qmldesignerplugin.h"
|
||||
#include "rewritingexception.h"
|
||||
|
||||
#include <qmljs/qmljsmodelmanagerinterface.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
#include <QStringList>
|
||||
|
||||
using namespace Utils;
|
||||
|
||||
namespace QmlDesigner::Internal {
|
||||
|
||||
BundleImporter::BundleImporter(const QString &bundleDir,
|
||||
const QString &bundleId,
|
||||
const QStringList &sharedFiles,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_bundleDir(FilePath::fromString(bundleDir))
|
||||
, m_bundleId(bundleId)
|
||||
, m_sharedFiles(sharedFiles)
|
||||
{
|
||||
m_importTimer.setInterval(200);
|
||||
connect(&m_importTimer, &QTimer::timeout, this, &BundleImporter::handleImportTimer);
|
||||
m_moduleName = QStringLiteral("%1.%2").arg(
|
||||
QLatin1String(Constants::COMPONENT_BUNDLES_FOLDER),
|
||||
m_bundleId).mid(1); // Chop leading slash
|
||||
}
|
||||
|
||||
// Returns empty string on success or an error message on failure.
|
||||
// Note that there is also an asynchronous portion to the import, which will only
|
||||
// be done if this method returns success. Once the asynchronous portion of the
|
||||
// import is completed, importFinished signal will be emitted.
|
||||
QString BundleImporter::importComponent(const QString &qmlFile,
|
||||
const QStringList &files)
|
||||
{
|
||||
FilePath bundleImportPath = QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath();
|
||||
if (bundleImportPath.isEmpty())
|
||||
return "Failed to resolve current project path";
|
||||
|
||||
const QString projectBundlePath = QStringLiteral("%1%2/%3").arg(
|
||||
QLatin1String(Constants::DEFAULT_ASSET_IMPORT_FOLDER),
|
||||
QLatin1String(Constants::COMPONENT_BUNDLES_FOLDER),
|
||||
m_bundleId).mid(1); // Chop leading slash
|
||||
bundleImportPath = bundleImportPath.resolvePath(projectBundlePath);
|
||||
|
||||
if (!bundleImportPath.exists()) {
|
||||
if (!bundleImportPath.createDir())
|
||||
return QStringLiteral("Failed to create bundle import folder: '%1'").arg(bundleImportPath.toString());
|
||||
}
|
||||
|
||||
for (const QString &file : qAsConst(m_sharedFiles)) {
|
||||
FilePath target = bundleImportPath.resolvePath(file);
|
||||
if (!target.exists()) {
|
||||
FilePath parentDir = target.parentDir();
|
||||
if (!parentDir.exists() && !parentDir.createDir())
|
||||
return QStringLiteral("Failed to create folder for: '%1'").arg(target.toString());
|
||||
FilePath source = m_bundleDir.resolvePath(file);
|
||||
if (!source.copyFile(target))
|
||||
return QStringLiteral("Failed to copy shared file: '%1'").arg(source.toString());
|
||||
}
|
||||
}
|
||||
|
||||
FilePath qmldirPath = bundleImportPath.resolvePath(QStringLiteral("qmldir"));
|
||||
QFile qmldirFile(qmldirPath.toString());
|
||||
|
||||
QString qmldirContent;
|
||||
if (qmldirPath.exists()) {
|
||||
if (!qmldirFile.open(QIODeviceBase::ReadOnly))
|
||||
return QStringLiteral("Failed to open qmldir file for reading: '%1'").arg(qmldirPath.toString());
|
||||
qmldirContent = QString::fromUtf8(qmldirFile.readAll());
|
||||
qmldirFile.close();
|
||||
} else {
|
||||
qmldirContent.append("module ");
|
||||
qmldirContent.append(m_moduleName);
|
||||
qmldirContent.append('\n');
|
||||
}
|
||||
|
||||
FilePath qmlSourceFile = FilePath::fromString(qmlFile);
|
||||
const bool qmlFileExists = qmlSourceFile.exists();
|
||||
const QString qmlType = qmlSourceFile.baseName();
|
||||
m_pendingTypes.append(QStringLiteral("%1.%2")
|
||||
.arg(QLatin1String(Constants::COMPONENT_BUNDLES_FOLDER).mid(1), qmlType));
|
||||
if (!qmldirContent.contains(qmlFile)) {
|
||||
QSaveFile qmldirSaveFile(qmldirPath.toString());
|
||||
if (!qmldirSaveFile.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate))
|
||||
return QStringLiteral("Failed to open qmldir file for writing: '%1'").arg(qmldirPath.toString());
|
||||
|
||||
qmldirContent.append(qmlType);
|
||||
qmldirContent.append(" 1.0 ");
|
||||
qmldirContent.append(qmlFile);
|
||||
qmldirContent.append('\n');
|
||||
|
||||
qmldirSaveFile.write(qmldirContent.toUtf8());
|
||||
qmldirSaveFile.commit();
|
||||
}
|
||||
|
||||
QStringList allFiles;
|
||||
allFiles.append(files);
|
||||
allFiles.append(qmlFile);
|
||||
for (const QString &file : qAsConst(allFiles)) {
|
||||
FilePath target = bundleImportPath.resolvePath(file);
|
||||
FilePath parentDir = target.parentDir();
|
||||
if (!parentDir.exists() && !parentDir.createDir())
|
||||
return QStringLiteral("Failed to create folder for: '%1'").arg(target.toString());
|
||||
|
||||
FilePath source = m_bundleDir.resolvePath(file);
|
||||
if (target.exists()) {
|
||||
if (source.lastModified() == target.lastModified())
|
||||
continue;
|
||||
target.removeFile(); // Remove existing file for update
|
||||
}
|
||||
if (!source.copyFile(target))
|
||||
return QStringLiteral("Failed to copy file: '%1'").arg(source.toString());
|
||||
}
|
||||
|
||||
m_fullReset = !qmlFileExists;
|
||||
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
||||
Model *model = doc ? doc->currentModel() : nullptr;
|
||||
if (!model)
|
||||
return "Model not available, cannot add import statement or update code model";
|
||||
|
||||
Import import = Import::createLibraryImport(m_moduleName, "1.0");
|
||||
if (!model->hasImport(import)) {
|
||||
if (model->possibleImports().contains(import)) {
|
||||
m_importAddPending = false;
|
||||
try {
|
||||
model->changeImports({import}, {});
|
||||
} catch (const RewritingException &) {
|
||||
// No point in trying to add import asynchronously either, so just fail out
|
||||
return QStringLiteral("Failed to add import statement for: '%1'").arg(m_moduleName);
|
||||
}
|
||||
} else {
|
||||
// If import is not yet possible, import statement needs to be added asynchronously to
|
||||
// avoid errors, as code model update takes a while. Full reset is not necessary
|
||||
// in this case, as new import directory appearing will trigger scanning of it.
|
||||
m_importAddPending = true;
|
||||
m_fullReset = false;
|
||||
}
|
||||
}
|
||||
m_importTimerCount = 0;
|
||||
m_importTimer.start();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void BundleImporter::handleImportTimer()
|
||||
{
|
||||
auto handleFailure = [this]() {
|
||||
m_importTimer.stop();
|
||||
m_fullReset = false;
|
||||
m_importAddPending = false;
|
||||
m_importTimerCount = 0;
|
||||
m_pendingTypes.clear();
|
||||
emit importFinished({});
|
||||
};
|
||||
|
||||
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
||||
Model *model = doc ? doc->currentModel() : nullptr;
|
||||
if (!model || ++m_importTimerCount > 100) {
|
||||
handleFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_fullReset) {
|
||||
// Force code model reset to notice changes to existing module
|
||||
auto modelManager = QmlJS::ModelManagerInterface::instance();
|
||||
if (modelManager)
|
||||
modelManager->resetCodeModel();
|
||||
m_fullReset = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_importAddPending) {
|
||||
try {
|
||||
Import import = Import::createLibraryImport(m_moduleName, "1.0");
|
||||
if (model->possibleImports().contains(import)) {
|
||||
model->changeImports({import}, {});
|
||||
m_importAddPending = false;
|
||||
}
|
||||
} catch (const RewritingException &) {
|
||||
// Import adding is unlikely to succeed later, either, so just bail out
|
||||
handleFailure();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect when the code model has the new material(s) fully available
|
||||
const QStringList pendingTypes = m_pendingTypes;
|
||||
for (const QString &pendingType : pendingTypes) {
|
||||
NodeMetaInfo metaInfo = model->metaInfo(pendingType.toUtf8());
|
||||
if (metaInfo.isValid() && !metaInfo.superClasses().isEmpty()) {
|
||||
m_pendingTypes.removeAll(pendingType);
|
||||
emit importFinished(metaInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_pendingTypes.isEmpty()) {
|
||||
m_importTimer.stop();
|
||||
m_importTimerCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner::Internal
|
@@ -0,0 +1,72 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include "nodemetainfo.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace QmlDesigner::Internal {
|
||||
|
||||
class BundleImporter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BundleImporter(const QString &bundleDir,
|
||||
const QString &bundleId,
|
||||
const QStringList &sharedFiles,
|
||||
QObject *parent = nullptr);
|
||||
~BundleImporter() = default;
|
||||
|
||||
QString importComponent(const QString &qmlFile,
|
||||
const QStringList &files);
|
||||
signals:
|
||||
// The metaInfo parameter will be invalid if an error was encountered during
|
||||
// asynchronous part of the import. In this case all remaining pending imports have been
|
||||
// terminated, and will not receive separate importFinished notifications.
|
||||
void importFinished(const QmlDesigner::NodeMetaInfo &metaInfo);
|
||||
|
||||
private:
|
||||
void handleImportTimer();
|
||||
|
||||
Utils::FilePath m_bundleDir;
|
||||
QString m_bundleId;
|
||||
QString m_moduleName;
|
||||
QStringList m_sharedFiles;
|
||||
QTimer m_importTimer;
|
||||
int m_importTimerCount = 0;
|
||||
bool m_importAddPending = false;
|
||||
bool m_fullReset = false;
|
||||
QStringList m_pendingTypes;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner::Internal
|
@@ -83,6 +83,7 @@ const char EDIT3D_BACKGROUND_COLOR_ACTIONS[] = "QmlDesigner.Editor3D.BackgroundC
|
||||
|
||||
|
||||
const char QML_DESIGNER_SUBFOLDER[] = "/designer/";
|
||||
const char COMPONENT_BUNDLES_FOLDER[] = "/ComponentBundles";
|
||||
const char QUICK_3D_ASSETS_FOLDER[] = "/Quick3DAssets";
|
||||
const char QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX[] = "_libicon";
|
||||
const char QUICK_3D_ASSET_ICON_DIR[] = "_icons";
|
||||
|
@@ -689,6 +689,8 @@ Project {
|
||||
"materialbrowser/materialbrowserview.h",
|
||||
"materialbrowser/materialbrowserwidget.cpp",
|
||||
"materialbrowser/materialbrowserwidget.h",
|
||||
"materialbrowser/bundleimporter.cpp",
|
||||
"materialbrowser/bundleimporter.h",
|
||||
"materialeditor/materialeditorcontextobject.cpp",
|
||||
"materialeditor/materialeditorcontextobject.h",
|
||||
"materialeditor/materialeditordynamicpropertiesproxymodel.cpp",
|
||||
|
Reference in New Issue
Block a user