diff --git a/share/qtcreator/qmldesigner/materialBrowserQmlSource/BundleMaterialItem.qml b/share/qtcreator/qmldesigner/materialBrowserQmlSource/BundleMaterialItem.qml new file mode 100644 index 00000000000..4b8b45be564 --- /dev/null +++ b/share/qtcreator/qmldesigner/materialBrowserQmlSource/BundleMaterialItem.qml @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuickDesignerTheme 1.0 +import HelperWidgets 2.0 +import StudioTheme 1.0 as StudioTheme + +Item { + id: root + + signal showContextMenu() + + visible: modelData.bundleMaterialVisible + + MouseArea { + id: mouseArea + + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPressed: (mouse) => { + if (mouse.button === Qt.LeftButton) + rootView.startDragBundleMaterial(modelData, mapToGlobal(mouse.x, mouse.y)) + else if (mouse.button === Qt.RightButton) + root.showContextMenu() + } + } + + Column { + anchors.fill: parent + spacing: 1 + + Item { width: 1; height: 5 } // spacer + + Image { + id: img + + width: root.width - 10 + height: img.width + anchors.horizontalCenter: parent.horizontalCenter + source: modelData.bundleMaterialIcon + cache: false + } + + TextInput { + id: matName + + text: modelData.bundleMaterialName + + width: img.width + clip: true + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: TextInput.AlignHCenter + + font.pixelSize: StudioTheme.Values.myFontSize + + readOnly: true + selectByMouse: !matName.readOnly + + color: StudioTheme.Values.themeTextColor + selectionColor: StudioTheme.Values.themeTextSelectionColor + selectedTextColor: StudioTheme.Values.themeTextSelectedTextColor + } + } +} diff --git a/share/qtcreator/qmldesigner/materialBrowserQmlSource/MaterialBrowser.qml b/share/qtcreator/qmldesigner/materialBrowserQmlSource/MaterialBrowser.qml index a8495ffe01e..bda29dcb132 100644 --- a/share/qtcreator/qmldesigner/materialBrowserQmlSource/MaterialBrowser.qml +++ b/share/qtcreator/qmldesigner/materialBrowserQmlSource/MaterialBrowser.qml @@ -16,13 +16,15 @@ Item { property var currentMaterial: null property int currentMaterialIdx: 0 + property var currentBundleMaterial: null property var matSectionsModel: [] // Called also from C++ to close context menu on focus out function closeContextMenu() { - contextMenu.close() + cxtMenu.close() + cxtMenuBundle.close() } // Called from C++ to refresh a preview material after it changes @@ -46,10 +48,15 @@ Item { acceptedButtons: Qt.RightButton - onClicked: { - if (!materialBrowserModel.hasMaterialRoot) { + onClicked: (mouse) => { + // root context-menu works only for user materials + var userMatsSecBottom = mapFromItem(userMaterialsSection, 0, userMaterialsSection.y).y + + userMaterialsSection.height; + + if (!materialBrowserModel.hasMaterialRoot && (!materialBrowserBundleModel.matBundleExists + || mouse.y < userMatsSecBottom)) { root.currentMaterial = null - contextMenu.popup() + cxtMenu.popup() } } } @@ -68,7 +75,7 @@ Item { } StudioControls.Menu { - id: contextMenu + id: cxtMenu closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside @@ -166,6 +173,32 @@ Item { } } + StudioControls.Menu { + id: cxtMenuBundle + + closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside + + StudioControls.MenuItem { + text: qsTr("Apply to selected (replace)") + enabled: root.currentBundleMaterial && materialBrowserModel.hasModelSelection + onTriggered: materialBrowserBundleModel.applyToSelected(root.currentBundleMaterial, false) + } + + StudioControls.MenuItem { + text: qsTr("Apply to selected (add)") + enabled: root.currentBundleMaterial && materialBrowserModel.hasModelSelection + onTriggered: materialBrowserBundleModel.applyToSelected(root.currentBundleMaterial, true) + } + + StudioControls.MenuSeparator {} + + StudioControls.MenuItem { + text: qsTr("Add to project") + + onTriggered: materialBrowserBundleModel.addMaterial(root.currentBundleMaterial) + } + } + Column { id: col y: 5 @@ -193,23 +226,12 @@ Item { } } - Text { - text: qsTr("No match found."); - color: StudioTheme.Values.themeTextColor - font.pixelSize: StudioTheme.Values.baseFontSize - leftPadding: 10 - visible: materialBrowserModel.hasQuick3DImport && materialBrowserModel.isEmpty - && !searchBox.isEmpty() && !materialBrowserModel.hasMaterialRoot - } - Text { text: { if (materialBrowserModel.hasMaterialRoot) qsTr("Material Browser is disabled inside a material component.") else if (!materialBrowserModel.hasQuick3DImport) qsTr("To use Material Browser, first add the QtQuick3D module in the Components view.") - else if (materialBrowserModel.isEmpty && searchBox.isEmpty()) - qsTr("There are no materials in this project.
Select '+' to create one.") else "" } @@ -230,31 +252,115 @@ Item { width: root.width height: root.height - searchBox.height clip: true + visible: materialBrowserModel.hasQuick3DImport && !materialBrowserModel.hasMaterialRoot - Grid { - id: grid + Column { + Section { + id: userMaterialsSection - width: scrollView.width - leftPadding: 5 - rightPadding: 5 - bottomPadding: 5 - columns: root.width / root.cellWidth + width: root.width + caption: qsTr("User materials") + hideHeader: !materialBrowserBundleModel.matBundleExists - Repeater { - id: gridRepeater + Grid { + id: grid - model: materialBrowserModel - delegate: MaterialItem { - width: root.cellWidth - height: root.cellHeight + width: scrollView.width + leftPadding: 5 + rightPadding: 5 + bottomPadding: 5 + columns: root.width / root.cellWidth - onShowContextMenu: { - if (searchBox.isEmpty()) { - root.currentMaterial = model - contextMenu.popup() + Repeater { + id: gridRepeater + + model: materialBrowserModel + delegate: MaterialItem { + width: root.cellWidth + height: root.cellHeight + + onShowContextMenu: { + if (searchBox.isEmpty()) { + root.currentMaterial = model + cxtMenu.popup() + } + } } } } + + Text { + text: qsTr("No match found."); + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + leftPadding: 10 + visible: materialBrowserModel.isEmpty && !searchBox.isEmpty() && !materialBrowserModel.hasMaterialRoot + } + + Text { + text:qsTr("There are no materials in this project.
Select '+' to create one.") + visible: materialBrowserModel.isEmpty && searchBox.isEmpty() + textFormat: Text.RichText + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.mediumFontSize + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + width: root.width + } + } + + Section { + width: root.width + caption: qsTr("Material Library") + addTopPadding: noMatchText.visible + visible: materialBrowserBundleModel.matBundleExists + + Column { + Repeater { + model: materialBrowserBundleModel + + delegate: Section { + width: root.width + caption: bundleCategory + addTopPadding: false + sectionBackgroundColor: "transparent" + visible: bundleCategoryVisible + + Grid { + width: scrollView.width + leftPadding: 5 + rightPadding: 5 + bottomPadding: 5 + columns: root.width / root.cellWidth + + Repeater { + model: bundleMaterialsModel + + delegate: BundleMaterialItem { + width: root.cellWidth + height: root.cellHeight + + onShowContextMenu: { + if (searchBox.isEmpty()) { + root.currentBundleMaterial = modelData + cxtMenuBundle.popup() + } + } + } + } + } + } + } + + Text { + id: noMatchText + text: qsTr("No match found."); + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + leftPadding: 10 + visible: materialBrowserBundleModel.isEmpty && !searchBox.isEmpty() && !materialBrowserModel.hasMaterialRoot + } + } } } } diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector2dEditorTemplate.template b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector2dEditorTemplate.template index b339c254bac..ab2eb508d17 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector2dEditorTemplate.template +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector2dEditorTemplate.template @@ -3,40 +3,38 @@ PropertyLabel { tooltip: "%1" } -ColumnLayout { - SecondColumnLayout { - SpinBox { - minimumValue: -9999999 - maximumValue: 9999999 - decimals: 2 - backendValue: backendValues.%2_x - implicitWidth: StudioTheme.Values.twoControlColumnWidth - + StudioTheme.Values.actionIndicatorWidth - } - - Spacer { implicitWidth: StudioTheme.Values.controlLabelGap } - - ControlLabel { - text: "X" - } - - Spacer { implicitWidth: StudioTheme.Values.controlGap } - - SpinBox { - minimumValue: -9999999 - maximumValue: 9999999 - decimals: 2 - backendValue: backendValues.%2_y - implicitWidth: StudioTheme.Values.twoControlColumnWidth - + StudioTheme.Values.actionIndicatorWidth - } - - Spacer { implicitWidth: StudioTheme.Values.controlLabelGap } - - ControlLabel { - text: "Y" - } - - ExpandingSpacer {} +SecondColumnLayout { + SpinBox { + minimumValue: -9999999 + maximumValue: 9999999 + decimals: 2 + backendValue: backendValues.%2_x + implicitWidth: StudioTheme.Values.twoControlColumnWidth + + StudioTheme.Values.actionIndicatorWidth } + + Spacer { implicitWidth: StudioTheme.Values.controlLabelGap } + + ControlLabel { + text: "X" + } + + Spacer { implicitWidth: StudioTheme.Values.controlGap } + + SpinBox { + minimumValue: -9999999 + maximumValue: 9999999 + decimals: 2 + backendValue: backendValues.%2_y + implicitWidth: StudioTheme.Values.twoControlColumnWidth + + StudioTheme.Values.actionIndicatorWidth + } + + Spacer { implicitWidth: StudioTheme.Values.controlLabelGap } + + ControlLabel { + text: "Y" + } + + ExpandingSpacer {} } diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector3dEditorTemplate.template b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector3dEditorTemplate.template index 5caff585d73..c5be9374741 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector3dEditorTemplate.template +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector3dEditorTemplate.template @@ -4,6 +4,7 @@ PropertyLabel { } ColumnLayout { + spacing: StudioTheme.Values.sectionRowSpacing / 2 SecondColumnLayout { SpinBox { minimumValue: -9999999 diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector4dEditorTemplate.template b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector4dEditorTemplate.template index 619f51cebe3..0fd6cb9f7f1 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector4dEditorTemplate.template +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/PropertyTemplates/Vector4dEditorTemplate.template @@ -4,6 +4,7 @@ PropertyLabel { } ColumnLayout { + spacing: StudioTheme.Values.sectionRowSpacing / 2 SecondColumnLayout { SpinBox { minimumValue: -9999999 diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml index 4bd99e3f68e..2a0580edf14 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml @@ -297,7 +297,7 @@ Section { onVecSizeChanged: updateProxyValues() - spacing: StudioTheme.Values.sectionRowSpacing + spacing: StudioTheme.Values.sectionRowSpacing / 2 function isValidValue(v) { return !(v === undefined || isNaN(v)) diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ToolTip.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ToolTip.qml new file mode 100644 index 00000000000..0c99890e6b5 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ToolTip.qml @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Templates as T +import StudioTheme 1.0 as StudioTheme + +T.ToolTip { + id: control + + x: parent ? (parent.width - implicitWidth) / 2 : 0 + y: -implicitHeight - 3 + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding) + + margins: 6 + padding: 4 + delay: 1000 + timeout: 5000 + closePolicy: T.Popup.CloseOnEscape | T.Popup.CloseOnPressOutsideParent | T.Popup.CloseOnReleaseOutsideParent + + contentItem: Text { + text: control.text + wrapMode: Text.Wrap + font.pixelSize: StudioTheme.Values.myFontSize + color: StudioTheme.Values.themeToolTipText + } + + background: Rectangle { + color: StudioTheme.Values.themeToolTipBackground + border.width: StudioTheme.Values.border + border.color: StudioTheme.Values.themeToolTipOutline + } +} + diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir index 61c35a00490..878b2307c38 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir @@ -45,5 +45,6 @@ TabBar 1.0 TabBar.qml TabButton 1.0 TabButton.qml TextArea 1.0 TextArea.qml TextField 1.0 TextField.qml +ToolTip 1.0 ToolTip.qml TranslationIndicator 1.0 TranslationIndicator.qml VerticalScrollBar 1.0 VerticalScrollBar.qml diff --git a/src/libs/utils/deviceshell.cpp b/src/libs/utils/deviceshell.cpp index 9ba61c5896b..bfa33418a3d 100644 --- a/src/libs/utils/deviceshell.cpp +++ b/src/libs/utils/deviceshell.cpp @@ -159,12 +159,12 @@ DeviceShell::DeviceShell() DeviceShell::~DeviceShell() { - m_shellProcess->deleteLater(); - if (m_thread.isRunning()) { m_thread.quit(); m_thread.wait(); } + + QTC_CHECK(!m_shellProcess); } /*! @@ -237,7 +237,7 @@ DeviceShell::RunResult DeviceShell::run(const CommandLine &cmd, const QByteArray const int id = ++m_currentId; const auto it = m_commandOutput.insert(id, CommandRun{{-1, {}, {}}, &waiter}); - QMetaObject::invokeMethod(m_shellProcess, [this, id, cmd, stdInData]() { + QMetaObject::invokeMethod(m_shellProcess.get(), [this, id, cmd, stdInData]() { const QString command = QString("%1 \"%2\" %3\n") .arg(id) .arg(QString::fromLatin1(stdInData.toBase64())) @@ -304,13 +304,12 @@ void DeviceShell::startupFailed(const CommandLine &cmdLine) */ bool DeviceShell::start() { - m_shellProcess = new QtcProcess(); - connect(m_shellProcess, &QtcProcess::done, m_shellProcess, + m_shellProcess = std::make_unique(); + connect(m_shellProcess.get(), &QtcProcess::done, m_shellProcess.get(), [this] { emit done(m_shellProcess->resultData()); }); - connect(m_shellProcess, &QObject::destroyed, this, [this] { m_shellProcess = nullptr; }); - connect(&m_thread, &QThread::finished, m_shellProcess, [this] { closeShellProcess(); }); + connect(&m_thread, &QThread::finished, m_shellProcess.get(), [this] { closeShellProcess(); }, Qt::DirectConnection); - setupShellProcess(m_shellProcess); + setupShellProcess(m_shellProcess.get()); m_shellProcess->setProcessMode(ProcessMode::Writer); @@ -319,7 +318,7 @@ bool DeviceShell::start() bool result = false; QMetaObject::invokeMethod( - m_shellProcess, + m_shellProcess.get(), [this] { qCDebug(deviceShellLog) << "Starting shell process:" << m_shellProcess->commandLine().toUserOutput(); m_shellProcess->start(); @@ -333,16 +332,21 @@ bool DeviceShell::start() if (m_shellScriptState == State::FailedToStart) closeShellProcess(); } else { - connect(m_shellProcess, &QtcProcess::readyReadStandardOutput, m_shellProcess, [this] { - onReadyRead(); - }); - connect(m_shellProcess, &QtcProcess::readyReadStandardError, m_shellProcess, [this] { - const QByteArray stdErr = m_shellProcess->readAllStandardError(); - qCWarning(deviceShellLog) << "Received unexpected output on stderr:" << stdErr; - }); + connect(m_shellProcess.get(), + &QtcProcess::readyReadStandardOutput, + m_shellProcess.get(), + [this] { onReadyRead(); }); + connect(m_shellProcess.get(), + &QtcProcess::readyReadStandardError, + m_shellProcess.get(), + [this] { + const QByteArray stdErr = m_shellProcess->readAllStandardError(); + qCWarning(deviceShellLog) + << "Received unexpected output on stderr:" << stdErr; + }); } - connect(m_shellProcess, &QtcProcess::done, m_shellProcess, [this] { + connect(m_shellProcess.get(), &QtcProcess::done, m_shellProcess.get(), [this] { if (m_shellProcess->resultData().m_exitCode != EXIT_SUCCESS || m_shellProcess->resultData().m_exitStatus != QProcess::NormalExit) { qCWarning(deviceShellLog) << "Shell exited with error code:" @@ -428,6 +432,7 @@ void DeviceShell::closeShellProcess() if (!m_shellProcess->waitForFinished(2000)) m_shellProcess->terminate(); } + m_shellProcess.reset(); } } diff --git a/src/libs/utils/deviceshell.h b/src/libs/utils/deviceshell.h index 4b376268387..1c780997706 100644 --- a/src/libs/utils/deviceshell.h +++ b/src/libs/utils/deviceshell.h @@ -79,7 +79,7 @@ private: QWaitCondition *waiter; }; - QtcProcess *m_shellProcess = nullptr; + std::unique_ptr m_shellProcess; QThread m_thread; int m_currentId{0}; diff --git a/src/plugins/docker/dockerdevice.cpp b/src/plugins/docker/dockerdevice.cpp index 0301e6f2315..71b406968f2 100644 --- a/src/plugins/docker/dockerdevice.cpp +++ b/src/plugins/docker/dockerdevice.cpp @@ -479,7 +479,7 @@ void DockerDevicePrivate::startContainer() return; qCWarning(dockerDeviceLog) << "Container shell encountered error:" << resultData.m_error; - m_shell.reset(); + m_shell.release()->deleteLater(); DockerApi::recheckDockerDaemon(); MessageManager::writeFlashing(Tr::tr("Docker daemon appears to be not running. " @@ -501,7 +501,7 @@ void DockerDevicePrivate::updateContainerAccess() if (m_shell) return; - startContainer(); + startContainer(); } void DockerDevice::setMounts(const QStringList &mounts) const diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 6909ef7a405..cbffad21b31 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -408,6 +408,9 @@ extend_qtc_plugin(QmlDesigner materialbrowserwidget.cpp materialbrowserwidget.h materialbrowsermodel.cpp materialbrowsermodel.h bundleimporter.cpp bundleimporter.h + materialbrowserbundlemodel.cpp materialbrowserbundlemodel.h + bundlematerial.cpp bundlematerial.h + bundlematerialcategory.cpp bundlematerialcategory.h ) extend_qtc_plugin(QmlDesigner diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dview.cpp b/src/plugins/qmldesigner/components/edit3d/edit3dview.cpp index f0789bd6d9d..d9a0ad2824f 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dview.cpp +++ b/src/plugins/qmldesigner/components/edit3d/edit3dview.cpp @@ -1,15 +1,16 @@ // Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 +#include "edit3dview.h" +#include "backgroundcolorselection.h" #include "edit3dactions.h" #include "edit3dcanvas.h" -#include "edit3dview.h" -#include "edit3dwidget.h" #include "edit3dviewconfig.h" -#include "backgroundcolorselection.h" +#include "edit3dwidget.h" #include "metainfo.h" -#include "seekerslider.h" #include "nodehints.h" +#include "seekerslider.h" +#include "view3dactioncommand.h" #include #include @@ -287,6 +288,8 @@ void Edit3DView::nodeAtPosReady(const ModelNode &modelNode, const QVector3D &pos assignMaterialTo3dModel(modelNode, m_droppedMaterial); }); } + } else if (m_nodeAtPosReqType == NodeAtPosReqType::BundleMaterialDrop) { + emitCustomNotification("drop_bundle_material", {modelNode}); // To MaterialBrowserView } m_nodeAtPosReqType = NodeAtPosReqType::None; } @@ -837,4 +840,11 @@ void Edit3DView::dropMaterial(const ModelNode &matNode, const QPointF &pos) emitView3DAction(View3DActionType::GetNodeAtPos, pos); } +void Edit3DView::dropBundleMaterial(const QPointF &pos) +{ + m_nodeAtPosReqType = NodeAtPosReqType::BundleMaterialDrop; + QmlDesignerPlugin::instance()->viewManager().nodeInstanceView()->view3DAction( + View3DActionType::GetNodeAtPos, pos); +} + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dview.h b/src/plugins/qmldesigner/components/edit3d/edit3dview.h index bd866098e5d..94c88ce5a20 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dview.h +++ b/src/plugins/qmldesigner/components/edit3d/edit3dview.h @@ -58,12 +58,14 @@ public: void addQuick3DImport(); void startContextMenu(const QPoint &pos); void dropMaterial(const ModelNode &matNode, const QPointF &pos); + void dropBundleMaterial(const QPointF &pos); private slots: void onEntriesChanged(); private: enum class NodeAtPosReqType { + BundleMaterialDrop, MaterialDrop, ContextMenu, None diff --git a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp index 2c497a425f3..2f02c1c620f 100644 --- a/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp +++ b/src/plugins/qmldesigner/components/edit3d/edit3dwidget.cpp @@ -309,7 +309,8 @@ void Edit3DWidget::dragEnterEvent(QDragEnterEvent *dragEnterEvent) const DesignerActionManager &actionManager = QmlDesignerPlugin::instance() ->viewManager().designerActionManager(); if (actionManager.externalDragHasSupportedAssets(dragEnterEvent->mimeData()) - || dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_MATERIAL)) { + || dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_MATERIAL) + || dragEnterEvent->mimeData()->hasFormat(Constants::MIME_TYPE_BUNDLE_MATERIAL)) { dragEnterEvent->acceptProposedAction(); } } @@ -329,6 +330,12 @@ void Edit3DWidget::dropEvent(QDropEvent *dropEvent) return; } + // handle dropping bundle materials + if (dropEvent->mimeData()->hasFormat(Constants::MIME_TYPE_BUNDLE_MATERIAL)) { + m_view->dropBundleMaterial(dropEvent->position()); + return; + } + // handle dropping external assets const DesignerActionManager &actionManager = QmlDesignerPlugin::instance() ->viewManager().designerActionManager(); diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.cpp b/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.cpp index 357be8db45a..422288c8b20 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.cpp +++ b/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.cpp @@ -33,9 +33,9 @@ #include -#include #include -#include +#include +#include #include using namespace Utils; @@ -65,20 +65,14 @@ BundleImporter::BundleImporter(const QString &bundleDir, QString BundleImporter::importComponent(const QString &qmlFile, const QStringList &files) { - FilePath bundleImportPath = QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath(); + FilePath bundleImportPath = resolveBundleImportPath(); if (bundleImportPath.isEmpty()) - return "Failed to resolve current project path"; + return "Failed to resolve bundle import folder"; - 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); + bool bundleImportPathExists = bundleImportPath.exists(); - if (!bundleImportPath.exists()) { - if (!bundleImportPath.createDir()) - return QStringLiteral("Failed to create bundle import folder: '%1'").arg(bundleImportPath.toString()); - } + if (!bundleImportPathExists && !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); @@ -93,37 +87,24 @@ QString BundleImporter::importComponent(const QString &qmlFile, } 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 { + QString qmldirContent = QString::fromUtf8(qmldirPath.fileContents().value_or(QByteArray())); + if (qmldirContent.isEmpty()) { qmldirContent.append("module "); qmldirContent.append(m_moduleName); qmldirContent.append('\n'); } - FilePath qmlSourceFile = FilePath::fromString(qmlFile); + FilePath qmlSourceFile = bundleImportPath.resolvePath(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(); + qmldirPath.writeFileContents(qmldirContent.toUtf8()); } QStringList allFiles; @@ -145,6 +126,19 @@ QString BundleImporter::importComponent(const QString &qmlFile, return QStringLiteral("Failed to copy file: '%1'").arg(source.toString()); } + QVariantHash assetRefMap = loadAssetRefMap(bundleImportPath); + bool writeAssetRefs = false; + for (const QString &assetFile : files) { + QStringList assets = assetRefMap[assetFile].toStringList(); + if (!assets.contains(qmlFile)) { + assets.append(qmlFile); + writeAssetRefs = true; + } + assetRefMap[assetFile] = assets; + } + if (writeAssetRefs) + writeAssetRefMap(bundleImportPath, assetRefMap); + m_fullReset = !qmlFileExists; auto doc = QmlDesignerPlugin::instance()->currentDesignDocument(); Model *model = doc ? doc->currentModel() : nullptr; @@ -163,10 +157,12 @@ QString BundleImporter::importComponent(const QString &qmlFile, } } 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. + // avoid errors, as code model update takes a while. m_importAddPending = true; - m_fullReset = false; + + // Full reset is not necessary if new import directory appearing will trigger scanning, + // but if directory existed but was not valid possible import, we need to do a reset. + m_fullReset = bundleImportPathExists; } } m_importTimerCount = 0; @@ -232,4 +228,114 @@ void BundleImporter::handleImportTimer() } } +QVariantHash BundleImporter::loadAssetRefMap(const Utils::FilePath &bundlePath) +{ + FilePath assetRefPath = bundlePath.resolvePath(QLatin1String(Constants::COMPONENT_BUNDLES_ASSET_REF_FILE)); + const std::optional content = assetRefPath.fileContents(); + if (content) { + QJsonParseError error; + QJsonDocument bundleDataJsonDoc = QJsonDocument::fromJson(*content, &error); + if (bundleDataJsonDoc.isNull()) { + // Failure to read asset refs is not considred fatal, so just print error + qWarning() << "Failed to parse bundle asset ref file:" << error.errorString(); + } else { + return bundleDataJsonDoc.object().toVariantHash(); + } + } + return {}; +} + +void BundleImporter::writeAssetRefMap(const Utils::FilePath &bundlePath, + const QVariantHash &assetRefMap) +{ + FilePath assetRefPath = bundlePath.resolvePath(QLatin1String(Constants::COMPONENT_BUNDLES_ASSET_REF_FILE)); + QJsonObject jsonObj = QJsonObject::fromVariantHash(assetRefMap); + if (!assetRefPath.writeFileContents(QJsonDocument{jsonObj}.toJson())) { + // Failure to write asset refs is not considred fatal, so just print error + qWarning() << QStringLiteral("Failed to save bundle asset ref file: '%1'").arg(assetRefPath.toString()) ; + } +} + +QString BundleImporter::unimportComponent(const QString &qmlFile) +{ + FilePath bundleImportPath = resolveBundleImportPath(); + if (bundleImportPath.isEmpty()) + return "Failed to resolve bundle import folder"; + + if (!bundleImportPath.exists()) + return {}; + + FilePath qmlFilePath = bundleImportPath.resolvePath(qmlFile); + if (!qmlFilePath.exists()) + return {}; + + QStringList removedFiles; + removedFiles.append(qmlFile); + + FilePath qmldirPath = bundleImportPath.resolvePath(QStringLiteral("qmldir")); + const std::optional qmldirContent = qmldirPath.fileContents(); + QByteArray newContent; + if (qmldirContent) { + QByteArray qmlType = qmlFilePath.baseName().toUtf8(); + int typeIndex = qmldirContent->indexOf(qmlType); + if (typeIndex != -1) { + int newLineIndex = qmldirContent->indexOf('\n', typeIndex); + newContent = qmldirContent->left(typeIndex); + if (newLineIndex != -1) + newContent.append(qmldirContent->mid(newLineIndex + 1)); + } + if (newContent != qmldirContent) { + if (!qmldirPath.writeFileContents(newContent)) + return QStringLiteral("Failed to write qmldir file: '%1'").arg(qmldirPath.toString()); + } + } + + QVariantHash assetRefMap = loadAssetRefMap(bundleImportPath); + bool writeAssetRefs = false; + const auto keys = assetRefMap.keys(); + for (const QString &assetFile : keys) { + QStringList assets = assetRefMap[assetFile].toStringList(); + if (assets.contains(qmlFile)) { + assets.removeAll(qmlFile); + writeAssetRefs = true; + } + if (!assets.isEmpty()) { + assetRefMap[assetFile] = assets; + } else { + removedFiles.append(assetFile); + assetRefMap.remove(assetFile); + writeAssetRefs = true; + } + } + + for (const QString &removedFile : removedFiles) { + FilePath removedFilePath = bundleImportPath.resolvePath(removedFile); + if (removedFilePath.exists()) + removedFilePath.removeFile(); + } + + if (writeAssetRefs) + writeAssetRefMap(bundleImportPath, assetRefMap); + + m_fullReset = true; + m_importTimerCount = 0; + m_importTimer.start(); + + return {}; +} + +FilePath BundleImporter::resolveBundleImportPath() +{ + FilePath bundleImportPath = QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath(); + if (bundleImportPath.isEmpty()) + return bundleImportPath; + + 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 + + return bundleImportPath.resolvePath(projectBundlePath); +} + } // namespace QmlDesigner::Internal diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.h b/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.h index 840c4c672f5..b22d0edd591 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.h +++ b/src/plugins/qmldesigner/components/materialbrowser/bundleimporter.h @@ -30,6 +30,7 @@ #include "nodemetainfo.h" #include +#include QT_BEGIN_NAMESPACE QT_END_NAMESPACE @@ -49,6 +50,8 @@ public: QString importComponent(const QString &qmlFile, const QStringList &files); + QString unimportComponent(const QString &qmlFile); + 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 @@ -57,6 +60,9 @@ signals: private: void handleImportTimer(); + QVariantHash loadAssetRefMap(const Utils::FilePath &bundlePath); + void writeAssetRefMap(const Utils::FilePath &bundlePath, const QVariantHash &assetRefMap); + Utils::FilePath resolveBundleImportPath(); Utils::FilePath m_bundleDir; QString m_bundleId; diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.cpp b/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.cpp new file mode 100644 index 00000000000..75549945f41 --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** 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 "bundlematerial.h" + +namespace QmlDesigner { + +BundleMaterial::BundleMaterial(QObject *parent, + const QString &name, + const QString &qml, + const QUrl &icon, + const QStringList &files) + : QObject(parent), m_name(name), m_qml(qml), m_icon(icon), m_files(files) {} + +bool BundleMaterial::filter(const QString &searchText) +{ + if (m_visible != m_name.contains(searchText, Qt::CaseInsensitive)) { + m_visible = !m_visible; + emit materialVisibleChanged(); + } + + return m_visible; +} + +QUrl BundleMaterial::icon() const +{ + return m_icon; +} + +QString BundleMaterial::qml() const +{ + return m_qml; +} + +QStringList BundleMaterial::files() const +{ + return m_files; +} + +bool BundleMaterial::visible() const +{ + return m_visible; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.h b/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.h new file mode 100644 index 00000000000..04f4ae26568 --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/bundlematerial.h @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** 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 +#include +#include + +namespace QmlDesigner { + +class BundleMaterial : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString bundleMaterialName MEMBER m_name CONSTANT) + Q_PROPERTY(QUrl bundleMaterialIcon MEMBER m_icon CONSTANT) + Q_PROPERTY(bool bundleMaterialVisible MEMBER m_visible NOTIFY materialVisibleChanged) + +public: + BundleMaterial(QObject *parent, + const QString &name, + const QString &qml, + const QUrl &icon, + const QStringList &files); + + bool filter(const QString &searchText); + + QUrl icon() const; + QString qml() const; + QStringList files() const; + bool visible() const; + +signals: + void materialVisibleChanged(); + +private: + QString m_name; + QString m_qml; + QUrl m_icon; + QStringList m_files; + + bool m_visible = true; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.cpp b/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.cpp new file mode 100644 index 00000000000..9a18ff75014 --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** 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 "bundlematerialcategory.h" + +#include "bundlematerial.h" + +namespace QmlDesigner { + +BundleMaterialCategory::BundleMaterialCategory(QObject *parent, const QString &name) + : QObject(parent), m_name(name) {} + +void BundleMaterialCategory::addBundleMaterial(BundleMaterial *bundleMat) +{ + m_categoryMaterials.append(bundleMat); +} + +bool BundleMaterialCategory::filter(const QString &searchText) +{ + bool visible = false; + for (BundleMaterial *mat : std::as_const(m_categoryMaterials)) + visible |= mat->filter(searchText); + + if (visible != m_visible) { + m_visible = visible; + emit categoryVisibleChanged(); + return true; + } + + return false; +} + +QString BundleMaterialCategory::name() const +{ + return m_name; +} + +bool BundleMaterialCategory::visible() const +{ + return m_visible; +} + +QList BundleMaterialCategory::categoryMaterials() const +{ + return m_categoryMaterials; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.h b/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.h new file mode 100644 index 00000000000..0a3a0f7c028 --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/bundlematerialcategory.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** 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 + +namespace QmlDesigner { + +class BundleMaterial; + +class BundleMaterialCategory : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT) + Q_PROPERTY(bool categoryVisible MEMBER m_visible NOTIFY categoryVisibleChanged) + +public: + BundleMaterialCategory(QObject *parent, const QString &name); + + void addBundleMaterial(BundleMaterial *bundleMat); + bool filter(const QString &searchText); + + QString name() const; + bool visible() const; + QList categoryMaterials() const; + +signals: + void categoryVisibleChanged(); + +private: + QString m_name; + bool m_visible = true; + + QList m_categoryMaterials; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.cpp b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.cpp new file mode 100644 index 00000000000..1632fc14fab --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.cpp @@ -0,0 +1,229 @@ +/**************************************************************************** +** +** 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 "materialbrowserbundlemodel.h" + +#include "bundleimporter.h" +#include "bundlematerial.h" +#include "bundlematerialcategory.h" +#include "utils/qtcassert.h" + +#include +#include +#include +#include + +namespace QmlDesigner { + +MaterialBrowserBundleModel::MaterialBrowserBundleModel(QObject *parent) + : QAbstractListModel(parent) +{ + loadMaterialBundle(); +} + +int MaterialBrowserBundleModel::rowCount(const QModelIndex &) const +{ + return m_bundleCategories.size(); +} + +QVariant MaterialBrowserBundleModel::data(const QModelIndex &index, int role) const +{ + QTC_ASSERT(index.isValid() && index.row() < m_bundleCategories.count(), return {}); + QTC_ASSERT(roleNames().contains(role), return {}); + + QByteArray roleName = roleNames().value(role); + if (roleName == "bundleCategory") + return m_bundleCategories.at(index.row())->name(); + + if (roleName == "bundleCategoryVisible") + return m_bundleCategories.at(index.row())->visible(); + + if (roleName == "bundleMaterialsModel") + return QVariant::fromValue(m_bundleCategories.at(index.row())->categoryMaterials()); + + return {}; +} + +bool MaterialBrowserBundleModel::isValidIndex(int idx) const +{ + return idx > -1 && idx < rowCount(); +} + +QHash MaterialBrowserBundleModel::roleNames() const +{ + static const QHash roles { + {Qt::UserRole + 1, "bundleCategory"}, + {Qt::UserRole + 2, "bundleCategoryVisible"}, + {Qt::UserRole + 3, "bundleMaterialsModel"} + }; + return roles; +} + +void MaterialBrowserBundleModel::loadMaterialBundle() +{ + if (m_matBundleExists || m_probeMatBundleDir) + return; + + QDir matBundleDir(qEnvironmentVariable("MATERIAL_BUNDLE_PATH")); + + // search for matBundleDir from exec dir and up + if (matBundleDir.dirName() == ".") { + m_probeMatBundleDir = true; // probe only once + + matBundleDir.setPath(QCoreApplication::applicationDirPath()); + while (!matBundleDir.cd("material_bundle") && matBundleDir.cdUp()) + ; // do nothing + + if (matBundleDir.dirName() != "material_bundle") // bundlePathDir not found + return; + } + + QString matBundlePath = matBundleDir.filePath("material_bundle.json"); + + if (m_matBundleObj.isEmpty()) { + QFile matPropsFile(matBundlePath); + + if (!matPropsFile.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open material_bundle.json"); + return; + } + + QJsonDocument matBundleJsonDoc = QJsonDocument::fromJson(matPropsFile.readAll()); + if (matBundleJsonDoc.isNull()) { + qWarning("Invalid material_bundle.json file"); + return; + } else { + m_matBundleObj = matBundleJsonDoc.object(); + } + } + + m_matBundleExists = true; + + const QJsonObject catsObj = m_matBundleObj.value("categories").toObject(); + const QStringList categories = catsObj.keys(); + for (const QString &cat : categories) { + auto category = new BundleMaterialCategory(this, cat); + + const QJsonObject matsObj = catsObj.value(cat).toObject(); + const QStringList mats = matsObj.keys(); + for (const QString &mat : mats) { + const QJsonObject matObj = matsObj.value(mat).toObject(); + + QStringList files; + const QJsonArray assetsArr = matObj.value("files").toArray(); + for (const QJsonValueRef &asset : assetsArr) + files.append(asset.toString()); + + auto bundleMat = new BundleMaterial(category, mat, matObj.value("qml").toString(), + QUrl::fromLocalFile(matBundleDir.filePath(matObj.value("icon").toString())), files); + + category->addBundleMaterial(bundleMat); + } + m_bundleCategories.append(category); + } + + QStringList sharedFiles; + const QJsonArray sharedFilesArr = m_matBundleObj.value("sharedFiles").toArray(); + for (const QJsonValueRef &file : sharedFilesArr) + sharedFiles.append(file.toString()); + + m_importer = new Internal::BundleImporter(matBundleDir.path(), "MaterialBundle", sharedFiles); + connect(m_importer, &Internal::BundleImporter::importFinished, this, [&](const QmlDesigner::NodeMetaInfo &metaInfo) { + if (metaInfo.isValid()) + emit addBundleMaterialToProjectRequested(metaInfo); + }); +} + +bool MaterialBrowserBundleModel::hasQuick3DImport() const +{ + return m_hasQuick3DImport; +} + +void MaterialBrowserBundleModel::setHasQuick3DImport(bool b) +{ + if (b == m_hasQuick3DImport) + return; + + m_hasQuick3DImport = b; + emit hasQuick3DImportChanged(); +} + +bool MaterialBrowserBundleModel::hasMaterialRoot() const +{ + return m_hasMaterialRoot; +} + +void MaterialBrowserBundleModel::setHasMaterialRoot(bool b) +{ + if (m_hasMaterialRoot == b) + return; + + m_hasMaterialRoot = b; + emit hasMaterialRootChanged(); +} + +void MaterialBrowserBundleModel::setSearchText(const QString &searchText) +{ + QString lowerSearchText = searchText.toLower(); + + if (m_searchText == lowerSearchText) + return; + + m_searchText = lowerSearchText; + + bool anyCatVisible = false; + bool catVisibilityChanged = false; + + for (BundleMaterialCategory *cat : std::as_const(m_bundleCategories)) { + catVisibilityChanged |= cat->filter(m_searchText); + anyCatVisible |= cat->visible(); + } + + if (anyCatVisible == m_isEmpty) { + m_isEmpty = !anyCatVisible; + emit isEmptyChanged(); + } + + if (catVisibilityChanged) + resetModel(); +} + +void MaterialBrowserBundleModel::resetModel() +{ + beginResetModel(); + endResetModel(); +} + +void MaterialBrowserBundleModel::applyToSelected(BundleMaterial *mat, bool add) +{ + emit applyToSelectedTriggered(mat, add); +} + +void MaterialBrowserBundleModel::addMaterial(BundleMaterial *mat) +{ + m_importer->importComponent(mat->qml(), mat->files()); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.h b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.h new file mode 100644 index 00000000000..60b48f1c9b9 --- /dev/null +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserbundlemodel.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** 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 +#include + +#include +#include +#include +#include + +namespace QmlDesigner { + +class BundleMaterial; +class BundleMaterialCategory; + +namespace Internal { +class BundleImporter; +} + +class MaterialBrowserBundleModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(bool matBundleExists MEMBER m_matBundleExists CONSTANT) + Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged) + Q_PROPERTY(bool hasQuick3DImport READ hasQuick3DImport WRITE setHasQuick3DImport NOTIFY hasQuick3DImportChanged) + Q_PROPERTY(bool hasMaterialRoot READ hasMaterialRoot WRITE setHasMaterialRoot NOTIFY hasMaterialRootChanged) + +public: + MaterialBrowserBundleModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setSearchText(const QString &searchText); + + bool hasQuick3DImport() const; + void setHasQuick3DImport(bool b); + + bool hasMaterialRoot() const; + void setHasMaterialRoot(bool b); + + void resetModel(); + + Q_INVOKABLE void applyToSelected(QmlDesigner::BundleMaterial *mat, bool add = false); + Q_INVOKABLE void addMaterial(QmlDesigner::BundleMaterial *mat); + +signals: + void isEmptyChanged(); + void hasQuick3DImportChanged(); + void hasMaterialRootChanged(); + void materialVisibleChanged(); + void applyToSelectedTriggered(QmlDesigner::BundleMaterial *mat, bool add = false); + void addBundleMaterialToProjectRequested(const QmlDesigner::NodeMetaInfo &metaInfo); + +private: + void loadMaterialBundle(); + bool isValidIndex(int idx) const; + + QString m_searchText; + QList m_bundleCategories; + QJsonObject m_matBundleObj; + Internal::BundleImporter *m_importer = nullptr; + + bool m_isEmpty = true; + bool m_hasQuick3DImport = false; + bool m_hasMaterialRoot = false; + bool m_matBundleExists = false; + bool m_probeMatBundleDir = false; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowsermodel.cpp b/src/plugins/qmldesigner/components/materialbrowser/materialbrowsermodel.cpp index 8c0d5b345da..bbe47ec94c7 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowsermodel.cpp +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowsermodel.cpp @@ -213,6 +213,8 @@ void MaterialBrowserModel::setSearchText(const QString &searchText) isEmpty = !isValidIndex(m_selectedIndex + inc) && !isValidIndex(m_selectedIndex - inc); } + if (!isMaterialVisible(m_selectedIndex)) // handles the case of a single material + isEmpty = true; } if (isEmpty != m_isEmpty) { diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.cpp b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.cpp index c511e35488e..aaf9391d382 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.cpp +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.cpp @@ -3,8 +3,10 @@ #include "materialbrowserview.h" #include "bindingproperty.h" +#include "bundlematerial.h" #include "materialbrowsermodel.h" #include "materialbrowserwidget.h" +#include "materialbrowserbundlemodel.h" #include "nodeabstractproperty.h" #include "nodemetainfo.h" #include "qmlobjectnode.h" @@ -14,9 +16,12 @@ #include #include #include +#include #include #include +#include +#include namespace QmlDesigner { @@ -93,6 +98,80 @@ WidgetInfo MaterialBrowserView::widgetInfo() } }); }); + + connect(m_widget, &MaterialBrowserWidget::bundleMaterialDragStarted, this, + [&] (QmlDesigner::BundleMaterial *bundleMat) { + m_draggedBundleMaterial = bundleMat; + }); + + MaterialBrowserBundleModel *matBrowserBundleModel = m_widget->materialBrowserBundleModel().data(); + + connect(matBrowserBundleModel, &MaterialBrowserBundleModel::applyToSelectedTriggered, this, + [&] (BundleMaterial *material, bool add) { + if (!m_selectedModel.isValid()) + return; + + m_bundleMaterialDropTarget = m_selectedModel; + m_bundleMaterialAddToSelected = add; + m_widget->materialBrowserBundleModel()->addMaterial(material); + }); + + connect(matBrowserBundleModel, &MaterialBrowserBundleModel::addBundleMaterialToProjectRequested, this, + [&] (const QmlDesigner::NodeMetaInfo &metaInfo) { + ModelNode matLib = materialLibraryNode(); + if (!matLib.isValid()) + return; + + executeInTransaction("MaterialBrowserView::widgetInfo", [&] { + ModelNode newMatNode = createModelNode(metaInfo.typeName(), metaInfo.majorVersion(), + metaInfo.minorVersion()); + matLib.defaultNodeListProperty().reparentHere(newMatNode); + + static QRegularExpression rgx("([A-Z])([a-z]*)"); + QString newName = QString::fromLatin1(metaInfo.simplifiedTypeName()).replace(rgx, " \\1\\2").trimmed(); + QString newId = model()->generateIdFromName(newName, "material"); + newMatNode.setIdWithRefactoring(newId); + + VariantProperty objNameProp = newMatNode.variantProperty("objectName"); + objNameProp.setValue(newName); + + if (m_bundleMaterialDropTarget.isValid()) { + QmlObjectNode qmlObjNode(m_bundleMaterialDropTarget); + if (m_bundleMaterialAddToSelected) { + // TODO: unify this logic as it exist elsewhere also + auto expToList = [](const QString &exp) { + QString copy = exp; + copy = copy.remove("[").remove("]"); + + QStringList tmp = copy.split(',', Qt::SkipEmptyParts); + for (QString &str : tmp) + str = str.trimmed(); + + return tmp; + }; + + auto listToExp = [](QStringList &stringList) { + if (stringList.size() > 1) + return QString("[" + stringList.join(",") + "]"); + + if (stringList.size() == 1) + return stringList.first(); + + return QString(); + }; + QStringList matList = expToList(qmlObjNode.expression("materials")); + matList.append(newMatNode.id()); + QString updatedExp = listToExp(matList); + qmlObjNode.setBindingProperty("materials", updatedExp); + } else { + qmlObjNode.setBindingProperty("materials", newMatNode.id()); + } + m_bundleMaterialDropTarget = {}; + } + + m_bundleMaterialAddToSelected = false; + }); + }); } return createWidgetInfo(m_widget.data(), @@ -162,24 +241,24 @@ void MaterialBrowserView::modelAboutToBeDetached(Model *model) void MaterialBrowserView::selectedNodesChanged(const QList &selectedNodeList, [[maybe_unused]] const QList &lastSelectedNodeList) { - ModelNode selectedModel; + m_selectedModel = {}; for (const ModelNode &node : selectedNodeList) { if (node.metaInfo().isQtQuick3DModel()) { - selectedModel = node; + m_selectedModel = node; break; } } - m_widget->materialBrowserModel()->setHasModelSelection(selectedModel.isValid()); + m_widget->materialBrowserModel()->setHasModelSelection(m_selectedModel.isValid()); if (!m_autoSelectModelMaterial) return; - if (selectedNodeList.size() > 1 || !selectedModel.isValid()) + if (selectedNodeList.size() > 1 || !m_selectedModel.isValid()) return; - QmlObjectNode qmlObjNode(selectedModel); + QmlObjectNode qmlObjNode(m_selectedModel); QString matExp = qmlObjNode.expression("materials"); if (matExp.isEmpty()) return; @@ -307,6 +386,10 @@ void MaterialBrowserView::customNotification(const AbstractView *view, }); } else if (identifier == "delete_selected_material") { m_widget->materialBrowserModel()->deleteSelectedMaterial(); + } else if (identifier == "drop_bundle_material") { + m_bundleMaterialDropTarget = nodeList.first(); + m_widget->materialBrowserBundleModel()->addMaterial(m_draggedBundleMaterial); + m_draggedBundleMaterial = nullptr; } } diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.h b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.h index 13bd3c36a30..6e5a45e2469 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.h +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserview.h @@ -9,6 +9,7 @@ namespace QmlDesigner { +class BundleMaterial; class MaterialBrowserWidget; class MaterialBrowserView : public AbstractView @@ -43,13 +44,17 @@ public: private: void refreshModel(bool updateImages); bool isMaterial(const ModelNode &node) const; + void loadPropertyGroups(); QPointer m_widget; + ModelNode m_bundleMaterialDropTarget; + ModelNode m_selectedModel; // first selected 3D model node + BundleMaterial *m_draggedBundleMaterial = nullptr; + bool m_bundleMaterialAddToSelected = false; bool m_hasQuick3DImport = false; bool m_autoSelectModelMaterial = false; // TODO: wire this to some action bool m_puppetResetPending = false; bool m_propertyGroupsLoaded = false; - void loadPropertyGroups(); }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp index fe746926765..3f6f2f39f5e 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.cpp @@ -2,25 +2,29 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "materialbrowserwidget.h" + +#include "bundlematerial.h" +#include "materialbrowserbundlemodel.h" #include "materialbrowsermodel.h" #include "materialbrowserview.h" -#include - #include #include #include #include #include -#include -#include -#include +#include + +#include +#include +#include -#include #include #include #include +#include +#include #include #include #include @@ -91,7 +95,7 @@ bool MaterialBrowserWidget::eventFilter(QObject *obj, QEvent *event) if (m_materialToDrag.isValid()) { QMouseEvent *me = static_cast(event); - if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 10) { + if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20) { QByteArray data; QMimeData *mimeData = new QMimeData; QDataStream stream(&data, QIODevice::WriteOnly); @@ -103,6 +107,17 @@ bool MaterialBrowserWidget::eventFilter(QObject *obj, QEvent *event) QString::number(m_materialToDrag.internalId()), nullptr, {128, 128})); m_materialToDrag = {}; } + } else if (m_bundleMaterialToDrag != nullptr) { + QMouseEvent *me = static_cast(event); + if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20) { + QMimeData *mimeData = new QMimeData; + mimeData->setData(Constants::MIME_TYPE_BUNDLE_MATERIAL, {}); + mimeData->removeFormat("text/plain"); + + model->startDrag(mimeData, m_bundleMaterialToDrag->icon().toLocalFile()); + emit bundleMaterialDragStarted(m_bundleMaterialToDrag); + m_bundleMaterialToDrag = {}; + } } } @@ -112,6 +127,7 @@ bool MaterialBrowserWidget::eventFilter(QObject *obj, QEvent *event) MaterialBrowserWidget::MaterialBrowserWidget(MaterialBrowserView *view) : m_materialBrowserView(view) , m_materialBrowserModel(new MaterialBrowserModel(this)) + , m_materialBrowserBundleModel(new MaterialBrowserBundleModel(this)) , m_quickWidget(new QQuickWidget(this)) , m_previewImageProvider(new PreviewImageProvider()) { @@ -130,6 +146,7 @@ MaterialBrowserWidget::MaterialBrowserWidget(MaterialBrowserView *view) m_quickWidget->rootContext()->setContextProperties({ {"rootView", QVariant::fromValue(this)}, {"materialBrowserModel", QVariant::fromValue(m_materialBrowserModel.data())}, + {"materialBrowserBundleModel", QVariant::fromValue(m_materialBrowserBundleModel.data())}, }); m_quickWidget->engine()->addImageProvider("materialBrowser", m_previewImageProvider); @@ -189,6 +206,12 @@ void MaterialBrowserWidget::startDragMaterial(int index, const QPointF &mousePos m_dragStartPoint = mousePos.toPoint(); } +void MaterialBrowserWidget::startDragBundleMaterial(QmlDesigner::BundleMaterial *bundleMat, const QPointF &mousePos) +{ + m_bundleMaterialToDrag = bundleMat; + m_dragStartPoint = mousePos.toPoint(); +} + QString MaterialBrowserWidget::qmlSourcesPath() { #ifdef SHARE_QML_PATH @@ -216,6 +239,7 @@ void MaterialBrowserWidget::reloadQmlSource() void MaterialBrowserWidget::updateSearch() { m_materialBrowserModel->setSearchText(m_filterText); + m_materialBrowserBundleModel->setSearchText(m_filterText); m_quickWidget->update(); } @@ -229,4 +253,10 @@ QPointer MaterialBrowserWidget::materialBrowserModel() con return m_materialBrowserModel; } +QPointer MaterialBrowserWidget::materialBrowserBundleModel() const +{ + return m_materialBrowserBundleModel; +} + + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.h b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.h index 212265f002b..1767286b49e 100644 --- a/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.h +++ b/src/plugins/qmldesigner/components/materialbrowser/materialbrowserwidget.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -26,8 +27,10 @@ QT_END_NAMESPACE namespace QmlDesigner { +class BundleMaterial; class MaterialBrowserView; class MaterialBrowserModel; +class MaterialBrowserBundleModel; class PreviewImageProvider; class MaterialBrowserWidget : public QFrame @@ -45,13 +48,18 @@ public: void clearSearchFilter(); QPointer materialBrowserModel() const; + QPointer materialBrowserBundleModel() const; void updateMaterialPreview(const ModelNode &node, const QPixmap &pixmap); Q_INVOKABLE void handleSearchfilterChanged(const QString &filterText); Q_INVOKABLE void startDragMaterial(int index, const QPointF &mousePos); + Q_INVOKABLE void startDragBundleMaterial(QmlDesigner::BundleMaterial *bundleMat, const QPointF &mousePos); QQuickWidget *quickWidget() const; +signals: + void bundleMaterialDragStarted(QmlDesigner::BundleMaterial *bundleMat); + protected: bool eventFilter(QObject *obj, QEvent *event) override; @@ -61,6 +69,7 @@ private: QPointer m_materialBrowserView; QPointer m_materialBrowserModel; + QPointer m_materialBrowserBundleModel; QScopedPointer m_quickWidget; QShortcut *m_qmlSourceUpdateShortcut = nullptr; @@ -70,6 +79,7 @@ private: QString m_filterText; ModelNode m_materialToDrag; + BundleMaterial *m_bundleMaterialToDrag = nullptr; QPoint m_dragStartPoint; }; diff --git a/src/plugins/qmldesigner/components/propertyeditor/dynamicpropertiesproxymodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/dynamicpropertiesproxymodel.cpp index 9dd87e54c99..acc6744e2ce 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/dynamicpropertiesproxymodel.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/dynamicpropertiesproxymodel.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -150,6 +151,12 @@ void DynamicPropertiesProxyModel::createProperty(const QString &name, const QStr if (selectedNodes.count() == 1) { const ModelNode modelNode = selectedNodes.constFirst(); if (modelNode.isValid()) { + if (modelNode.hasProperty(name.toUtf8())) { + Core::AsynchronousMessageBox::warning(tr("Property already exists"), + tr("Property '%1' already exists") + .arg(name)); + return; + } try { if (Internal::DynamicPropertiesModel::isValueType(typeName)) { QVariant value = Internal::DynamicPropertiesModel::defaultValueForType(typeName); diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp index 188e4f7ed7d..f56c3391c38 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp @@ -437,6 +437,9 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q } ); + contextObject()->setHasMultiSelection( + !qmlObjectNode.view()->singleSelectedModelNode().isValid()); + qCInfo(propertyEditorBenchmark) << "anchors:" << time.elapsed(); qCInfo(propertyEditorBenchmark) << "context:" << time.elapsed(); diff --git a/src/plugins/qmldesigner/designercore/model/model.cpp b/src/plugins/qmldesigner/designercore/model/model.cpp index 0f0d391b816..95cab9b15ab 100644 --- a/src/plugins/qmldesigner/designercore/model/model.cpp +++ b/src/plugins/qmldesigner/designercore/model/model.cpp @@ -1557,6 +1557,10 @@ QString Model::generateIdFromName(const QString &name, const QString &fallbackId newId.prepend('_'); } + // If the new id is not valid (e.g. qml keyword match), try fixing it by prepending underscore + if (!ModelNode::isValidId(newId)) + newId.prepend("_"); + QRegularExpression rgx("\\d+$"); // matches a number at the end of a string while (hasId(newId)) { // id exists QRegularExpressionMatch match = rgx.match(newId); diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 0315601379b..ee9870386bf 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -62,6 +62,7 @@ const char EDIT3D_BACKGROUND_COLOR_ACTIONS[] = "QmlDesigner.Editor3D.BackgroundC const char QML_DESIGNER_SUBFOLDER[] = "/designer/"; const char COMPONENT_BUNDLES_FOLDER[] = "/ComponentBundles"; +const char COMPONENT_BUNDLES_ASSET_REF_FILE[] = "_asset_ref.json"; const char QUICK_3D_ASSETS_FOLDER[] = "/Quick3DAssets"; const char QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX[] = "_libicon"; const char QUICK_3D_ASSET_ICON_DIR[] = "_icons"; @@ -74,6 +75,7 @@ const char MATERIAL_LIB_ID[] = "__materialLibrary__"; const char MIME_TYPE_ITEM_LIBRARY_INFO[] = "application/vnd.qtdesignstudio.itemlibraryinfo"; const char MIME_TYPE_ASSETS[] = "application/vnd.qtdesignstudio.assets"; const char MIME_TYPE_MATERIAL[] = "application/vnd.qtdesignstudio.material"; +const char MIME_TYPE_BUNDLE_MATERIAL[] = "application/vnd.qtdesignstudio.bundlematerial"; const char MIME_TYPE_ASSET_IMAGE[] = "application/vnd.qtdesignstudio.asset.image"; const char MIME_TYPE_ASSET_FONT[] = "application/vnd.qtdesignstudio.asset.font"; const char MIME_TYPE_ASSET_SHADER[] = "application/vnd.qtdesignstudio.asset.shader"; diff --git a/src/plugins/qmldesigner/qmldesignerplugin.qbs b/src/plugins/qmldesigner/qmldesignerplugin.qbs index fbb4a05f073..c6989823f6e 100644 --- a/src/plugins/qmldesigner/qmldesignerplugin.qbs +++ b/src/plugins/qmldesigner/qmldesignerplugin.qbs @@ -731,6 +731,12 @@ Project { "itemlibrary/itemlibraryiconimageprovider.h", "materialbrowser/materialbrowsermodel.cpp", "materialbrowser/materialbrowsermodel.h", + "materialbrowser/materialbrowserbundlemodel.cpp", + "materialbrowser/materialbrowserbundlemodel.h", + "materialbrowser/bundlematerial.cpp", + "materialbrowser/bundlematerial.h", + "materialbrowser/bundlematerialcategory.cpp", + "materialbrowser/bundlematerialcategory.h", "materialbrowser/materialbrowserview.cpp", "materialbrowser/materialbrowserview.h", "materialbrowser/materialbrowserwidget.cpp", diff --git a/src/plugins/remotelinux/linuxdevice.cpp b/src/plugins/remotelinux/linuxdevice.cpp index 72efd9cbc61..88be06dd539 100644 --- a/src/plugins/remotelinux/linuxdevice.cpp +++ b/src/plugins/remotelinux/linuxdevice.cpp @@ -802,7 +802,9 @@ public: cmd.addArg("/bin/sh"); m_shell.reset(new LinuxDeviceShell(cmd, FilePath::fromString(QString("ssh://%1/").arg(parameters.userAtHost())))); - connect(m_shell.get(), &DeviceShell::done, this, [this] { m_shell.reset(); }); + connect(m_shell.get(), &DeviceShell::done, this, [this] { + m_shell.release()->deleteLater(); + }); return m_shell->start(); }