/**************************************************************************** ** ** Copyright (C) 2016 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 "itemlibrarywidget.h" #include "customfilesystemmodel.h" #include "itemlibraryiconimageprovider.h" #include "itemlibraryimport.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace QmlDesigner { static QString propertyEditorResourcesPath() { return Core::ICore::resourcePath() + QStringLiteral("/qmldesigner/propertyEditorQmlSources"); } bool ItemLibraryWidget::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::FocusOut) { if (obj == m_itemViewQuickWidget.data()) QMetaObject::invokeMethod(m_itemViewQuickWidget->rootObject(), "closeContextMenu"); } else if (event->type() == QMouseEvent::MouseMove) { if (m_itemToDrag.isValid()) { QMouseEvent *me = static_cast(event); if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 10) { ItemLibraryEntry entry = m_itemToDrag.value(); // For drag to be handled correctly, we must have the component properly imported // beforehand, so we import the module immediately when the drag starts if (!entry.requiredImport().isEmpty()) { Import import = Import::createLibraryImport(entry.requiredImport()); if (!m_model->hasImport(import, true, true)) { const QList possImports = m_model->possibleImports(); for (const auto &possImport : possImports) { if (possImport.url() == import.url()) { m_model->changeImports({possImport}, {}); break; } } } } auto drag = new QDrag(this); drag->setPixmap(Utils::StyleHelper::dpiSpecificImageFile(entry.libraryEntryIconPath())); drag->setMimeData(m_itemLibraryModel->getMimeData(entry)); drag->exec(); drag->deleteLater(); m_itemToDrag = {}; } } } else if (event->type() == QMouseEvent::MouseButtonRelease) { m_itemToDrag = {}; } return QObject::eventFilter(obj, event); } ItemLibraryWidget::ItemLibraryWidget(AsynchronousImageCache &imageCache, AsynchronousImageCache &asynchronousFontImageCache, SynchronousImageCache &synchronousFontImageCache) : m_itemIconSize(24, 24) , m_itemLibraryModel(new ItemLibraryModel(this)) , m_itemLibraryAddImportModel(new ItemLibraryAddImportModel(this)) , m_resourcesFileSystemModel{new CustomFileSystemModel(synchronousFontImageCache, this)} , m_headerWidget(new QQuickWidget(this)) , m_addImportWidget(new QQuickWidget(this)) , m_itemViewQuickWidget(new QQuickWidget(this)) , m_resourcesView(new ItemLibraryResourceView(asynchronousFontImageCache, this)) , m_imageCache{imageCache} { m_compressionTimer.setInterval(200); m_compressionTimer.setSingleShot(true); ItemLibraryModel::registerQmlTypes(); setWindowTitle(tr("Library", "Title of library view")); setMinimumWidth(100); // create header widget m_headerWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); Theme::setupTheme(m_headerWidget->engine()); m_headerWidget->engine()->addImportPath(propertyEditorResourcesPath() + "/imports"); m_headerWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground)); m_headerWidget->rootContext()->setContextProperty("rootView", QVariant::fromValue(this)); // create add imports widget m_addImportWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); Theme::setupTheme(m_addImportWidget->engine()); m_addImportWidget->engine()->addImportPath(propertyEditorResourcesPath() + "/imports"); m_addImportWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground)); m_addImportWidget->rootContext()->setContextProperties({ {"addImportModel", QVariant::fromValue(m_itemLibraryAddImportModel.data())}, {"rootView", QVariant::fromValue(this)}, }); // set up Item Library view and model m_itemViewQuickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); m_itemViewQuickWidget->engine()->addImportPath(propertyEditorResourcesPath() + "/imports"); m_itemViewQuickWidget->rootContext()->setContextProperties(QVector{ {{"itemLibraryModel"}, QVariant::fromValue(m_itemLibraryModel.data())}, {{"itemLibraryIconWidth"}, m_itemIconSize.width()}, {{"itemLibraryIconHeight"}, m_itemIconSize.height()}, {{"rootView"}, QVariant::fromValue(this)}, {{"highlightColor"}, Utils::StyleHelper::notTooBrightHighlightColor()}, }); m_previewTooltipBackend = std::make_unique(m_imageCache); m_itemViewQuickWidget->rootContext()->setContextProperty("tooltipBackend", m_previewTooltipBackend.get()); m_itemViewQuickWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground)); m_itemViewQuickWidget->engine()->addImageProvider(QStringLiteral("qmldesigner_itemlibrary"), new Internal::ItemLibraryImageProvider); Theme::setupTheme(m_itemViewQuickWidget->engine()); m_itemViewQuickWidget->installEventFilter(this); // connect Resources view and its model m_resourcesView->setModel(m_resourcesFileSystemModel.data()); m_stackedWidget = new QStackedWidget(this); m_stackedWidget->addWidget(m_itemViewQuickWidget.data()); m_stackedWidget->addWidget(m_resourcesView.data()); m_stackedWidget->addWidget(m_addImportWidget.data()); m_stackedWidget->setMinimumHeight(30); m_stackedWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); auto layout = new QVBoxLayout(this); layout->setContentsMargins({}); layout->setSpacing(0); layout->addWidget(m_headerWidget.data()); layout->addWidget(m_stackedWidget.data()); updateSearch(); /* style sheets */ setStyleSheet(Theme::replaceCssColors(QString::fromUtf8(Utils::FileReader::fetchQrc(QLatin1String(":/qmldesigner/stylesheet.css"))))); m_resourcesView->setStyleSheet(Theme::replaceCssColors(QString::fromUtf8(Utils::FileReader::fetchQrc(QLatin1String(":/qmldesigner/scrollbar.css"))))); m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F5), this); connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &ItemLibraryWidget::reloadQmlSource); connect(&m_compressionTimer, &QTimer::timeout, this, &ItemLibraryWidget::updateModel); const auto dropSupport = new Utils::DropSupport( m_resourcesView.data(), [this](QDropEvent *event, Utils::DropSupport *) { // Accept supported file types if (event->type() == QDropEvent::DragEnter && !Utils::DropSupport::isFileDrop(event)) return false; // do not accept drops without files bool accept = false; const QSet &suffixes = m_resourcesFileSystemModel->supportedSuffixes(); const QList urls = event->mimeData()->urls(); for (const QUrl &url : urls) { QFileInfo fi(url.toLocalFile()); if (suffixes.contains(fi.suffix().toLower())) { accept = true; break; } } return accept; }); connect(dropSupport, &Utils::DropSupport::filesDropped, this, &ItemLibraryWidget::importDroppedFiles); m_itemViewQuickWidget->engine()->addImageProvider("itemlibrary_preview", new ItemLibraryIconImageProvider{m_imageCache}); // init the first load of the QML UI elements reloadQmlSource(); } ItemLibraryWidget::~ItemLibraryWidget() = default; void ItemLibraryWidget::setItemLibraryInfo(ItemLibraryInfo *itemLibraryInfo) { if (m_itemLibraryInfo.data() == itemLibraryInfo) return; if (m_itemLibraryInfo) { disconnect(m_itemLibraryInfo.data(), &ItemLibraryInfo::entriesChanged, this, &ItemLibraryWidget::delayedUpdateModel); disconnect(m_itemLibraryInfo.data(), &ItemLibraryInfo::priorityImportsChanged, this, &ItemLibraryWidget::handlePriorityImportsChanged); } m_itemLibraryInfo = itemLibraryInfo; if (itemLibraryInfo) { connect(m_itemLibraryInfo.data(), &ItemLibraryInfo::entriesChanged, this, &ItemLibraryWidget::delayedUpdateModel); connect(m_itemLibraryInfo.data(), &ItemLibraryInfo::priorityImportsChanged, this, &ItemLibraryWidget::handlePriorityImportsChanged); m_itemLibraryAddImportModel->setPriorityImports(m_itemLibraryInfo->priorityImports()); } delayedUpdateModel(); } QList ItemLibraryWidget::createToolBarWidgets() { // TODO: implement QList buttons; return buttons; } void ItemLibraryWidget::handleSearchfilterChanged(const QString &filterText) { m_filterText = filterText; updateSearch(); } void ItemLibraryWidget::handleAddModule() { QMetaObject::invokeMethod(m_headerWidget->rootObject(), "setTab", Q_ARG(QVariant, 0)); handleTabChanged(2); } void ItemLibraryWidget::handleAddAsset() { addResources({}); } void ItemLibraryWidget::handleAddImport(int index) { Import import = m_itemLibraryAddImportModel->getImportAt(index); if (import.isLibraryImport() && (import.url().startsWith("QtQuick") || import.url().startsWith("SimulinkConnector"))) { QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_IMPORT_ADDED + import.toImportString()); } m_model->changeImports({import}, {}); m_stackedWidget->setCurrentIndex(0); // switch to the Components view after import is added updateSearch(); } void ItemLibraryWidget::delayedUpdateModel() { static bool disableTimer = DesignerSettings::getValue(DesignerSettingsKey::DISABLE_ITEM_LIBRARY_UPDATE_TIMER).toBool(); if (disableTimer) updateModel(); else m_compressionTimer.start(); } void ItemLibraryWidget::setModel(Model *model) { m_model = model; if (!model) return; setItemLibraryInfo(model->metaInfo().itemLibraryInfo()); } void ItemLibraryWidget::handleTabChanged(int index) { m_stackedWidget->setCurrentIndex(index); updateSearch(); } QString ItemLibraryWidget::qmlSourcesPath() { return Core::ICore::resourcePath() + QStringLiteral("/qmldesigner/itemLibraryQmlSources"); } void ItemLibraryWidget::clearSearchFilter() { QMetaObject::invokeMethod(m_headerWidget->rootObject(), "clearSearchFilter"); } void ItemLibraryWidget::reloadQmlSource() { const QString libraryHeaderQmlPath = qmlSourcesPath() + "/LibraryHeader.qml"; QTC_ASSERT(QFileInfo::exists(libraryHeaderQmlPath), return); m_headerWidget->engine()->clearComponentCache(); m_headerWidget->setSource(QUrl::fromLocalFile(libraryHeaderQmlPath)); const QString addImportQmlPath = qmlSourcesPath() + "/AddImport.qml"; QTC_ASSERT(QFileInfo::exists(addImportQmlPath), return); m_addImportWidget->engine()->clearComponentCache(); m_addImportWidget->setSource(QUrl::fromLocalFile(addImportQmlPath)); const QString itemLibraryQmlPath = qmlSourcesPath() + "/ItemsView.qml"; QTC_ASSERT(QFileInfo::exists(itemLibraryQmlPath), return); m_itemViewQuickWidget->engine()->clearComponentCache(); m_itemViewQuickWidget->setSource(QUrl::fromLocalFile(itemLibraryQmlPath)); } void ItemLibraryWidget::updateModel() { QTC_ASSERT(m_itemLibraryModel, return); if (m_compressionTimer.isActive()) { m_updateRetry = false; m_compressionTimer.stop(); } m_itemLibraryModel->update(m_itemLibraryInfo.data(), m_model.data()); if (m_itemLibraryModel->rowCount() == 0 && !m_updateRetry) { m_updateRetry = true; // Only retry once to avoid endless loops m_compressionTimer.start(); } else { m_updateRetry = false; } updateSearch(); } void ItemLibraryWidget::updatePossibleImports(const QList &possibleImports) { m_itemLibraryAddImportModel->update(possibleImports); delayedUpdateModel(); } void ItemLibraryWidget::updateUsedImports(const QList &usedImports) { m_itemLibraryModel->updateUsedImports(usedImports); } void ItemLibraryWidget::updateSearch() { if (m_stackedWidget->currentIndex() == 0) { // Item Library tab selected m_itemLibraryModel->setSearchText(m_filterText); m_itemViewQuickWidget->update(); } else if (m_stackedWidget->currentIndex() == 1) { // Assets tab selected m_resourcesFileSystemModel->setSearchFilter(m_filterText); m_resourcesFileSystemModel->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); m_resourcesView->scrollToTop(); } else if (m_stackedWidget->currentIndex() == 2) { // QML imports tab selected m_itemLibraryAddImportModel->setSearchText(m_filterText); } } void ItemLibraryWidget::handlePriorityImportsChanged() { if (!m_itemLibraryInfo.isNull()) { m_itemLibraryAddImportModel->setPriorityImports(m_itemLibraryInfo->priorityImports()); m_itemLibraryAddImportModel->update(m_model->possibleImports()); } } void ItemLibraryWidget::setResourcePath(const QString &resourcePath) { if (m_resourcesView->model() == m_resourcesFileSystemModel.data()) { m_resourcesFileSystemModel->setRootPath(resourcePath); m_resourcesView->setRootIndex(m_resourcesFileSystemModel->indexForPath(resourcePath)); } updateSearch(); } void ItemLibraryWidget::startDragAndDrop(const QVariant &itemLibEntry, const QPointF &mousePos) { // Actual drag is created after mouse has moved to avoid a QDrag bug that causes drag to stay // active (and blocks mouse release) if mouse is released at the same spot of the drag start. // This doesn't completely eliminate the bug but makes it significantly harder to produce. m_itemToDrag = itemLibEntry; m_dragStartPoint = mousePos.toPoint(); } void ItemLibraryWidget::setFlowMode(bool b) { m_itemLibraryModel->setFlowMode(b); } void ItemLibraryWidget::removeImport(const QString &importUrl) { QTC_ASSERT(m_model, return); ItemLibraryImport *importSection = m_itemLibraryModel->importByUrl(importUrl); if (importSection) m_model->changeImports({}, {importSection->importEntry()}); } void ItemLibraryWidget::addImportForItem(const QString &importUrl) { QTC_ASSERT(m_itemLibraryModel, return); QTC_ASSERT(m_model, return); Import import = m_itemLibraryAddImportModel->getImport(importUrl); m_model->changeImports({import}, {}); } void ItemLibraryWidget::addResources(const QStringList &files) { auto document = QmlDesignerPlugin::instance()->currentDesignDocument(); QTC_ASSERT(document, return); QList handlers = QmlDesignerPlugin::instance()->viewManager().designerActionManager().addResourceHandler(); QMultiMap map; for (const AddResourceHandler &handler : handlers) { map.insert(handler.category, handler.filter); } QMap reverseMap; for (const AddResourceHandler &handler : handlers) { reverseMap.insert(handler.filter, handler.category); } QMap priorities; for (const AddResourceHandler &handler : handlers) { priorities.insert(handler.category, handler.piority); } QStringList sortedKeys = map.uniqueKeys(); Utils::sort(sortedKeys, [&priorities](const QString &first, const QString &second){ return priorities.value(first) < priorities.value(second); }); QStringList fileNames = files; if (fileNames.isEmpty()) { QStringList filters; for (const QString &key : qAsConst(sortedKeys)) { QString str = key + " ("; str.append(map.values(key).join(" ")); str.append(")"); filters.append(str); } filters.prepend(tr("All Files (%1)").arg(map.values().join(" "))); static QString lastDir; const QString currentDir = lastDir.isEmpty() ? document->fileName().parentDir().toString() : lastDir; fileNames = QFileDialog::getOpenFileNames(Core::ICore::dialogParent(), tr("Add Assets"), currentDir, filters.join(";;")); if (!fileNames.isEmpty()) { lastDir = QFileInfo(fileNames.first()).absolutePath(); // switch to assets view after an asset is added m_stackedWidget->setCurrentIndex(1); QMetaObject::invokeMethod(m_headerWidget->rootObject(), "setTab", Q_ARG(QVariant, 1)); } } QMultiMap partitionedFileNames; for (const QString &fileName : qAsConst(fileNames)) { const QString suffix = "*." + QFileInfo(fileName).suffix().toLower(); const QString category = reverseMap.value(suffix); partitionedFileNames.insert(category, fileName); } for (const QString &category : partitionedFileNames.uniqueKeys()) { for (const AddResourceHandler &handler : handlers) { QStringList fileNames = partitionedFileNames.values(category); if (handler.category == category) { QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_RESOURCE_IMPORTED + category); if (!handler.operation(fileNames, document->fileName().parentDir().toString())) Core::AsynchronousMessageBox::warning(tr("Failed to Add Files"), tr("Could not add %1 to project.").arg(fileNames.join(" "))); break; } } } } void ItemLibraryWidget::importDroppedFiles(const QList &files) { QStringList fileNames; for (const auto &file : files) { QFileInfo fi(file.filePath); if (m_resourcesFileSystemModel->supportedSuffixes().contains(fi.suffix().toLower())) fileNames.append(fi.absoluteFilePath()); } if (!fileNames.isEmpty()) addResources(fileNames); } } // namespace QmlDesigner