1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "itemlibrarymodel.h"
27 #include "itemlibrarycategoriesmodel.h"
28 #include "itemlibraryimport.h"
29 #include "itemlibrarycategory.h"
30 #include "itemlibraryitem.h"
31 #include "itemlibraryinfo.h"
32 
33 #include <designermcumanager.h>
34 #include <model.h>
35 #include <nodehints.h>
36 #include <nodemetainfo.h>
37 #include <projectexplorer/project.h>
38 #include <projectexplorer/session.h>
39 #include "qmldesignerplugin.h"
40 #include <utils/algorithm.h>
41 #include <utils/qtcassert.h>
42 
43 #include <QIODevice>
44 #include <QLoggingCategory>
45 #include <QMetaProperty>
46 #include <QMimeData>
47 #include <QVariant>
48 
49 namespace QmlDesigner {
50 
51 // sectionName can be an import url or a category name
saveExpandedState(bool expanded,const QString & sectionName)52 void ItemLibraryModel::saveExpandedState(bool expanded, const QString &sectionName)
53 {
54     expandedStateHash.insert(sectionName, expanded);
55 }
56 
loadExpandedState(const QString & sectionName)57 bool ItemLibraryModel::loadExpandedState(const QString &sectionName)
58 {
59     return expandedStateHash.value(sectionName, true);
60 }
61 
saveCategoryVisibleState(bool isVisible,const QString & categoryName,const QString & importName)62 void ItemLibraryModel::saveCategoryVisibleState(bool isVisible, const QString &categoryName, const
63                                                 QString &importName)
64 {
65     categoryVisibleStateHash.insert(categoryName + '_' + importName, isVisible);
66 }
67 
loadCategoryVisibleState(const QString & categoryName,const QString & importName)68 bool ItemLibraryModel::loadCategoryVisibleState(const QString &categoryName, const QString &importName)
69 {
70     return categoryVisibleStateHash.value(categoryName + '_' + importName, true);
71 }
72 
showHiddenCategories()73 void ItemLibraryModel::showHiddenCategories()
74 {
75     for (const QPointer<ItemLibraryImport> &import : std::as_const(m_importList)) {
76         if (import->hasCategories())
77             import->showAllCategories(true);
78     }
79 
80     categoryVisibleStateHash.clear();
81 }
82 
getIsAnyCategoryHidden() const83 bool ItemLibraryModel::getIsAnyCategoryHidden() const
84 {
85     for (const bool &catState : std::as_const(categoryVisibleStateHash)) {
86         if (!catState)
87             return true;
88     }
89 
90     return false;
91 }
92 
isAnyCategoryHidden() const93 bool ItemLibraryModel::isAnyCategoryHidden() const
94 {
95     return m_isAnyCategoryHidden;
96 }
97 
setIsAnyCategoryHidden(bool state)98 void ItemLibraryModel::setIsAnyCategoryHidden(bool state)
99 {
100     if (state != m_isAnyCategoryHidden) {
101         m_isAnyCategoryHidden = state;
102         emit isAnyCategoryHiddenChanged();
103     }
104 }
105 
expandAll()106 void ItemLibraryModel::expandAll()
107 {
108     int i = 0;
109     for (const QPointer<ItemLibraryImport> &import : std::as_const(m_importList)) {
110         if (!import->importExpanded()) {
111             import->setImportExpanded();
112             emit dataChanged(index(i), index(i), {m_roleNames.key("importExpanded")});
113             saveExpandedState(true, import->importUrl());
114         }
115         import->expandCategories(true);
116         ++i;
117     }
118 }
119 
collapseAll()120 void ItemLibraryModel::collapseAll()
121 {
122     int i = 0;
123     for (const QPointer<ItemLibraryImport> &import : std::as_const(m_importList)) {
124         if (import->hasCategories() && import->importExpanded()) {
125             import->setImportExpanded(false);
126             emit dataChanged(index(i), index(i), {m_roleNames.key("importExpanded")});
127             saveExpandedState(false, import->importUrl());
128         }
129         ++i;
130     }
131 }
132 
setFlowMode(bool b)133 void ItemLibraryModel::setFlowMode(bool b)
134 {
135     m_flowMode = b;
136     bool changed;
137     updateVisibility(&changed);
138 }
139 
ItemLibraryModel(QObject * parent)140 ItemLibraryModel::ItemLibraryModel(QObject *parent)
141     : QAbstractListModel(parent)
142 {
143     addRoleNames();
144 }
145 
~ItemLibraryModel()146 ItemLibraryModel::~ItemLibraryModel()
147 {
148     clearSections();
149 }
150 
rowCount(const QModelIndex &) const151 int ItemLibraryModel::rowCount(const QModelIndex & /*parent*/) const
152 {
153     return m_importList.count();
154 }
155 
data(const QModelIndex & index,int role) const156 QVariant ItemLibraryModel::data(const QModelIndex &index, int role) const
157 {
158     if (!index.isValid() || index.row() >= m_importList.count())
159         return {};
160 
161     if (m_roleNames.contains(role)) {
162         QVariant value = m_importList.at(index.row())->property(m_roleNames.value(role));
163 
164         auto model = qobject_cast<ItemLibraryCategoriesModel *>(value.value<QObject *>());
165         if (model)
166             return QVariant::fromValue(model);
167 
168         return value;
169     }
170 
171     qWarning() << Q_FUNC_INFO << "invalid role requested";
172 
173     return {};
174 }
175 
setData(const QModelIndex & index,const QVariant & value,int role)176 bool ItemLibraryModel::setData(const QModelIndex &index, const QVariant &value, int role)
177 {
178     // currently only importExpanded property is updatable
179     if (index.isValid() && m_roleNames.contains(role)) {
180         QVariant currValue = m_importList.at(index.row())->property(m_roleNames.value(role));
181         if (currValue != value) {
182             m_importList[index.row()]->setProperty(m_roleNames.value(role), value);
183             if (m_roleNames.value(role) == "importExpanded")
184                 saveExpandedState(value.toBool(), m_importList[index.row()]->importUrl());
185             emit dataChanged(index, index, {role});
186             return true;
187         }
188     }
189     return false;
190 }
191 
roleNames() const192 QHash<int, QByteArray> ItemLibraryModel::roleNames() const
193 {
194     return m_roleNames;
195 }
196 
searchText() const197 QString ItemLibraryModel::searchText() const
198 {
199     return m_searchText;
200 }
201 
setSearchText(const QString & searchText)202 void ItemLibraryModel::setSearchText(const QString &searchText)
203 {
204     QString lowerSearchText = searchText.toLower();
205 
206     if (m_searchText != lowerSearchText) {
207         m_searchText = lowerSearchText;
208 
209         bool changed = false;
210         updateVisibility(&changed);
211     }
212 }
213 
entryToImport(const ItemLibraryEntry & entry)214 Import ItemLibraryModel::entryToImport(const ItemLibraryEntry &entry)
215 {
216     if (entry.majorVersion() == -1 && entry.minorVersion() == -1)
217         return Import::createFileImport(entry.requiredImport());
218 
219     return Import::createLibraryImport(entry.requiredImport(), QString::number(entry.majorVersion()) + QLatin1Char('.') +
220                                                                QString::number(entry.minorVersion()));
221 
222 }
223 
224 // Returns true if first import version is higher or equal to second import version
compareVersions(const QString & version1,const QString & version2)225 static bool compareVersions(const QString &version1, const QString &version2)
226 {
227     if (version2.isEmpty() || version1 == version2)
228         return true;
229     const QStringList version1List = version1.split(QLatin1Char('.'));
230     const QStringList version2List = version2.split(QLatin1Char('.'));
231     if (version1List.count() == 2 && version2List.count() == 2) {
232         int major1 = version1List.constFirst().toInt();
233         int major2 = version2List.constFirst().toInt();
234         if (major1 > major2) {
235             return true;
236         } else if (major1 == major2) {
237             int minor1 = version1List.constLast().toInt();
238             int minor2 = version2List.constLast().toInt();
239             if (minor1 >= minor2)
240                 return true;
241         }
242     }
243     return false;
244 }
245 
update(ItemLibraryInfo * itemLibraryInfo,Model * model)246 void ItemLibraryModel::update(ItemLibraryInfo *itemLibraryInfo, Model *model)
247 {
248     if (!model)
249         return;
250 
251     beginResetModel();
252     clearSections();
253 
254     Utils::FilePath qmlFileName = QmlDesignerPlugin::instance()->currentDesignDocument()->fileName();
255     ProjectExplorer::Project *project = ProjectExplorer::SessionManager::projectForFile(qmlFileName);
256     QString projectName = project ? project->displayName() : "";
257 
258     // create import sections
259     const QList<Import> usedImports = model->usedImports();
260     QHash<QString, ItemLibraryImport *> importHash;
261     for (const Import &import : model->imports()) {
262         if (import.url() != projectName) {
263             bool addNew = true;
264             bool isQuick3DAsset = import.url().startsWith("Quick3DAssets.");
265             QString importUrl = import.url();
266             if (isQuick3DAsset)
267                 importUrl = ItemLibraryImport::quick3DAssetsTitle();
268             else if (import.isFileImport())
269                 importUrl = import.toString(true, true).remove("\"");
270 
271             ItemLibraryImport *oldImport = importHash.value(importUrl);
272             if (oldImport && oldImport->sectionType() == ItemLibraryImport::SectionType::Quick3DAssets
273                 && isQuick3DAsset) {
274                 addNew = false; // add only 1 Quick3DAssets import section
275             } else if (oldImport && oldImport->importEntry().url() == import.url()) {
276                 // Retain the higher version if multiples exist
277                 if (compareVersions(oldImport->importEntry().version(), import.version()))
278                     addNew = false;
279                 else
280                     delete oldImport;
281             }
282 
283             if (addNew) {
284                 auto sectionType = isQuick3DAsset ? ItemLibraryImport::SectionType::Quick3DAssets
285                                                   : ItemLibraryImport::SectionType::Default;
286                 ItemLibraryImport *itemLibImport = new ItemLibraryImport(import, this, sectionType);
287                 itemLibImport->setImportUsed(usedImports.contains(import));
288                 importHash.insert(importUrl, itemLibImport);
289             }
290         }
291     }
292 
293     for (const auto itemLibImport : qAsConst(importHash)) {
294         m_importList.append(itemLibImport);
295         itemLibImport->setImportExpanded(loadExpandedState(itemLibImport->importUrl()));
296     }
297 
298     const QList<ItemLibraryEntry> itemLibEntries = itemLibraryInfo->entries();
299     for (const ItemLibraryEntry &entry : itemLibEntries) {
300         NodeMetaInfo metaInfo = model->metaInfo(entry.typeName());
301 
302         bool valid = metaInfo.isValid()
303                      && (metaInfo.majorVersion() >= entry.majorVersion()
304                          || metaInfo.majorVersion() < 0);
305 
306         bool isItem = valid && metaInfo.isSubclassOf("QtQuick.Item");
307         bool forceVisibility = valid && NodeHints::fromItemLibraryEntry(entry).visibleInLibrary();
308 
309         if (m_flowMode && metaInfo.isValid()) {
310             isItem = metaInfo.isSubclassOf("FlowView.FlowItem")
311                     || metaInfo.isSubclassOf("FlowView.FlowWildcard")
312                     || metaInfo.isSubclassOf("FlowView.FlowDecision");
313             forceVisibility = isItem;
314         }
315 
316         bool blocked = false;
317         const DesignerMcuManager &mcuManager = DesignerMcuManager::instance();
318         if (mcuManager.isMCUProject()) {
319             const QSet<QString> blockTypes = mcuManager.bannedItems();
320 
321             if (blockTypes.contains(QString::fromUtf8(entry.typeName())))
322                 blocked = true;
323         }
324 
325         Import import = entryToImport(entry);
326         bool hasImport = model->hasImport(import, true, true);
327         bool isImportPossible = false;
328         if (!hasImport)
329             isImportPossible = model->isImportPossible(import, true, true);
330         bool isUsable = (valid && (isItem || forceVisibility))
331                 && (entry.requiredImport().isEmpty() || hasImport);
332         if (!blocked && (isUsable || isImportPossible)) {
333             ItemLibraryImport *importSection = nullptr;
334             QString catName = entry.category();
335             if (isUsable) {
336                 if (catName == ItemLibraryImport::userComponentsTitle()) {
337                     if (entry.requiredImport().isEmpty()) { // user components
338                         importSection = importHash[ItemLibraryImport::userComponentsTitle()];
339                         if (!importSection) {
340                             importSection = new ItemLibraryImport(
341                                         {}, this, ItemLibraryImport::SectionType::User);
342                             m_importList.append(importSection);
343                             importHash.insert(ItemLibraryImport::userComponentsTitle(), importSection);
344                             importSection->setImportExpanded(loadExpandedState(catName));
345                         }
346                     } else { // directory import
347                         importSection = importHash[entry.requiredImport()];
348 
349                     }
350                 } else if (catName == ItemLibraryImport::quick3DAssetsTitle()) {
351                     importSection = importHash[ItemLibraryImport::quick3DAssetsTitle()];
352                 } else {
353                     if (catName.contains("Qt Quick - ")) {
354                         QString sortingName = catName;
355                         catName = catName.mid(11 + catName.indexOf("Qt Quick - ")); // remove "Qt Quick - " or "x.Qt Quick - "
356                         categorySortingHash.insert(catName, sortingName);
357                     }
358 
359                     importSection = importHash[entry.requiredImport().isEmpty() ? "QtQuick"
360                                                                                 : entry.requiredImport()];
361                 }
362             } else {
363                 catName = ItemLibraryImport::unimportedComponentsTitle();
364                 importSection = importHash[catName];
365                 if (!importSection) {
366                     importSection = new ItemLibraryImport(
367                                 {}, this, ItemLibraryImport::SectionType::Unimported);
368                     m_importList.append(importSection);
369                     importHash.insert(ItemLibraryImport::unimportedComponentsTitle(), importSection);
370                     importSection->setImportExpanded(loadExpandedState(catName));
371                 }
372             }
373 
374             if (!importSection) {
375                 qWarning() << __FUNCTION__ << "No import section found! skipping entry: " << entry.name();
376                 continue;
377             }
378 
379             // get or create category section
380             ItemLibraryCategory *categorySection = importSection->getCategorySection(catName);
381             if (!categorySection) {
382                 categorySection = new ItemLibraryCategory(catName, importSection);
383                 importSection->addCategory(categorySection);
384             }
385             if (importSection->sectionType() == ItemLibraryImport::SectionType::Default
386                 && !importSection->hasSingleCategory()) {
387                 categorySection->setExpanded(loadExpandedState(categorySection->categoryName()));
388             }
389 
390             // create item
391             auto item = new ItemLibraryItem(entry, isUsable, categorySection);
392             categorySection->addItem(item);
393         }
394     }
395 
396     sortSections();
397     bool changed = false;
398     updateVisibility(&changed);
399     endResetModel();
400 }
401 
getMimeData(const ItemLibraryEntry & itemLibraryEntry)402 QMimeData *ItemLibraryModel::getMimeData(const ItemLibraryEntry &itemLibraryEntry)
403 {
404     auto mimeData = new QMimeData();
405 
406     QByteArray data;
407     QDataStream stream(&data, QIODevice::WriteOnly);
408     stream << itemLibraryEntry;
409     mimeData->setData(QStringLiteral("application/vnd.bauhaus.itemlibraryinfo"), data);
410 
411     mimeData->removeFormat(QStringLiteral("text/plain"));
412 
413     return mimeData;
414 }
415 
clearSections()416 void ItemLibraryModel::clearSections()
417 {
418     qDeleteAll(m_importList);
419     m_importList.clear();
420 }
421 
registerQmlTypes()422 void ItemLibraryModel::registerQmlTypes()
423 {
424     qmlRegisterAnonymousType<QmlDesigner::ItemLibraryModel>("ItemLibraryModel", 1);
425 }
426 
importByUrl(const QString & importUrl) const427 ItemLibraryImport *ItemLibraryModel::importByUrl(const QString &importUrl) const
428 {
429     for (ItemLibraryImport *itemLibraryImport : std::as_const(m_importList)) {
430         if (itemLibraryImport->importUrl() == importUrl
431             || (importUrl.isEmpty() && itemLibraryImport->importUrl() == "QtQuick")
432             || (importUrl == ItemLibraryImport::userComponentsTitle()
433                 && itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::User)
434             || (importUrl == ItemLibraryImport::quick3DAssetsTitle()
435                 && itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::Quick3DAssets)
436             || (importUrl == ItemLibraryImport::unimportedComponentsTitle()
437                 && itemLibraryImport->sectionType() == ItemLibraryImport::SectionType::Unimported)) {
438             return itemLibraryImport;
439         }
440     }
441 
442     return nullptr;
443 }
444 
updateUsedImports(const QList<Import> & usedImports)445 void ItemLibraryModel::updateUsedImports(const QList<Import> &usedImports)
446 {
447     // imports in the excludeList are not marked used and thus can always be removed even when in use.
448     const QList<QString> excludeList = {"SimulinkConnector"};
449 
450     for (ItemLibraryImport *importSection : std::as_const(m_importList)) {
451         if (!excludeList.contains(importSection->importUrl()))
452             importSection->setImportUsed(usedImports.contains(importSection->importEntry()));
453     }
454 }
455 
updateVisibility(bool * changed)456 void ItemLibraryModel::updateVisibility(bool *changed)
457 {
458     for (ItemLibraryImport *import : std::as_const(m_importList)) {
459         bool categoryChanged = false;
460         bool hasVisibleItems = import->updateCategoryVisibility(m_searchText, &categoryChanged);
461         *changed |= categoryChanged;
462 
463         if (import->sectionType() == ItemLibraryImport::SectionType::Unimported)
464             *changed |= import->setVisible(!m_searchText.isEmpty());
465 
466         // expand import if it has an item matching search criteria
467         if (!m_searchText.isEmpty() && hasVisibleItems && !import->importExpanded())
468             import->setImportExpanded();
469     }
470 
471     if (changed) {
472         beginResetModel();
473         endResetModel();
474     }
475 }
476 
addRoleNames()477 void ItemLibraryModel::addRoleNames()
478 {
479     int role = 0;
480     const QMetaObject meta = ItemLibraryImport::staticMetaObject;
481     for (int i = meta.propertyOffset(); i < meta.propertyCount(); ++i)
482         m_roleNames.insert(role++, meta.property(i).name());
483 }
484 
sortSections()485 void ItemLibraryModel::sortSections()
486 {
487     auto sectionSort = [](ItemLibraryImport *first, ItemLibraryImport *second) {
488         return QString::localeAwareCompare(first->sortingName(), second->sortingName()) < 0;
489     };
490 
491     std::sort(m_importList.begin(), m_importList.end(), sectionSort);
492 
493     for (ItemLibraryImport *itemLibImport : qAsConst(m_importList))
494         itemLibImport->sortCategorySections();
495 }
496 
497 } // namespace QmlDesigner
498 
499