diff options
Diffstat (limited to 'src')
83 files changed, 2188 insertions, 387 deletions
diff --git a/src/libs/qtcreatorcdbext/CMakeLists.txt b/src/libs/qtcreatorcdbext/CMakeLists.txt index 297da2d33bb..c90d871e574 100644 --- a/src/libs/qtcreatorcdbext/CMakeLists.txt +++ b/src/libs/qtcreatorcdbext/CMakeLists.txt @@ -69,6 +69,7 @@ if (NOT QT_CREATOR_API_DEFINED) SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CMAKE_GENERATOR "${generator}" CMAKE_GENERATOR_PLATFORM "${arch}" + USES_TERMINAL_INSTALL TRUE LIST_SEPARATOR | CMAKE_ARGS -D${PROJECT_NAME}-MultiBuild=ON diff --git a/src/libs/solutions/tasking/tasktree.cpp b/src/libs/solutions/tasking/tasktree.cpp index 35050607ddf..ddcc30eb577 100644 --- a/src/libs/solutions/tasking/tasktree.cpp +++ b/src/libs/solutions/tasking/tasktree.cpp @@ -2296,33 +2296,27 @@ void TaskTreePrivate::startChildren(RuntimeContainer *container) const ContainerNode &containerNode = container->m_containerNode; const int childCount = int(containerNode.m_children.size()); - if (container->m_iterationCount == 0) { - if (container->m_shouldIterate && !invokeLoopHandler(container)) { - if (isProgressive(container)) - advanceProgress(containerNode.m_taskCount); - container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit); - return; - } - container->m_iterations.emplace_back( - std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); - ++container->m_iterationCount; - } - GuardLocker locker(container->m_startGuard); while (containerNode.m_parallelLimit == 0 || container->m_runningChildren < containerNode.m_parallelLimit) { container->deleteFinishedIterations(); - if (container->m_nextToStart == childCount) { - if (invokeLoopHandler(container)) { + const bool firstIteration = container->m_iterationCount == 0; + if (firstIteration || container->m_nextToStart == childCount) { + const bool skipHandler = firstIteration && !container->m_shouldIterate; + if (skipHandler || invokeLoopHandler(container)) { container->m_nextToStart = 0; - container->m_iterations.emplace_back( - std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); + if (containerNode.m_children.size() > 0) { + container->m_iterations.emplace_back( + std::make_unique<RuntimeIteration>(container->m_iterationCount, container)); + } ++container->m_iterationCount; - } else if (container->m_iterations.empty()) { - container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit); - return; } else { + if (container->m_iterations.empty()) { + if (firstIteration && isProgressive(container)) + advanceProgress(containerNode.m_taskCount); + container->m_parentTask->m_setupResult = toSetupResult(container->m_successBit); + } return; } } diff --git a/src/libs/utils/aspects.h b/src/libs/utils/aspects.h index 2343933e0af..2fe1c45223b 100644 --- a/src/libs/utils/aspects.h +++ b/src/libs/utils/aspects.h @@ -360,12 +360,12 @@ public: announceChanges(changes, howToAnnounce); } -protected: bool isDirty() override { return m_internal != m_buffer; } +protected: bool internalToBuffer() override { return updateStorage(m_buffer, m_internal); diff --git a/src/libs/utils/filesystemwatcher.cpp b/src/libs/utils/filesystemwatcher.cpp index 999b884e8d9..82c40c81719 100644 --- a/src/libs/utils/filesystemwatcher.cpp +++ b/src/libs/utils/filesystemwatcher.cpp @@ -86,8 +86,14 @@ class WatchEntry public: using WatchMode = FileSystemWatcher::WatchMode; - explicit WatchEntry(const FilePath &file, WatchMode wm) : - watchMode(wm), modifiedTime(file.lastModified()) {} + explicit WatchEntry(const FilePath &file, WatchMode wm) + : watchMode(wm) + { + if (watchMode == FileSystemWatcher::WatchModifiedDate) { + modifiedTime = file.lastModified(); + QTC_CHECK(modifiedTime.isValid()); + } + } bool trigger(const FilePath &fileName); @@ -100,7 +106,9 @@ bool WatchEntry::trigger(const FilePath &filePath) { if (watchMode == FileSystemWatcher::WatchAllChanges) return true; + // Modified changed? + QTC_ASSERT(modifiedTime.isValid(), return false); const QDateTime newModifiedTime = filePath.exists() ? filePath.lastModified() : QDateTime(); if (newModifiedTime != modifiedTime) { modifiedTime = newModifiedTime; diff --git a/src/libs/utils/id.h b/src/libs/utils/id.h index b23c143ccdb..43f2f6cd518 100644 --- a/src/libs/utils/id.h +++ b/src/libs/utils/id.h @@ -54,12 +54,12 @@ public: bool operator>(Id id) const { return m_id > id.m_id; } bool alphabeticallyBefore(Id other) const; - static Id fromString(QStringView str); // FIXME: avoid. - static Id fromName(QByteArrayView ba); // FIXME: avoid. - static Id fromSetting(const QVariant &variant); // Good to use. + [[nodiscard]] static Id fromString(QStringView str); // FIXME: avoid. + [[nodiscard]] static Id fromName(QByteArrayView ba); // FIXME: avoid. + [[nodiscard]] static Id fromSetting(const QVariant &variant); // Good to use. - static QSet<Id> fromStringList(const QStringList &list); - static QStringList toStringList(const QSet<Id> &ids); + [[nodiscard]] static QSet<Id> fromStringList(const QStringList &list); + [[nodiscard]] static QStringList toStringList(const QSet<Id> &ids); friend size_t qHash(Id id) { return static_cast<size_t>(id.m_id); } friend QTCREATOR_UTILS_EXPORT QDataStream &operator<<(QDataStream &ds, Id id); diff --git a/src/libs/utils/multitextcursor.cpp b/src/libs/utils/multitextcursor.cpp index 2ddef5222f8..f57eac0199d 100644 --- a/src/libs/utils/multitextcursor.cpp +++ b/src/libs/utils/multitextcursor.cpp @@ -339,21 +339,6 @@ void MultiTextCursor::mergeCursors() setCursors(cursors); } -// could go into QTextCursor... -static QTextLine currentTextLine(const QTextCursor &cursor) -{ - const QTextBlock block = cursor.block(); - if (!block.isValid()) - return {}; - - const QTextLayout *layout = block.layout(); - if (!layout) - return {}; - - const int relativePos = cursor.position() - block.position(); - return layout->lineForTextPosition(relativePos); -} - bool MultiTextCursor::multiCursorEvent( QKeyEvent *e, QKeySequence::StandardKey matchKey, Qt::KeyboardModifiers filterModifiers) { diff --git a/src/libs/utils/stringutils.cpp b/src/libs/utils/stringutils.cpp index 8733a0f0ae2..9a63a74c0cd 100644 --- a/src/libs/utils/stringutils.cpp +++ b/src/libs/utils/stringutils.cpp @@ -23,6 +23,7 @@ #include <QPalette> #include <QRegularExpression> #include <QSet> +#include <QStack> #include <QTextDocument> #include <QTextList> #include <QTime> @@ -96,6 +97,58 @@ QTCREATOR_UTILS_EXPORT QString stripAccelerator(const QString &text) return res; } +static bool isJsonWhitespace(char ch) +{ + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; +} + +static bool isNotJsonWhitespace(char ch) +{ + return !isJsonWhitespace(ch); +} + +QTCREATOR_UTILS_EXPORT QByteArray removeExtraCommasFromJson(const QByteArray &json) +{ + QByteArray result; + result.reserve(json.size()); + + enum State { Normal, InString, Escape }; + + State state = Normal; + + for (const char c : json) { + result.append(c); + + switch (state) { + case Normal: + if (c == '"') { + state = InString; + } else if (c == '}' || c == ']') { + auto firstNonWhitespace + = std::find_if(result.rbegin() + 1, result.rend(), isNotJsonWhitespace); + + if (firstNonWhitespace != result.rend() && *firstNonWhitespace == ',') + result.erase(firstNonWhitespace.base() - 1); + } + break; + + case InString: + if (c == '\\') { + state = Escape; + } else if (c == '"') { + state = Normal; + } + break; + + case Escape: + state = InString; + break; + } + } + + return result; +} + QTCREATOR_UTILS_EXPORT QByteArray removeCommentsFromJson(const QByteArray &input) { QByteArray output; @@ -146,6 +199,11 @@ QTCREATOR_UTILS_EXPORT QByteArray removeCommentsFromJson(const QByteArray &input return output; } +QTCREATOR_UTILS_EXPORT QByteArray cleanJson(const QByteArray &json) +{ + return removeExtraCommasFromJson(removeCommentsFromJson(json)); +} + QTCREATOR_UTILS_EXPORT bool readMultiLineString(const QJsonValue &value, QString *out) { QTC_ASSERT(out, return false); diff --git a/src/libs/utils/stringutils.h b/src/libs/utils/stringutils.h index 452297516e2..34068fe1366 100644 --- a/src/libs/utils/stringutils.h +++ b/src/libs/utils/stringutils.h @@ -41,7 +41,9 @@ QTCREATOR_UTILS_EXPORT QString asciify(const QString &input); QTCREATOR_UTILS_EXPORT bool readMultiLineString(const QJsonValue &value, QString *out); +QTCREATOR_UTILS_EXPORT QByteArray removeExtraCommasFromJson(const QByteArray &json); QTCREATOR_UTILS_EXPORT QByteArray removeCommentsFromJson(const QByteArray &json); +QTCREATOR_UTILS_EXPORT QByteArray cleanJson(const QByteArray &json); // Compare case insensitive and use case sensitive comparison in case of that being equal. QTCREATOR_UTILS_EXPORT int caseFriendlyCompare(const QString &a, const QString &b); diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index b094f3b986b..7a9383c67c3 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -36,7 +36,6 @@ add_subdirectory(languageclient) # Level 4: (only depends on Level 3 and below) add_subdirectory(classview) add_subdirectory(glsleditor) -add_subdirectory(learning) add_subdirectory(modeleditor) add_subdirectory(qtsupport) add_subdirectory(todo) @@ -55,6 +54,7 @@ add_subdirectory(fakevim) add_subdirectory(fossil) add_subdirectory(genericprojectmanager) add_subdirectory(git) +add_subdirectory(learning) add_subdirectory(mercurial) add_subdirectory(mesonprojectmanager) add_subdirectory(perforce) diff --git a/src/plugins/android/androidconfigurations.cpp b/src/plugins/android/androidconfigurations.cpp index ad3d4086a3c..ee91d3cb084 100644 --- a/src/plugins/android/androidconfigurations.cpp +++ b/src/plugins/android/androidconfigurations.cpp @@ -1442,12 +1442,10 @@ void AndroidConfigurations::updateAutomaticKitList() k->setSticky(QtKitAspect::id(), true); k->setSticky(RunDeviceTypeKitAspect::id(), true); - QString versionStr = QLatin1String("Qt %{Qt:Version}"); - if (!qt->detectionSource().isAutoDetected()) - versionStr = QString("%1").arg(qt->displayName()); const QStringList abis = static_cast<const AndroidQtVersion *>(qt)->androidAbis(); - k->setUnexpandedDisplayName(Tr::tr("Android %1 %2") - .arg(versionStr, getMultiOrSingleAbiString(abis))); + + k->setUnexpandedDisplayName(Tr::tr("%1 for Android %2") + .arg(QLatin1String("Qt %{Qt:Version}"), getMultiOrSingleAbiString(abis))); k->setValueSilently( Constants::ANDROID_KIT_NDK, AndroidConfig::ndkLocation(qt).toSettings()); k->setValueSilently( diff --git a/src/plugins/android/androidmanifesteditor.cpp b/src/plugins/android/androidmanifesteditor.cpp index af08ed67d4c..23635049fb8 100644 --- a/src/plugins/android/androidmanifesteditor.cpp +++ b/src/plugins/android/androidmanifesteditor.cpp @@ -1996,7 +1996,7 @@ AndroidManifestEditor::AndroidManifestEditor(AndroidManifestEditorWidget *editor m_actionGroup->addAction(sourceAction); sourceAction->setChecked(true); - QAction *generalAction = m_toolBar->addAction(Tr::tr("General")); + QAction *generalAction = m_toolBar->addAction(Tr::tr("Graphical Editor")); generalAction->setData(AndroidManifestEditorWidget::General); generalAction->setCheckable(true); m_actionGroup->addAction(generalAction); diff --git a/src/plugins/clangtools/clangselectablefilesdialog.cpp b/src/plugins/clangtools/clangselectablefilesdialog.cpp index 3ae08067f41..b2d0993c690 100644 --- a/src/plugins/clangtools/clangselectablefilesdialog.cpp +++ b/src/plugins/clangtools/clangselectablefilesdialog.cpp @@ -21,6 +21,7 @@ #include <QPushButton> #include <QSortFilterProxyModel> #include <QStandardItem> +#include <QTreeView> using namespace CppEditor; using namespace Utils; @@ -95,17 +96,16 @@ public: projectDirTree->fullPath = m_root->fullPath; projectDirTree->parent = m_root->parent; - delete m_root; // OK, it has no files / child dirs. - - m_root = projectDirTree; + // The m_root has no files / child dirs, so we delete it. + m_root.reset(projectDirTree); } else { // Set up project dir node as sub node of the project file node - linkDirNode(m_root, projectDirTree); + linkDirNode(m_root.get(), projectDirTree); // Add files outside of the base directory to a separate node Tree *externalFilesNode = createDirNode(Tr::tr("Files outside of the base directory"), "/"); - linkDirNode(m_root, externalFilesNode); + linkDirNode(m_root.get(), externalFilesNode); for (const FileInfo &fileInfo : outOfBaseDirFiles) linkFileNode(externalFilesNode, createFileNode(fileInfo, true)); } diff --git a/src/plugins/clangtools/diagnosticconfigswidget.cpp b/src/plugins/clangtools/diagnosticconfigswidget.cpp index 1499013298b..6a1e78c1f0e 100644 --- a/src/plugins/clangtools/diagnosticconfigswidget.cpp +++ b/src/plugins/clangtools/diagnosticconfigswidget.cpp @@ -566,13 +566,13 @@ class TidyChecksTreeModel final : public BaseChecksTreeModel public: TidyChecksTreeModel(const QStringList &supportedChecks) { - buildTree(nullptr, m_root, ClangTidyPrefixTree::Node::fromCheckList(supportedChecks)); + buildTree(nullptr, m_root.get(), ClangTidyPrefixTree::Node::fromCheckList(supportedChecks)); } QString selectedChecks() const override { QString checks; - collectChecks(m_root, checks); + collectChecks(m_root.get(), checks); return "-*" + checks; } @@ -685,7 +685,7 @@ public: ClazyChecksTreeModel(const ClazyChecks &supportedClazyChecks) { // Top level node - m_root = new ClazyChecksTree("*", ClazyChecksTree::TopLevelNode); + m_root.reset(new ClazyChecksTree("*", ClazyChecksTree::TopLevelNode)); for (const ClazyCheck &check : supportedClazyChecks) { // Level node @@ -693,7 +693,7 @@ public: if (!levelNode) { levelNode = new ClazyChecksTree(levelDescription(check.level), ClazyChecksTree::LevelNode); - levelNode->parent = m_root; + levelNode->parent = m_root.get(); levelNode->check.level = check.level; // Pass on the level for sorting m_root->childDirectories << levelNode; } @@ -712,7 +712,7 @@ public: QStringList enabledChecks() const { QStringList checks; - collectChecks(m_root, checks); + collectChecks(m_root.get(), checks); return checks; } diff --git a/src/plugins/cppcheck/cppcheckmanualrundialog.cpp b/src/plugins/cppcheck/cppcheckmanualrundialog.cpp index 9ba540f50fd..628044a81bc 100644 --- a/src/plugins/cppcheck/cppcheckmanualrundialog.cpp +++ b/src/plugins/cppcheck/cppcheckmanualrundialog.cpp @@ -16,6 +16,7 @@ #include <QBoxLayout> #include <QDialogButtonBox> #include <QPushButton> +#include <QTreeView> namespace Cppcheck::Internal { @@ -23,7 +24,7 @@ ManualRunDialog::ManualRunDialog(const ProjectExplorer::Project *project, CppcheckSettings *settings) : m_model(new ProjectExplorer::SelectableFilesFromDirModel(this)) { - QTC_ASSERT(project, return ); + QTC_ASSERT(project, return); QTC_ASSERT(settings, return); setWindowTitle(Tr::tr("Cppcheck Run Configuration")); diff --git a/src/plugins/git/gitsubmiteditor.cpp b/src/plugins/git/gitsubmiteditor.cpp index 1a79429d6f2..b8f08e843f5 100644 --- a/src/plugins/git/gitsubmiteditor.cpp +++ b/src/plugins/git/gitsubmiteditor.cpp @@ -11,7 +11,7 @@ #include <coreplugin/editormanager/editormanager.h> #include <coreplugin/fileutils.h> #include <coreplugin/iversioncontrol.h> -#include <coreplugin/progressmanager/progressmanager.h> +#include <coreplugin/progressmanager/taskprogress.h> #include <utils/async.h> #include <utils/environment.h> @@ -29,6 +29,8 @@ #include <QStringList> #include <QTimer> +using namespace Core; +using namespace Tasking; using namespace Utils; using namespace VcsBase; @@ -71,7 +73,7 @@ private: } }; -Result<CommitData> fetchCommitData(CommitType commitType, const FilePath &workingDirectory) +static Result<CommitData> fetchCommitData(CommitType commitType, const FilePath &workingDirectory) { return gitClient().getCommitData(commitType, workingDirectory); } @@ -89,10 +91,8 @@ GitSubmitEditor::GitSubmitEditor() : connect(submitEditorWidget(), &GitSubmitEditorWidget::logRequested, this, &GitSubmitEditor::showLog); connect(submitEditorWidget(), &GitSubmitEditorWidget::fileActionRequested, this, &GitSubmitEditor::performFileAction); - connect(versionControl(), &Core::IVersionControl::repositoryChanged, + connect(versionControl(), &IVersionControl::repositoryChanged, this, &GitSubmitEditor::forceUpdateFileModel); - connect(&m_fetchWatcher, &QFutureWatcher<Result<CommitData>>::finished, - this, &GitSubmitEditor::commitDataRetrieved); } GitSubmitEditor::~GitSubmitEditor() = default; @@ -109,7 +109,7 @@ const GitSubmitEditorWidget *GitSubmitEditor::submitEditorWidget() const void GitSubmitEditor::setCommitData(const CommitData &d) { - using IVCF = Core::IVersionControl::FileState; + using IVCF = IVersionControl::FileState; m_commitEncoding = d.commitEncoding; m_workingDirectory = d.panelInfo.repository; @@ -184,7 +184,7 @@ void GitSubmitEditor::slotDiffSelected(const QList<int> &rows) } stagedFiles.push_back(fileName); } else if (state == UntrackedFile) { - Core::EditorManager::openEditor(m_workingDirectory.pathAppended(fileName)); + EditorManager::openEditor(m_workingDirectory.pathAppended(fileName)); } else { unstagedFiles.push_back(fileName); } @@ -292,7 +292,7 @@ void GitSubmitEditor::performFileAction(const Utils::FilePath &filePath, FileAct break; case FileOpenEditor: - Core::EditorManager::openEditor(fullPath); + EditorManager::openEditor(fullPath); break; case FileStage: @@ -352,13 +352,36 @@ void GitSubmitEditor::updateFileModel() if (w->updateInProgress() || m_workingDirectory.isEmpty()) return; w->setUpdateInProgress(true); - // TODO: Check if fetch works OK from separate thread, refactor otherwise - m_fetchWatcher.setFuture(Utils::asyncRun(&fetchCommitData, - m_commitType, m_workingDirectory)); - Core::ProgressManager::addTask(m_fetchWatcher.future(), Tr::tr("Refreshing Commit Data"), - TASK_UPDATE_COMMIT); - Utils::futureSynchronizer()->addFuture(m_fetchWatcher.future()); + using ResultType = Result<CommitData>; + // TODO: Check if fetch works OK from separate thread, refactor otherwise + const auto onSetup = [this](Async<ResultType> &task) { + task.setConcurrentCallData(&fetchCommitData,m_commitType, + m_workingDirectory); + }; + const auto onDone = [this](const Async<ResultType> &task) { + const ResultType result = task.result(); + GitSubmitEditorWidget *w = submitEditorWidget(); + if (result) { + setCommitData(result.value()); + w->refreshLog(m_workingDirectory); + w->setEnabled(true); + } else { + // Nothing to commit left! + VcsOutputWindow::appendError(m_workingDirectory, result.error()); + m_model->clear(); + w->setEnabled(false); + } + w->setUpdateInProgress(false); + }; + const auto onTreeSetup = [](TaskTree &taskTree) { + auto progress = new TaskProgress(&taskTree); + progress->setDisplayName(Tr::tr("Refreshing Commit Data")); + progress->setId(TASK_UPDATE_COMMIT); + }; + m_taskTreeRunner.start( + {AsyncTask<ResultType>(onSetup, onDone, CallDone::OnSuccess)}, + onTreeSetup); } void GitSubmitEditor::forceUpdateFileModel() @@ -370,23 +393,6 @@ void GitSubmitEditor::forceUpdateFileModel() updateFileModel(); } -void GitSubmitEditor::commitDataRetrieved() -{ - const Result<CommitData> result = m_fetchWatcher.result(); - GitSubmitEditorWidget *w = submitEditorWidget(); - if (result) { - setCommitData(result.value()); - w->refreshLog(m_workingDirectory); - w->setEnabled(true); - } else { - // Nothing to commit left! - VcsOutputWindow::appendError(m_workingDirectory, result.error()); - m_model->clear(); - w->setEnabled(false); - } - w->setUpdateInProgress(false); -} - GitSubmitEditorPanelData GitSubmitEditor::panelData() const { return submitEditorWidget()->panelData(); diff --git a/src/plugins/git/gitsubmiteditor.h b/src/plugins/git/gitsubmiteditor.h index fac9d42fc12..5c49f6a953a 100644 --- a/src/plugins/git/gitsubmiteditor.h +++ b/src/plugins/git/gitsubmiteditor.h @@ -5,11 +5,12 @@ #include "commitdata.h" +#include <solutions/tasking/tasktreerunner.h> + #include <utils/filepath.h> #include <vcsbase/vcsbasesubmiteditor.h> -#include <QFutureWatcher> #include <QStringList> namespace VcsBase { class SubmitFileModel; } @@ -42,7 +43,6 @@ private: void showCommit(const QString &commit); void showLog(const QStringList &range); void performFileAction(const Utils::FilePath &filePath, FileAction action); - void commitDataRetrieved(); void addToGitignore(const Utils::FilePath &relativePath); inline GitSubmitEditorWidget *submitEditorWidget(); @@ -54,7 +54,7 @@ private: QString m_amenHash; Utils::FilePath m_workingDirectory; bool m_firstUpdate = true; - QFutureWatcher<Utils::Result<CommitData>> m_fetchWatcher; + Tasking::SingleTaskTreeRunner m_taskTreeRunner; }; } // Git::Internal diff --git a/src/plugins/languageclient/languageclientsettings.cpp b/src/plugins/languageclient/languageclientsettings.cpp index d5059cbc470..4624dc374fd 100644 --- a/src/plugins/languageclient/languageclientsettings.cpp +++ b/src/plugins/languageclient/languageclientsettings.cpp @@ -675,6 +675,7 @@ void BaseSettings::fromMap(const Store &map) m_languageFilter.filePattern.removeAll(QString()); // remove empty entries m_initializationOptions = map[initializationOptionsKey].toString(); m_configuration = map[configurationKey].toString(); + m_settingsTypeId = Id::fromSetting(map[typeIdKey]); } static LanguageClientSettingsPage &settingsPage() diff --git a/src/plugins/learning/CMakeLists.txt b/src/plugins/learning/CMakeLists.txt index f7f33898552..288f52fd12e 100644 --- a/src/plugins/learning/CMakeLists.txt +++ b/src/plugins/learning/CMakeLists.txt @@ -1,7 +1,10 @@ add_qtc_plugin(Learning - PLUGIN_DEPENDS Core + PLUGIN_DEPENDS Core ProjectExplorer QtSupport SOURCES learningplugin.cpp + learningsettings.h learningsettings.cpp + onboardingwizard.h onboardingwizard.cpp + overviewwelcomepage.h overviewwelcomepage.cpp qtacademywelcomepage.h qtacademywelcomepage.cpp ) @@ -10,3 +13,11 @@ extend_qtc_plugin(Learning SOURCES learning_test.qrc ) + +file(GLOB images RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} overview/*.webp overview/*.json) +qtc_add_resources(Learning "overview" + PREFIX "/learning" + BASE "." + FILES + ${images} +) diff --git a/src/plugins/learning/learning.qbs b/src/plugins/learning/learning.qbs index aadeef97bbb..3beed3c9e3e 100644 --- a/src/plugins/learning/learning.qbs +++ b/src/plugins/learning/learning.qbs @@ -4,12 +4,21 @@ QtcPlugin { name: "Learning" Depends { name: "Core" } + Depends { name: "ProjectExplorer" } + Depends { name: "QtSupport" } Depends { name: "Spinner" } Depends { name: "Tasking" } + Depends { name: "Qt.network" } files: [ "learningplugin.cpp", + "learningsettings.cpp", + "learningsettings.h", + "onboardingwizard.cpp", + "onboardingwizard.h", + "overviewwelcomepage.cpp", + "overviewwelcomepage.h", "qtacademywelcomepage.cpp", "qtacademywelcomepage.h", ] @@ -19,4 +28,15 @@ QtcPlugin { "learning_test.qrc", ] } + + Group { + name: "recommendations" + prefix: "overview/" + files: [ + "*.webp", + "*.json", + ] + fileTags: "qt.core.resource_data" + Qt.core.resourcePrefix: "learning" + } } diff --git a/src/plugins/learning/learningplugin.cpp b/src/plugins/learning/learningplugin.cpp index f716cb7cef0..12259424990 100644 --- a/src/plugins/learning/learningplugin.cpp +++ b/src/plugins/learning/learningplugin.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2025 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#include "overviewwelcomepage.h" #include "qtacademywelcomepage.h" #include <extensionsystem/iplugin.h> @@ -16,6 +17,10 @@ public: void initialize() final { setupQtAcademyWelcomePage(this); + setupOverviewWelcomePage(this); +#ifdef WITH_TESTS + addTest<LearningTest>(); +#endif // WITH_TESTS } }; diff --git a/src/plugins/learning/learningsettings.cpp b/src/plugins/learning/learningsettings.cpp new file mode 100644 index 00000000000..5a14fbf02b4 --- /dev/null +++ b/src/plugins/learning/learningsettings.cpp @@ -0,0 +1,37 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "learningsettings.h" + +using namespace Utils; + +namespace Learning::Internal { + +LearningSettings::LearningSettings() +{ + setSettingsGroup("Learning"); + + userFlags.setSettingsKey("UserFlags"); + userFlags.setDefaultValue({ + defaultExperience(), + QLatin1String(TARGET_PREFIX) + TARGET_DESKTOP, + }); + + showWizardOnStart.setSettingsKey("ShowWizardOnStart"); + showWizardOnStart.setDefaultValue(true); + + readSettings(); +} + +QString LearningSettings::defaultExperience() +{ + return QLatin1String(EXPERIENCE_PREFIX) + EXPERIENCE_BASIC; +} + +LearningSettings &settings() +{ + static LearningSettings theSettings; + return theSettings; +} + +} // namespace Learning::Internal diff --git a/src/plugins/learning/learningsettings.h b/src/plugins/learning/learningsettings.h new file mode 100644 index 00000000000..17b9e47b17e --- /dev/null +++ b/src/plugins/learning/learningsettings.h @@ -0,0 +1,34 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <utils/aspects.h> + +namespace Learning::Internal { + +const char EXPERIENCE_PREFIX[] = "experience_"; +const char EXPERIENCE_BASIC[] = "basic"; +const char EXPERIENCE_ADVANCED[] = "advanced"; + +const char TARGET_PREFIX[] = "target_"; +const char TARGET_DESKTOP[] = "desktop"; +const char TARGET_ANDROID[] = "android"; +const char TARGET_IOS[] = "ios"; +const char TARGET_BOOT2QT[] = "boot2qt"; +const char TARGET_QTFORMCUS[] = "qtformcus"; + +class LearningSettings final : public Utils::AspectContainer +{ +public: + LearningSettings(); + + static QString defaultExperience(); + + Utils::StringListAspect userFlags{this}; + Utils::BoolAspect showWizardOnStart{this}; +}; + +LearningSettings &settings(); + +} // namespace Learning::Internal diff --git a/src/plugins/learning/onboardingwizard.cpp b/src/plugins/learning/onboardingwizard.cpp new file mode 100644 index 00000000000..1abd43c82d1 --- /dev/null +++ b/src/plugins/learning/onboardingwizard.cpp @@ -0,0 +1,167 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "onboardingwizard.h" + +#include "learningtr.h" +#include "learningsettings.h" + +#include <utils/algorithm.h> +#include <utils/layoutbuilder.h> +#include <utils/overlaywidget.h> +#include <utils/qtcwidgets.h> +#include <utils/stylehelper.h> + +#include <coreplugin/welcomepagehelper.h> + +#include <QPainter> +#include <QRadioButton> +#include <QWidget> + +using namespace Utils; +using namespace Utils::StyleHelper; + +namespace Learning::Internal { + +class RecommendationsOptionsPage final : public QWidget +{ +public: + RecommendationsOptionsPage(QWidget *parent = nullptr) + : QWidget(parent) + { + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + auto levelBasic = new QRadioButton(Tr::tr("Basic")); + auto levelAdvanced = new QRadioButton(Tr::tr("Advanced")); + (level() == QLatin1String(EXPERIENCE_PREFIX) + EXPERIENCE_BASIC ? levelBasic + : levelAdvanced) + ->setChecked(true); + + const QString captionText = + Tr::tr("This helps us recommend the right tutorials and features."); + + using namespace Layouting; + auto vSpace = Space(SpacingTokens::GapVM); + Column { + tfLabel(Tr::tr("Personalize learning"), pageHeaderTf), + Tr::tr("Get tutorials and tips tailored to your role and experience."), + vSpace, + tfLabel(Tr::tr("Your experience level"), sectionHeaderTf), + Row { levelBasic, levelAdvanced, st }, + tfLabel(captionText, captionTf), + vSpace, + tfLabel(Tr::tr("Select your target platform(s)"), sectionHeaderTf), + Flow { + targetButton(Tr::tr("Desktop"), TARGET_DESKTOP), + targetButton(Tr::tr("Android"), TARGET_ANDROID), + targetButton(Tr::tr("iOS"), TARGET_IOS), + targetButton(Tr::tr("Boot2Qt"), TARGET_BOOT2QT), + targetButton(Tr::tr("Qt for MCUs"), TARGET_QTFORMCUS), + }, + tfLabel(captionText, captionTf), + noMargin, + }.attachTo(this); + + connect(levelBasic, &QAbstractButton::clicked, this, [] { + setLevel(EXPERIENCE_BASIC); + }); + connect(levelAdvanced, &QAbstractButton::clicked, this, [] { + setLevel(EXPERIENCE_ADVANCED); + }); + } + +private: + static void setLevel(const QString &level) + { + QStringList userFlags = Utils::filtered(settings().userFlags(), [](const QString &flag) { + return !flag.startsWith(EXPERIENCE_PREFIX); + }); + userFlags.append(EXPERIENCE_PREFIX + level); + settings().userFlags.setValue(userFlags); + } + + static QString level() + { + QStringList userFlags = Utils::filtered(settings().userFlags(), [](const QString &flag) { + return flag.startsWith(EXPERIENCE_PREFIX); + }); + return userFlags.isEmpty() ? settings().defaultExperience() : userFlags.first(); + } + + static QAbstractButton *targetButton(const QString &text, const QString &target) + { + const QString targetWithPrefix = TARGET_PREFIX + target; + auto button = new QtcButton(text, QtcButton::Tag); + button->setCheckable(true); + button->setChecked(settings().userFlags().contains(targetWithPrefix)); + button->setProperty(TARGET_PREFIX, targetWithPrefix); + QObject::connect(button, &QAbstractButton::toggled, button, [button]{ + const QString target = button->property(TARGET_PREFIX).toString(); + QStringList userFlags = settings().userFlags(); + userFlags.removeAll(target); + if (button->isChecked()) + userFlags.append(target); + settings().userFlags.setValue(userFlags); + }); + return button; + } + + static constexpr TextFormat pageHeaderTf + {Theme::Token_Text_Default, UiElement::UiElementH3}; + static constexpr TextFormat sectionHeaderTf + {pageHeaderTf.themeColor, UiElement::UiElementH5}; + static constexpr TextFormat captionTf + {Theme::Token_Text_Muted, UiElement::UiElementCaption}; +}; + +QWidget *createOnboardingWizard(QWidget *parent) +{ + auto closeButton = new QtcButton(Tr::tr("Close"), QtcButton::LargePrimary); + + auto widget = new OverlayWidget(parent); + widget->setAttribute(Qt::WA_TransparentForMouseEvents, false); + widget->setPaintFunction( + [](QWidget *that, QPainter &p, QPaintEvent *) + { + QColor color = creatorColor(Theme::Token_Background_Default); + color.setAlpha(210); + p.fillRect(that->rect(), color); + }); + widget->attachToWidget(parent); + + using namespace Layouting; + QWidget *wizard = Column { + QtcWidgets::Rectangle { + radius(SpacingTokens::PrimitiveL), + fillBrush(creatorColor(Theme::Token_Background_Muted)), + strokePen(creatorColor(Core::WelcomePageHelpers::cardDefaultStroke)), + Column { + new RecommendationsOptionsPage, + Row { st, closeButton }, + spacing(SpacingTokens::GapVL), + }, + }, + }.emerge(); + + Grid { + GridCell({ Align(Qt::AlignCenter, wizard) }), + }.attachTo(widget); + + QObject::connect(closeButton, &QAbstractButton::clicked, widget, [widget] { + settings().showWizardOnStart.setValue(false); + settings().writeSettings(); + widget->hide(); + }); + + return widget; +} + +QWidget *tfLabel(const QString &text, const TextFormat &format) +{ + auto label = new QLabel(text); + applyTf(label, format); + label->setFixedHeight(label->height() * 1.2); // HACK: many lineHeights are too low + return label; +} + +} // namespace Learning::Internal diff --git a/src/plugins/learning/onboardingwizard.h b/src/plugins/learning/onboardingwizard.h new file mode 100644 index 00000000000..9c5b007a182 --- /dev/null +++ b/src/plugins/learning/onboardingwizard.h @@ -0,0 +1,18 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <QWidget> + +namespace Utils { +class TextFormat; +} + +namespace Learning::Internal { + +QWidget *createOnboardingWizard(QWidget *parent); + +QWidget *tfLabel(const QString &text, const Utils::TextFormat &format); + +} // namespace Learning::Internal diff --git a/src/plugins/learning/overview/blogpostgetbetterqmlcodecompletions.webp b/src/plugins/learning/overview/blogpostgetbetterqmlcodecompletions.webp Binary files differnew file mode 100644 index 00000000000..d8406cd4ba8 --- /dev/null +++ b/src/plugins/learning/overview/blogpostgetbetterqmlcodecompletions.webp diff --git a/src/plugins/learning/overview/blogpostintroducingqtcertificationtestingplatform.webp b/src/plugins/learning/overview/blogpostintroducingqtcertificationtestingplatform.webp Binary files differnew file mode 100644 index 00000000000..3abbe647d65 --- /dev/null +++ b/src/plugins/learning/overview/blogpostintroducingqtcertificationtestingplatform.webp diff --git a/src/plugins/learning/overview/blogpostqtcustomertrainingcourses.webp b/src/plugins/learning/overview/blogpostqtcustomertrainingcourses.webp Binary files differnew file mode 100644 index 00000000000..e8293bc6781 --- /dev/null +++ b/src/plugins/learning/overview/blogpostqtcustomertrainingcourses.webp diff --git a/src/plugins/learning/overview/coursebestpracticesqml.webp b/src/plugins/learning/overview/coursebestpracticesqml.webp Binary files differnew file mode 100644 index 00000000000..3ce4de4ca89 --- /dev/null +++ b/src/plugins/learning/overview/coursebestpracticesqml.webp diff --git a/src/plugins/learning/overview/coursecreatingqtquickapp.webp b/src/plugins/learning/overview/coursecreatingqtquickapp.webp Binary files differnew file mode 100644 index 00000000000..c8c141cbb61 --- /dev/null +++ b/src/plugins/learning/overview/coursecreatingqtquickapp.webp diff --git a/src/plugins/learning/overview/courseexposecpptoqml.webp b/src/plugins/learning/overview/courseexposecpptoqml.webp Binary files differnew file mode 100644 index 00000000000..6198341095f --- /dev/null +++ b/src/plugins/learning/overview/courseexposecpptoqml.webp diff --git a/src/plugins/learning/overview/coursegettingstartedandroid.webp b/src/plugins/learning/overview/coursegettingstartedandroid.webp Binary files differnew file mode 100644 index 00000000000..3d0e1820111 --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedandroid.webp diff --git a/src/plugins/learning/overview/coursegettingstartedboot2qt.webp b/src/plugins/learning/overview/coursegettingstartedboot2qt.webp Binary files differnew file mode 100644 index 00000000000..caa9046280f --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedboot2qt.webp diff --git a/src/plugins/learning/overview/coursegettingstartedcmake.webp b/src/plugins/learning/overview/coursegettingstartedcmake.webp Binary files differnew file mode 100644 index 00000000000..db249947184 --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedcmake.webp diff --git a/src/plugins/learning/overview/coursegettingstartedios.webp b/src/plugins/learning/overview/coursegettingstartedios.webp Binary files differnew file mode 100644 index 00000000000..23b26ec0cc3 --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedios.webp diff --git a/src/plugins/learning/overview/coursegettingstartedqtcreator.webp b/src/plugins/learning/overview/coursegettingstartedqtcreator.webp Binary files differnew file mode 100644 index 00000000000..4744ade1818 --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedqtcreator.webp diff --git a/src/plugins/learning/overview/coursegettingstartedqtformcus.webp b/src/plugins/learning/overview/coursegettingstartedqtformcus.webp Binary files differnew file mode 100644 index 00000000000..2515ea3eca9 --- /dev/null +++ b/src/plugins/learning/overview/coursegettingstartedqtformcus.webp diff --git a/src/plugins/learning/overview/courseintroductionqml.webp b/src/plugins/learning/overview/courseintroductionqml.webp Binary files differnew file mode 100644 index 00000000000..24449b7867c --- /dev/null +++ b/src/plugins/learning/overview/courseintroductionqml.webp diff --git a/src/plugins/learning/overview/courseintroductionqtquick.webp b/src/plugins/learning/overview/courseintroductionqtquick.webp Binary files differnew file mode 100644 index 00000000000..dc917991ed3 --- /dev/null +++ b/src/plugins/learning/overview/courseintroductionqtquick.webp diff --git a/src/plugins/learning/overview/courseintroductionqtquickcontrols.webp b/src/plugins/learning/overview/courseintroductionqtquickcontrols.webp Binary files differnew file mode 100644 index 00000000000..26bda7ca166 --- /dev/null +++ b/src/plugins/learning/overview/courseintroductionqtquickcontrols.webp diff --git a/src/plugins/learning/overview/courseintroductionsugnalsslots.webp b/src/plugins/learning/overview/courseintroductionsugnalsslots.webp Binary files differnew file mode 100644 index 00000000000..f090b273f75 --- /dev/null +++ b/src/plugins/learning/overview/courseintroductionsugnalsslots.webp diff --git a/src/plugins/learning/overview/courseintroductiontoqtwidgets01.webp b/src/plugins/learning/overview/courseintroductiontoqtwidgets01.webp Binary files differnew file mode 100644 index 00000000000..16a293087b9 --- /dev/null +++ b/src/plugins/learning/overview/courseintroductiontoqtwidgets01.webp diff --git a/src/plugins/learning/overview/coursemodelviewqml.webp b/src/plugins/learning/overview/coursemodelviewqml.webp Binary files differnew file mode 100644 index 00000000000..c20602b21c6 --- /dev/null +++ b/src/plugins/learning/overview/coursemodelviewqml.webp diff --git a/src/plugins/learning/overview/coursemodelviewqtquick.webp b/src/plugins/learning/overview/coursemodelviewqtquick.webp Binary files differnew file mode 100644 index 00000000000..d556bb92c21 --- /dev/null +++ b/src/plugins/learning/overview/coursemodelviewqtquick.webp diff --git a/src/plugins/learning/overview/coursemultithreading.webp b/src/plugins/learning/overview/coursemultithreading.webp Binary files differnew file mode 100644 index 00000000000..00ff9ac879c --- /dev/null +++ b/src/plugins/learning/overview/coursemultithreading.webp diff --git a/src/plugins/learning/overview/coursepositionersandlayouts.webp b/src/plugins/learning/overview/coursepositionersandlayouts.webp Binary files differnew file mode 100644 index 00000000000..da3653c548b --- /dev/null +++ b/src/plugins/learning/overview/coursepositionersandlayouts.webp diff --git a/src/plugins/learning/overview/courseqmlfluidelements.webp b/src/plugins/learning/overview/courseqmlfluidelements.webp Binary files differnew file mode 100644 index 00000000000..49992c9a671 --- /dev/null +++ b/src/plugins/learning/overview/courseqmlfluidelements.webp diff --git a/src/plugins/learning/overview/courseqmlintegrationbasics.webp b/src/plugins/learning/overview/courseqmlintegrationbasics.webp Binary files differnew file mode 100644 index 00000000000..d37e230eebe --- /dev/null +++ b/src/plugins/learning/overview/courseqmlintegrationbasics.webp diff --git a/src/plugins/learning/overview/courseqmlintegrationintermediate.webp b/src/plugins/learning/overview/courseqmlintegrationintermediate.webp Binary files differnew file mode 100644 index 00000000000..8779b3395d9 --- /dev/null +++ b/src/plugins/learning/overview/courseqmlintegrationintermediate.webp diff --git a/src/plugins/learning/overview/courseqtdatavisualizationtoqtgraphs.webp b/src/plugins/learning/overview/courseqtdatavisualizationtoqtgraphs.webp Binary files differnew file mode 100644 index 00000000000..556925469ac --- /dev/null +++ b/src/plugins/learning/overview/courseqtdatavisualizationtoqtgraphs.webp diff --git a/src/plugins/learning/overview/courseqtquick3dviewsscenesandnodes.webp b/src/plugins/learning/overview/courseqtquick3dviewsscenesandnodes.webp Binary files differnew file mode 100644 index 00000000000..9eb85ee9fc1 --- /dev/null +++ b/src/plugins/learning/overview/courseqtquick3dviewsscenesandnodes.webp diff --git a/src/plugins/learning/overview/coursetranslations.webp b/src/plugins/learning/overview/coursetranslations.webp Binary files differnew file mode 100644 index 00000000000..a188a2db7da --- /dev/null +++ b/src/plugins/learning/overview/coursetranslations.webp diff --git a/src/plugins/learning/overview/examplecarconfigurator.webp b/src/plugins/learning/overview/examplecarconfigurator.webp Binary files differnew file mode 100644 index 00000000000..b7c1eb691ad --- /dev/null +++ b/src/plugins/learning/overview/examplecarconfigurator.webp diff --git a/src/plugins/learning/overview/exampledice.webp b/src/plugins/learning/overview/exampledice.webp Binary files differnew file mode 100644 index 00000000000..405f8c9370d --- /dev/null +++ b/src/plugins/learning/overview/exampledice.webp diff --git a/src/plugins/learning/overview/exampledocumentviewer.webp b/src/plugins/learning/overview/exampledocumentviewer.webp Binary files differnew file mode 100644 index 00000000000..6c847ba254f --- /dev/null +++ b/src/plugins/learning/overview/exampledocumentviewer.webp diff --git a/src/plugins/learning/overview/examplelightningviewer.webp b/src/plugins/learning/overview/examplelightningviewer.webp Binary files differnew file mode 100644 index 00000000000..32fb4a27b12 --- /dev/null +++ b/src/plugins/learning/overview/examplelightningviewer.webp diff --git a/src/plugins/learning/overview/examplemediaplayer.webp b/src/plugins/learning/overview/examplemediaplayer.webp Binary files differnew file mode 100644 index 00000000000..d075473fa45 --- /dev/null +++ b/src/plugins/learning/overview/examplemediaplayer.webp diff --git a/src/plugins/learning/overview/exampleplanespotter.webp b/src/plugins/learning/overview/exampleplanespotter.webp Binary files differnew file mode 100644 index 00000000000..567824b0af7 --- /dev/null +++ b/src/plugins/learning/overview/exampleplanespotter.webp diff --git a/src/plugins/learning/overview/examplepositioners.webp b/src/plugins/learning/overview/examplepositioners.webp Binary files differnew file mode 100644 index 00000000000..329b893bcb5 --- /dev/null +++ b/src/plugins/learning/overview/examplepositioners.webp diff --git a/src/plugins/learning/overview/examplerestfulapi.webp b/src/plugins/learning/overview/examplerestfulapi.webp Binary files differnew file mode 100644 index 00000000000..802e0419daf --- /dev/null +++ b/src/plugins/learning/overview/examplerestfulapi.webp diff --git a/src/plugins/learning/overview/examplesamegame.webp b/src/plugins/learning/overview/examplesamegame.webp Binary files differnew file mode 100644 index 00000000000..020d94df639 --- /dev/null +++ b/src/plugins/learning/overview/examplesamegame.webp diff --git a/src/plugins/learning/overview/examplesatelliteinfo.webp b/src/plugins/learning/overview/examplesatelliteinfo.webp Binary files differnew file mode 100644 index 00000000000..9f06b1464e3 --- /dev/null +++ b/src/plugins/learning/overview/examplesatelliteinfo.webp diff --git a/src/plugins/learning/overview/examplesgraphgallery.webp b/src/plugins/learning/overview/examplesgraphgallery.webp Binary files differnew file mode 100644 index 00000000000..552cbeeeb40 --- /dev/null +++ b/src/plugins/learning/overview/examplesgraphgallery.webp diff --git a/src/plugins/learning/overview/exampleshadereffects.webp b/src/plugins/learning/overview/exampleshadereffects.webp Binary files differnew file mode 100644 index 00000000000..b3c1b590850 --- /dev/null +++ b/src/plugins/learning/overview/exampleshadereffects.webp diff --git a/src/plugins/learning/overview/examplesrobotarm.webp b/src/plugins/learning/overview/examplesrobotarm.webp Binary files differnew file mode 100644 index 00000000000..fbf151fda22 --- /dev/null +++ b/src/plugins/learning/overview/examplesrobotarm.webp diff --git a/src/plugins/learning/overview/examplestockqt.webp b/src/plugins/learning/overview/examplestockqt.webp Binary files differnew file mode 100644 index 00000000000..729630405a9 --- /dev/null +++ b/src/plugins/learning/overview/examplestockqt.webp diff --git a/src/plugins/learning/overview/examplewindowembedding.webp b/src/plugins/learning/overview/examplewindowembedding.webp Binary files differnew file mode 100644 index 00000000000..e563c45ebea --- /dev/null +++ b/src/plugins/learning/overview/examplewindowembedding.webp diff --git a/src/plugins/learning/overview/recommendations.json b/src/plugins/learning/overview/recommendations.json new file mode 100644 index 00000000000..dd32904d092 --- /dev/null +++ b/src/plugins/learning/overview/recommendations.json @@ -0,0 +1,513 @@ +{ + "items": [ + { + "description": "Testing that your installation is successful by opening an existing example application project.", + "flags": [ + ], + "id": "qthelp://org.qt-project.qtcreator/doc/creator-build-example-application.html", + "name": "Build and run", + "thumbnail": ":/qtsupport/images/icons/tutorialicon.png", + "type": "tutorial" + }, + { + "description": "In this tutorial, you will build your first app with Qt using Qt Quick.\nThis tutorial is for anyone wanting to start their journey with Qt and learn how to build applications with Qt and Qt Quick.", + "flags": [ + "experience_basic" + ], + "id": "3426607", + "id_url": "https://www.qt.io/academy/course-catalog/#creating-a-simple-qt-quick-application-", + "name": "Creating a Simple Qt Quick Application", + "thumbnail": "coursecreatingqtquickapp.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1109611/large/ad97006d-2302-4810-875e-f1ba99d942b5-Creating-Qt-Quick-App.jpg", + "type": "course" + }, + { + "description": "In this course, you will launch the Qt Creator IDE for the first time, go through its basic views, and create a new project that you can use to try out some of the basic functionalities.", + "flags": [ + "experience_basic" + ], + "id": "4266987", + "id_url": "https://www.qt.io/academy/course-catalog/#getting-started-with-qt-creator", + "name": "Getting Started with Qt Creator", + "thumbnail": "coursegettingstartedqtcreator.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1473377/large/f1c4af33-dff5-418d-a7cf-43246fe9155e-Getting-Started-Qt-Creator.png", + "type": "course" + }, + { + "description": "A car model example that demonstrates using Qt Quick 3D cameras, extended scene environment and Qt Quick 3D.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Car Configurator", + "thumbnail": "examplecarconfigurator.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/car-configurator/doc/images/car_configurator_overview.png", + "type": "example" + }, + { + "description": "Developing Qt Quick applications using Qt Quick and Qt Quick Controls.", + "flags": [ + ], + "id": "qthelp://org.qt-project.qtdoc/qtdoc/qtdoc-tutorials-alarms-example.html", + "name": "Getting Started Programming with Qt Quick", + "thumbnail": ":/qtsupport/images/icons/tutorialicon.png", + "type": "tutorial" + }, + { + "description": "This tutorial is a beginner's guide to using the Qt for Android toolchain, including the Qt Creator IDE, to get you started developing apps.", + "flags": [ + "target_android" + ], + "id": "3518738", + "id_url": "https://www.qt.io/academy/course-catalog/#getting-started-with-qt-for-android-", + "name": "Getting Started with Qt for Android", + "thumbnail": "coursegettingstartedandroid.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1125669/large/461cb4b1-7c33-4e85-9abf-09d556a1087c-Thumbnails-Platform-Development-Android.jpg", + "type": "course" + }, + { + "description": "Getting Started with Qt for iOS course covers various themes that prepare you for creating iOS apps with Qt Quick and QML.", + "flags": [ + "target_ios" + ], + "id": "3520889", + "id_url": "https://www.qt.io/academy/course-catalog/#getting-started-with-qt-for-ios-", + "name": "Getting Started with Qt for iOS", + "thumbnail": "coursegettingstartedios.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1126805/large/bf10809c-57ce-4009-9ca8-b09b57937d7b-Thumbnails-Platform-Development-iOS.jpg", + "type": "course" + }, + { + "description": "A dice throwing application using Qt Quick 3D Physics and other Qt Modules.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Dice", + "thumbnail": "exampledice.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/dice/doc/images/dice-screenshot.webp", + "type": "example" + }, + { + "description": "Using basic QML types and learning about basic concepts of Qt Quick.", + "flags": [ + "experience_basic" + ], + "id": "qthelp://org.qt-project.qtcreator/doc/qtcreator-transitions-example.html", + "name": "Qt Quick application", + "thumbnail": ":/qtsupport/images/icons/tutorialicon.png", + "type": "tutorial" + }, + { + "description": "Using Qt Creator to create a small Qt application, Text Finder.", + "flags": [ + "experience_basic" + ], + "id": "qthelp://org.qt-project.qtcreator/doc/creator-writing-program.html", + "name": "Qt Widgets application", + "thumbnail": ":/qtsupport/images/icons/tutorialicon.png", + "type": "tutorial" + }, + { + "description": "Getting Started: Boot to Qt will help you get started with developing for embedded devices with stunning user interfaces using the power of Qt.", + "flags": [ + "target_boot2qt" + ], + "id": "4403920", + "id_url": "https://www.qt.io/academy/course-catalog/#getting-started:-boot-to-qt", + "name": "Getting Started: Boot to Qt", + "thumbnail": "coursegettingstartedboot2qt.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1537506/large/53e303b3-4b7b-4cb5-ae7d-a99b16dd38e4-Course-Intro-Boot-Qt-Updated.png", + "type": "course" + }, + { + "description": "In this course, you'll get an overview of what Qt for MCUs is, and why use it. You will create your first project with Qt for MCUs: a simple heartbeat monitor.", + "flags": [ + "target_qtformcus" + ], + "id": "4422548", + "id_url": "https://www.qt.io/academy/course-catalog/#getting-started:-qt-for-mcus", + "name": "Getting Started: Qt for MCUs", + "thumbnail": "coursegettingstartedqtformcus.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1547729/large/13a52f79-86a8-4aa1-b3da-6adbe1899a7a-Course-Intro-MCUs.png", + "type": "course" + }, + { + "description": "Are you looking to create engaging and dynamic user interfaces for your applications? Introduction to QML will start you on your journey to building beautiful, responsive interfaces.", + "flags": [ + "experience_basic" + ], + "id": "3699024", + "id_url": "https://www.qt.io/academy/course-catalog/#introduction-to-qml", + "name": "Introduction to QML", + "thumbnail": "courseintroductionqml.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1313002/large/c38918c4-51a8-4bcb-b30e-12fc3d6f4563-Course-Intro-QML.png", + "type": "course" + }, + { + "description": "Dive in and begin your journey of learning the foundations of interactive UI creation with Qt Quick. Introduction to Qt Quick is a comprehensive look into application development using QML and JavaScript.", + "flags": [ + "experience_basic" + ], + "id": "3799368", + "id_url": "https://www.qt.io/academy/course-catalog/#introduction-to-qt-quick", + "name": "Introduction to Qt Quick", + "thumbnail": "courseintroductionqtquick.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1312993/large/4290f571-ab42-4720-9f50-c02f56805acf-Course-Intro-QML-1.png", + "type": "course" + }, + { + "description": "A QML implementation of the popular puzzle game by Kuniaki Moribe.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Same Game", + "thumbnail": "examplesamegame.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/samegame/doc/images/qtquick-demo-samegame-med-2.png", + "type": "example" + }, + { + "description": "Dive in and learn the power of Qt Quick Controls. Introduction to Qt Quick Controls is a comprehensive look into application development using the Qt Quick Controls module, an extensive library of UI elements for building applications quickly.", + "flags": [ + "experience_basic" + ], + "id": "3890472", + "id_url": "https://www.qt.io/academy/course-catalog/#introduction-to-qt-quick-controls", + "name": "Introduction to Qt Quick Controls", + "thumbnail": "courseintroductionqtquickcontrols.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1312989/large/8212a3f9-c909-4f41-870b-80e3e455ea4d-Course-Intro-Qt-Quick-Controls.png", + "type": "course" + }, + { + "description": "Dive into the essential best practices for writing robust, maintainable, high-performance QML code. This course will equip you with actionable insights and practical examples to elevate your projects and gain a comprehensive understanding of how to write clean, error-resistant QML and leverage advanced language features effectively.", + "flags": [ + ], + "id": "4192694", + "id_url": "https://www.qt.io/academy/course-catalog/#qml-best-practice", + "name": "QML Best Practice", + "thumbnail": "coursebestpracticesqml.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1438492/large/a510f0da-303e-4321-8b5c-350e8a0572af-Course-BestPractice.png", + "type": "course" + }, + { + "description": "The Plane Spotter example demonstrates the tight integration of location and positioning data types into QML.", + "flags": [ + "experience_advanced" + ], + "id": "qtlocation/examples-manifest.xml", + "name": "Plane Spotter (QML)", + "thumbnail": "exampleplanespotter.webp", + "thumbnailurl": "https://doc.qt.io/qt-6/images/planespotter.png", + "type": "example" + }, + { + "description": "An application with a responsive UI showing lightning strikes on a map in real-time by combining Qt Quick, Qt Location, Qt Positioning and Qt Websockets.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Lightning Viewer", + "thumbnail": "examplelightningviewer.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/lightningviewer/doc/images/lightningviewer.jpg", + "type": "example" + }, + { + "description": "In this course, you will explore the Model View Delegate pattern within QML and the Qt framework. You will gain the skills to handle complex data models and present them effectively within your applications.", + "flags": [ + ], + "id": "4405561", + "id_url": "https://www.qt.io/academy/course-catalog/#model-view-delegate-with-qml", + "name": "Model View Delegate with QML", + "thumbnail": "coursemodelviewqml.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1538253/large/d5d02fdc-33c5-40c8-afa9-fde07c504967-Course-MVD-Update.png", + "type": "course" + }, + { + "description": "Example of how to create a RESTful API QML client.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Qt Quick Demo - RESTful API client", + "thumbnail": "examplerestfulapi.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/colorpaletteclient/doc/images/colorpalette_listing.png", + "type": "example" + }, + { + "description": "Debugging Qt Quick applications in the Qt Creator Debug mode.", + "flags": [ + "experience_basic" + ], + "id": "qthelp://org.qt-project.qtcreator/doc/creator-qml-debugging-example.html", + "name": "Qt Quick debugging", + "thumbnail": ":/qtsupport/images/icons/tutorialicon.png", + "type": "tutorial" + }, + { + "description": "This course is the first part of the Introduction to Qt Widgets series.\nThis course is for anyone interested in learning about the Qt Widgets. To get the most out of this course, you should understand the basic software development concepts.", + "flags": [ + "experience_basic" + ], + "id": "3657828", + "id_url": "https://www.qt.io/academy/course-catalog/#introduction-to-qt-widgets:-part-1", + "name": "Introduction to Qt Widgets: Part 1", + "thumbnail": "courseintroductiontoqtwidgets01.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1340496/large/14aa4deb-e296-4a5d-b0bf-7f5627ae92d8-KDAB-Course-Qt-Widget-Part-01.png", + "type": "course" + }, + { + "description": "A widget example with menus, toolbars and a status bar.", + "flags": [ + "experience_basic" + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Document Viewer", + "thumbnail": "exampledocumentviewer.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/documentviewer/doc/images/documentviewer_open.png", + "type": "example" + }, + { + "description": "In this course, you will gain a comprehensive understanding of features that Qt contains to localize applications. Qt Quick and Qt C++ applications use the same underlying localization system, so it is possible to have translatable strings in QML and C++ source files in the same application.", + "flags": [ + ], + "id": "4237958", + "id_url": "https://www.qt.io/academy/course-catalog/#translations", + "name": "Translations", + "thumbnail": "coursetranslations.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1459038/large/5d90b7d9-4d12-4eda-83f2-038d44642e33-Course-Translations.png", + "type": "course" + }, + { + "description": "In this course, you learn how to access C++ functionality from QML: expose C++ to QML. This is done by registering a C++ class as a QML type.\nThis course is for people with basic knowledge of QML and an understanding of object-oriented programming concepts using C++.", + "flags": [ + ], + "id": "4265149", + "id_url": "https://www.qt.io/academy/course-catalog/#how-to-expose-c++-to-qml", + "name": "How to Expose C++ to QML", + "thumbnail": "courseexposecpptoqml.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1472368/large/9f3d001c-af86-4091-97d5-f3e40e7ff5c2-Expose_C__.png", + "type": "course" + }, + { + "description": "The Satellite Info example shows the available satellites using Sky View, Table View, or RSSI View and the user's current position. It is implemented with Qt Positioning and Qt Quick.", + "flags": [ + ], + "id": "qtpositioning/examples-manifest.xml", + "name": "Satellite Info", + "thumbnail": "examplesatelliteinfo.webp", + "thumbnailurl": "https://doc.qt.io/qt-6/images/skyview_tableview.webp", + "type": "example" + }, + { + "description": "In this tutorial, you will learn what CMake is and how it is used in application development with Qt. If you are a developer interested in building applications using Qt - learn CMakes power tools for building your applications.", + "flags": [ + "experience_advanced" + ], + "id": "4560344", + "id_url": "https://www.qt.io/academy/course-catalog?q=building+wi#building-with-cmake:-getting-started-with-cmake-and-qt", + "name": "Getting Started with CMake and Qt", + "thumbnail": "coursegettingstartedcmake.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1590045/large/08d9adad-eea2-45df-9bdc-9040ddf49241-Building-Cmake-Updated.png", + "type": "course" + }, + { + "description": "In this course, you will learn the basics of signals and slots and how to connect signals to slots. This is key for enabling dynamic behavior and interactivity in Qt applications, allowing, for instance, user input to trigger actions or updates in the interface.", + "flags": [ + "experience_basic" + ], + "id": "3597977", + "id_url": "https://www.qt.io/academy/course-catalog/#introduction-to-signals-and-slots", + "name": "Introduction to Signals and Slots", + "thumbnail": "courseintroductionsugnalsslots.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1159710/large/731584ce-2dbe-4b0b-b537-d7cc33da55bc-Course-Signals-Slots.png", + "type": "course" + }, + { + "description": "In this course, you will gain a comprehensive understanding of the most used QML model types, views and how to interact with them. Models in QML are built using one of the QML model types. There are collections of different model types available, however the most used model types are ListModel, TableModel, and ObjectModel.", + "flags": [ + ], + "id": "4209352", + "id_url": "https://www.qt.io/academy/course-catalog/#model-and-views-in-qt-quick", + "name": "Model and Views in Qt Quick", + "thumbnail": "coursemodelviewqtquick.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1445368/large/4a4c0f51-836a-42b8-91ec-a535dede24a0-QML-ModelViewQuick.png", + "type": "course" + }, + { + "description": "Playing audio and video using Qt Quick.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Media Player", + "thumbnail": "examplemediaplayer.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/mediaplayer/doc/images/mediaplayerapp.png", + "type": "example" + }, + { + "description": "Explore multithreading concepts and basic concurrency within the context of building GUI applications with Qt. We will go through some basic concepts of concurrent programming and how these are implemented within Qt. You will explore these multithreading concepts with Qt within an example application provided.", + "flags": [ + "experience_advanced" + ], + "id": "4104439", + "id_url": "https://www.qt.io/academy/course-catalog?q=multi#let's-get-thready!-multithreading-with-qt", + "name": "Multithreading with Qt", + "thumbnail": "coursemultithreading.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1392277/large/80365fd5-91ec-4344-8c66-db0a0cfc3a86-Qt-Multithreading-Updated.png", + "type": "course" + }, + { + "description": "A configurable stock chart for 100 stocks.", + "flags": [ + "experience_advanced" + ], + "id": "qtdoc/examples-manifest.xml", + "name": "StocQt", + "thumbnail": "examplestockqt.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/stocqt/doc/images/qtquick-demo-stocqt.png", + "type": "example" + }, + { + "description": "In this course, you will explore a range of methods to arrange and size visual items within your applications effectively. You’ll start with basic techniques using anchors and gradually progress to more advanced layout methods, ensuring your user interface works seamlessly across various screen sizes and devices.", + "flags": [ + "experience_advanced" + ], + "id": "4088413", + "id_url": "https://www.qt.io/academy/course-catalog?q=positioners+and+layouts#positioners-and-layouts", + "name": "Positioners and Layouts", + "thumbnail": "coursepositionersandlayouts.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1384888/large/39f2100a-1bf9-4c9a-8462-385cdbb1fb92-Course-Positioners-Layouts.png", + "type": "course" + }, + { + "description": "This is a collection of QML Positioner examples.", + "flags": [ + "experience_advanced" + ], + "id": "qtquick/examples-manifest.xml", + "name": "Qt Quick Examples - Positioners", + "thumbnail": "examplepositioners.webp", + "thumbnailurl": "https://doc.qt.io/qt-6/images/qml-positioners-example.png", + "type": "example" + }, + { + "description": "Explore the power of fluid elements and animation in QML, learning how to create smooth, dynamic, and visually engaging user interfaces. This course will cover various QML animation techniques, including property animations, easing curves, behavior animations, path animations, and advanced state transitions.", + "flags": [ + ], + "id": "4221088", + "id_url": "https://www.qt.io/academy/course-catalog/#qml-fluid-elements-and-animation", + "name": "QML Fluid Elements and Animation", + "thumbnail": "courseqmlfluidelements.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1450720/large/dd4774d5-e854-4162-b400-f371ecf7dd99-Course-FluidElements.png", + "type": "course" + }, + { + "description": "In this course, you will explore integrating C++ with QML to create Qt applications. You will learn how to bridge the gap between the QML front-end and the C++ back-end, enabling you to leverage the strengths of both languages.\n\nThis course was updated in April 2025 to continually improve course content and address feedback from the Qt Community.", + "flags": [ + "experience_basic" + ], + "id": "4339162", + "id_url": "https://www.qt.io/academy/course-catalog/#qml-integration-basics", + "name": "QML Integration Basics", + "thumbnail": "courseqmlintegrationbasics.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1506849/large/454d7b52-6e6a-4a29-a052-ff45239407e5-Course-QML-Integration-Basic.png", + "type": "course" + }, + { + "description": "In this course, you will deepen your understanding of integrating C++ with QML by exploring advanced techniques such as singletons, non-instantiable types, and enumerators. By the end of this course, you will be well-equipped to implement more complex and flexible C++/QML interactions, allowing for a seamless blend of C++ backend functionality with QML front-end designs.", + "flags": [ + "experience_advanced" + ], + "id": "4052985", + "id_url": "https://www.qt.io/academy/course-catalog?q=qml+integration#qml-integration-intermediate", + "name": "QML Integration Intermediate", + "thumbnail": "courseqmlintegrationintermediate.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1364900/large/414d8c6a-7d60-4eb2-b37e-0564266e2578-Course-QML-Integration-Intermediate.png", + "type": "course" + }, + { + "description": "A demonstration of how to embed non-Qt UI elements into Qt applications.", + "flags": [ + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Window Embedding", + "thumbnail": "examplewindowembedding.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/windowembedding/doc/images/macos.webp", + "type": "example" + }, + { + "description": "A Qt Quick example demonstrating the use of shader effects.", + "flags": [ + "experience_advanced" + ], + "id": "qtquick/examples-manifest.xml", + "name": "Qt Quick Examples - Shader Effects", + "thumbnail": "exampleshadereffects.webp", + "thumbnailurl": "https://doc.qt.io/qt-6/images/qml-shadereffects-example.png", + "type": "example" + }, + { + "description": "Learn how to migrate applications from the Qt DataVisualization Module to the Qt Graphs Module. Focusing on the practical steps required for migration, we'll guide you through key changes in CMake configurations, code, and asset handling. By the end of this course, you'll have a solid understanding of how to successfully transition your 3D application and leverage the enhanced features of Qt Graphs.", + "flags": [ + "experience_advanced" + ], + "id": "4192219", + "id_url": "https://www.qt.io/academy/course-catalog?q=datavisualization#qt-datavisualization-to-qt-graphs", + "name": "Qt DataVisualization to Qt Graphs", + "thumbnail": "courseqtdatavisualizationtoqtgraphs.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1437062/large/5dd17236-0704-429a-a9a6-a591ef85e6ae-Migrating-QtGraphs.png", + "type": "course" + }, + { + "description": "Gallery of Bar, Scatter, and Surface graphs.", + "flags": [ + "experience_advanced" + ], + "id": "qtgraphs/examples-manifest.xml", + "name": "Graph Gallery", + "thumbnail": "examplesgraphgallery.webp", + "thumbnailurl": "https://doc.qt.io/qt-6/images/widgetgraphgallery-example.png", + "type": "example" + }, + { + "description": "In this course, You will learn how to get started with Qt Quick 3D and familiarize yourself with some key concepts for creating 3D content.\n\nThis course is for 3D and Technical Artists with some knowledge of real-time rendering looking to apply their skills within the Qt Framework.", + "flags": [ + "experience_advanced" + ], + "id": "3657611", + "id_url": "https://www.qt.io/academy/course-catalog?q=datavisualization#qt-datavisualization-to-qt-graphs", + "name": "Qt Quick 3D: Views, Scenes & Nodes", + "thumbnail": "courseqtquick3dviewsscenesandnodes.webp", + "thumbnailurl": "https://learnupon.s3.eu-west-1.amazonaws.com/courseimages/1313024/large/8c4ef6f9-4734-4bf0-9d5a-8bacc453ad69-Course-Views-Scenes-Nodes.png", + "type": "course" + }, + { + "description": "Demonstrates how to add a C++ backend to a 3D project from Qt Design Studio. This example demonstrates adding a C++ backend to a 3D project created in Qt Design Studio. The example itself consists of an interactive industrial robot arm in a Qt Quick 3D scene. The 2D UI to control the robot arm is implement using Qt Quick Controls.", + "flags": [ + "experience_advanced" + ], + "id": "qtdoc/examples-manifest.xml", + "name": "Robot Arm", + "thumbnail": "examplesrobotarm.webp", + "thumbnailurl": "https://code.qt.io/cgit/qt/qtdoc.git/plain/examples/demos/robotarm/doc/images/robotarm-example.png", + "type": "example" + }, + + { + "id_url": "https://www.qt.io/blog/get-better-qml-code-completions-with-codellama-13b-qml-and-7b-qml-v2.0", + "name": "Get Better QML Code Completions with CodeLlama 13B-QML and 7B-QML v3", + "thumbnail": "blogpostgetbetterqmlcodecompletions.webp", + "thumbnailurl": "https://www.qt.io/hubfs/CodeCompletionRectangularShadowClip-converted.gif", + "type": "blogpost" + }, + { + "id_url": "https://www.qt.io/blog/introducing-qt-certification-testing-platform", + "name": "Introducing the Qt Certification Testing Platform", + "thumbnail": "blogpostintroducingqtcertificationtestingplatform.webp", + "thumbnailurl": "https://www.qt.io/hubfs/Certificate_post_image-1.jpeg", + "type": "blogpost" + }, + { + "id_url": "https://www.qt.io/blog/qt-customer-training-courses-available-online", + "name": "Qt Customer Training Courses Available Online", + "thumbnail": "blogpostqtcustomertrainingcourses.webp", + "thumbnailurl": "https://www.qt.io/hs-fs/hubfs/undefined-Jun-25-2025-05-56-15-4543-AM.png", + "type": "blogpost" + } + ] +} diff --git a/src/plugins/learning/overviewwelcomepage.cpp b/src/plugins/learning/overviewwelcomepage.cpp new file mode 100644 index 00000000000..6053e0fb7d3 --- /dev/null +++ b/src/plugins/learning/overviewwelcomepage.cpp @@ -0,0 +1,828 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "overviewwelcomepage.h" + +#include "learningtr.h" +#include "learningsettings.h" +#include "onboardingwizard.h" + +#include <utils/algorithm.h> +#include <utils/elidinglabel.h> +#include <utils/environment.h> +#include <utils/fileutils.h> +#include <utils/layoutbuilder.h> +#include <utils/overlaywidget.h> +#include <utils/qtcwidgets.h> +#include <utils/stylehelper.h> +#include <utils/utilsicons.h> + +#include <coreplugin/helpmanager.h> +#include <coreplugin/welcomepagehelper.h> + +#include <projectexplorer/projectexplorer.h> + +#include <qtsupport/qtversionmanager.h> + +#include <QDesktopServices> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QLayout> +#include <QLoggingCategory> +#include <QPainter> +#include <QPainterPath> +#include <QPixmapCache> +#include <QRadioButton> +#include <QVariantAnimation> +#include <QXmlStreamReader> + +#ifdef WITH_TESTS +#include <QTest> +#endif // WITH_TESTS + +using namespace Core; +using namespace Utils; +using namespace Utils::StyleHelper; +using namespace QtSupport; + +Q_LOGGING_CATEGORY(qtWelcomeOverviewLog, "qtc.welcomeoverview", QtWarningMsg) + +namespace Learning::Internal { + +const int radiusS = StyleHelper::SpacingTokens::PrimitiveXs; +const int radiusL = StyleHelper::SpacingTokens::PrimitiveS; +constexpr QSize blogThumbSize(450, 192); + +using OverviewItems = QList<class OverviewItem*>; +class OverviewItem : public ListItem +{ +public: + enum ItemType { + Example, + Tutorial, + Course, + Blogpost, + }; + + static QString displayName(OverviewItem::ItemType itemType) + { + switch (itemType) { + case OverviewItem::Example: return Tr::tr("Example"); + case OverviewItem::Tutorial: return Tr::tr("Tutorial"); + case OverviewItem::Course: return Tr::tr("Course"); + case OverviewItem::Blogpost: return Tr::tr("Blog post"); + } + Q_UNREACHABLE_RETURN({}); + } + + static ItemType itemType(const QString &string) + { + if (string == "example") + return OverviewItem::Example; + if (string == "tutorial") + return OverviewItem::Tutorial; + if (string == "course") + return OverviewItem::Course; + if (string == "blogpost") + return OverviewItem::Blogpost; + qCDebug(qtWelcomeOverviewLog) << "Invalid item type: " << string; + return OverviewItem::Example; + }; + + static FilePath jsonFile() + { + const QString path = qtcEnvironmentVariable("QTC_LEARNING_RECOMMENDATIONSDIR", + ":/learning/overview/"); + return FilePath::fromUserInput(path) / "recommendations.json"; + } + + static QList<ListItem *> items(const QSet<ItemType> &types) + { + const Result<QByteArray> json = OverviewItem::jsonFile().fileContents(); + if (!json) { + qCWarning(qtWelcomeOverviewLog).noquote() << json.error() << OverviewItem::jsonFile(); + return {}; + } + qCWarning(qtWelcomeOverviewLog).noquote() << "Reading" << types << "from" << + OverviewItem::jsonFile(); + return itemsFromJson(json->data(), types); + } + + static void openExample(const OverviewItem *item) + { + for (const auto version : qtVersionsWithDocsAndExamples()) { + const Result<ExampleData> data = exampleData(item, version->docsPath(), + version->examplesPath()); + if (data) { + QtVersionManager::openExampleProject(data->project, data->toOpen, data->mainFile, + data->dependencies, data->docUrl); + break; + } + qCDebug(qtWelcomeOverviewLog).noquote() << data.error(); + } + } + + static void handleClicked(const OverviewItem *item) + { + switch (item->type) { + case Blogpost: + case Course: // TODO: switch to courses page and show built-in course details widget + QDesktopServices::openUrl(item->id); + break; + case Example: + openExample(item); + break; + case Tutorial: + HelpManager::showHelpUrl(QUrl::fromUserInput(item->id), + HelpManager::ExternalHelpAlways); + break; + } + } + + static bool validByFlags(const QStringList &userFlags, const QStringList &itemFlags) + { + if (itemFlags.isEmpty() || userFlags.isEmpty()) + return true; + const FlagMap userFlagMap = flagListToMap(userFlags); + FlagMap itemFlagMap = flagListToMap(itemFlags); + for (const QString &flag : itemFlagMap.keys()) { + if (!userFlagMap.contains(flag)) + return false; + const QStringList &userSubFlags = userFlagMap.value(flag); + const QStringList itemSubFlags = itemFlagMap.value(flag); + for (const QString &itemSubFlag : itemSubFlags) { + if (userSubFlags.contains(itemSubFlag)) { + itemFlagMap.remove(flag); + break; + } + } + } + return itemFlagMap.isEmpty(); + } + + ItemType type = Example; + QString id; // Could be some kind of ID, or an Url + QStringList flags; + +private: + struct ExampleData + { + FilePath project; + FilePaths toOpen; + FilePath mainFile; + FilePaths dependencies; + QUrl docUrl; + }; + static QList<ListItem *> itemsFromJson(const QByteArray &json, const QSet<ItemType> &types) + { + QJsonParseError error; + const QJsonObject jsonObj = QJsonDocument::fromJson(json, &error).object(); + if (error.error != QJsonParseError::NoError) + qCDebug(qtWelcomeOverviewLog) << "QJsonParseError:" << error.errorString(); + const QJsonArray overviewItems = jsonObj.value("items").toArray(); + + QList<ListItem *> items; + for (const auto overviewItem : overviewItems) { + const QJsonObject overviewItemObj = overviewItem.toObject(); + const QString itemTypeString = overviewItemObj.value("type").toString(); + const OverviewItem::ItemType type = OverviewItem::itemType(itemTypeString); + if (!types.contains(type)) + continue; + const bool idIsUrl = type == Course || type == Blogpost; + const QString itemId = overviewItemObj.value(QLatin1String(idIsUrl ? "id_url" + : "id")).toString(); + const QString itemName = overviewItemObj.value("name").toString(); + if (type == OverviewItem::Example && !exampleInstalled(itemId, itemName)) { + qCDebug(qtWelcomeOverviewLog) << "Excluding" << itemTypeString << itemName + << "because it is not installed."; + continue; + } + const QStringList itemFlags = overviewItemObj.value("flags").toVariant().toStringList(); + if (!validByFlags(itemFlags)) { + qCDebug(qtWelcomeOverviewLog) << "Excluding" << itemTypeString << itemName + << "due to flags:" << itemFlags; + continue; + } + + auto item = new OverviewItem; + item->id = itemId; + item->name = itemName; + item->type = type; + const FilePath imageUrl = FilePath::fromSettings(overviewItemObj.value("thumbnail")); + const FilePath resolvedImageUrl = + imageUrl.isAbsolutePath() ? imageUrl : jsonFile().parentDir().resolvePath(imageUrl); + item->imageUrl = StyleHelper::dpiSpecificImageFile(resolvedImageUrl.toFSPathString()); + item->description = overviewItemObj.value("description").toString(); + item->flags = itemFlags; + items.append(item); + } + return items; + } + + using FlagMap = QMap<QString, QStringList>; + // "Flags" consist of "key_flag" + static FlagMap flagListToMap(const QStringList &itemFlags) + { + FlagMap result; + for (const QString &flagString : itemFlags) { + const QStringList keyAndFlag = flagString.split("_"); + if (keyAndFlag.count() != 2) + continue; + const QString key = keyAndFlag.first(); + QStringList flags = result.value(key); + flags.append(keyAndFlag.at(1)); + result.insert(key, flags); + } + return result; + } + + static bool validByFlags(const QStringList &itemFlags) + { + return validByFlags(settings().userFlags(), itemFlags); + } + + static QtVersions qtVersionsWithDocsAndExamples() + { + using namespace QtSupport; + const QtVersions versions = QtVersionManager::sortVersions( + QtVersionManager::versions([](const QtVersion *v) { + return v->hasExamples() && v->hasDocs(); + })); + return versions; + } + + static Result<ExampleData> exampleData(const OverviewItem *item, const FilePath &docsPath, + const FilePath &examplePath) + { + const FilePath manifestPath = docsPath / item->id; + const ResultError error(QString::fromLatin1("Could not read \"%1\" from: %2") + .arg(item->name).arg(manifestPath.toUserOutput())); + const Result<QByteArray> xmlContents = manifestPath.fileContents(); + if (!xmlContents) + return error; + + ExampleData result; + + QXmlStreamReader reader(xmlContents->data()); + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: + if (reader.name() == QLatin1String(XML_ELEMENT_EXAMPLE)) { + const QXmlStreamAttributes attributes = reader.attributes(); + if (attributes.value(XML_ELEMENT_NAME) != item->name) { + reader.skipCurrentElement(); + continue; + } + result.docUrl = QUrl::fromUserInput( + attributes.value(XML_ATTRIBUTE_DOCURL).toString()); + const FilePath projectPath = FilePath::fromUserInput( + attributes.value(XML_ATTRIBUTE_PROJECTPATH).toString()); + result.project = examplePath.resolvePath(projectPath); + } else if (reader.name() == QLatin1String(XML_ATTRIBUTE_FILETOOPEN)) { + const QXmlStreamAttributes attributes = reader.attributes(); + const FilePath filePath = FilePath::fromUserInput(reader.readElementText()); + const FilePath absoluteFilePath = examplePath.resolvePath(filePath); + result.toOpen.append(absoluteFilePath); + if (attributes.value(XML_ATTRIBUTE_MAINFILE) == QLatin1String(XML_VALUE_TRUE)) + result.mainFile = absoluteFilePath; + } + break; + default: + break; + } + } + if (result.project.isEmpty()) + return error; + + // HACK: Workaround for QTCREATORBUG-33266 + if (!result.mainFile.isEmpty()) { + result.toOpen.removeAll(result.mainFile); + result.toOpen.prepend(result.mainFile); + result.mainFile.clear(); + } + + return result; + } + + static bool exampleInstalled(const QString &exampleId, const QString &exampleName) + { + if (!QtVersionManager::isLoaded()) + return false; + for (const auto version : qtVersionsWithDocsAndExamples()) { + const FilePath manifestPath = version->docsPath() / exampleId; + if (!manifestPath.exists()) + continue; + const Result<QByteArray> xmlContents = manifestPath.fileContents(); + if (!xmlContents) + continue; + QXmlStreamReader reader(xmlContents->data()); + while (!reader.atEnd()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: + if (reader.name() == QLatin1String(XML_ELEMENT_EXAMPLE)) { + const QXmlStreamAttributes attributes = reader.attributes(); + if (attributes.value(XML_ELEMENT_NAME) == exampleName) + return true; + } + break; + default: + break; + } + } + } + return false; + } + + static constexpr char XML_ELEMENT_EXAMPLE[] = "example"; + static constexpr char XML_ELEMENT_NAME[] = "name"; + static constexpr char XML_ATTRIBUTE_DOCURL[] = "docUrl"; + static constexpr char XML_ATTRIBUTE_FILETOOPEN[] = "fileToOpen"; + static constexpr char XML_ATTRIBUTE_MAINFILE[] = "mainFile"; + static constexpr char XML_ATTRIBUTE_PROJECTPATH[] = "projectPath"; + static constexpr char XML_VALUE_TRUE[] = "true"; +}; + +class BlogButton : public QAbstractButton +{ +public: + BlogButton(const FilePath &mask, QWidget *parent = nullptr) + : QAbstractButton(parent) + { + m_icon = Icon({{mask, Theme::Token_Text_Muted}}, Icon::Tint).pixmap(); + setAttribute(Qt::WA_Hover); + setCursor(Qt::ArrowCursor); + } + +protected: + void paintEvent(QPaintEvent *event) override + { + QWidget::paintEvent(event); + QPainter p(this); + + const QColor bgFill = creatorColor(Theme::Token_Background_Muted); + p.setOpacity(underMouse() ? 1 : 0.6); + drawCardBg(&p, rect(), bgFill, Qt::NoPen, radiusS); + p.setOpacity(1); + + const QSizeF iconSize = m_icon.deviceIndependentSize(); + const QPoint iconPos((width() - iconSize.width()) / 2, + (height() - iconSize.height()) / 2); + p.drawPixmap(iconPos, m_icon); + }; + +private: + QPixmap m_icon; +}; + +class BlogCarousel : public QWidget +{ + Q_OBJECT + +public: + BlogCarousel(QWidget *parent = nullptr) + : QWidget(parent) + { + setAttribute(Qt::WA_Hover); + setCursor(Qt::PointingHandCursor); + setFixedSize(blogThumbSize); + + m_animation.setStartValue(0.0); + m_animation.setEndValue(1.0); + + const int btnS = 32; + const int btnPad = SpacingTokens::PaddingHM; + m_prevBtn = new BlogButton(FilePath::fromUserInput(":/utils/images/prev.png"), this); + m_prevBtn->setGeometry(btnPad, (blogThumbSize.height() - btnS) / 2, btnS, btnS); + m_prevBtn->hide(); + m_prevBtn->setToolTip(Tr::tr("Previous blog post")); + m_nextBtn = new BlogButton(FilePath::fromUserInput(":/utils/images/next.png"), this); + m_nextBtn->setGeometry(blogThumbSize.width() - btnPad - btnS, m_prevBtn->y(), btnS, btnS); + m_nextBtn->hide(); + m_nextBtn->setToolTip(Tr::tr("Next blog post")); + + connect(m_prevBtn, &QAbstractButton::pressed, this, &BlogCarousel::prevPressed); + connect(m_nextBtn, &QAbstractButton::pressed, this, &BlogCarousel::nextPressed); + } + + void setThumbnail(const FilePath &path) + { + m_previousPixmap = m_currentPixmap; + m_currentPixmap = QPixmap(path.toFSPathString()) + .scaled(blogThumbSize * devicePixelRatio(), Qt::KeepAspectRatio, + Qt::SmoothTransformation); + m_currentPixmap.setDevicePixelRatio(devicePixelRatio()); + if (!m_previousPixmap.isNull()) { + m_animation.stop(); + m_animation.start(); + } + update(); + } + +signals: + void prevPressed(); + void nextPressed(); + void thumbnailPressed(); + +protected: + void enterEvent(QEnterEvent *event) override + { + QWidget::enterEvent(event); + if (m_currentPixmap.isNull()) + return; + m_nextBtn->show(); + m_prevBtn->show(); + } + + void leaveEvent(QEvent *event) override + { + QWidget::leaveEvent(event); + m_nextBtn->hide(); + m_prevBtn->hide(); + } + + void mousePressEvent(QMouseEvent *event) override + { + emit thumbnailPressed(); + QWidget::mousePressEvent(event); + } + + void paintEvent(QPaintEvent *event) override + { + QWidget::paintEvent(event); + + QPainter p(this); + QPainterPath clipPath; + clipPath.addRoundedRect(rect(), radiusS, radiusS); + p.setClipPath(clipPath); + p.setRenderHint(QPainter::Antialiasing); + if (!m_previousPixmap.isNull() && m_animation.state() == QAbstractAnimation::Running) { + p.drawPixmap(0, 0, m_previousPixmap); + p.setOpacity(m_animation.currentValue().toReal()); + update(); + } + p.drawPixmap(0, 0, m_currentPixmap); + } + +private: + QPixmap m_currentPixmap; + QPixmap m_previousPixmap; + QVariantAnimation m_animation; + BlogButton *m_prevBtn; + BlogButton *m_nextBtn; +}; + +class BlogWidget : public QWidget +{ +public: + BlogWidget(QWidget *parent = nullptr) + : QWidget(parent) + { + m_carousel = new BlogCarousel; + m_title = new Utils::ElidingLabel; + applyTf(m_title, {.themeColor = Theme::Token_Text_Default, + .uiElement = StyleHelper::UiElementH4}); + + m_pageIndicator = new QtcPageIndicator; + + using namespace Layouting; + Column { + m_carousel, + m_title, + Row { + Space(67), // HACK: compensate Button, to have pageIndicator centered + st, + Column { st, m_pageIndicator, st }, + st, + QtcWidgets::Button { + text(tr("Show All")), + role(QtcButton::LargeTertiary), + onClicked(this, [] { + QDesktopServices::openUrl(QUrl::fromUserInput("https://www.qt.io/blog")); + }), + }, + }, + noMargin, spacing(SpacingTokens::GapVM), + }.attachTo(this); + + setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + + connect(m_carousel, &BlogCarousel::prevPressed, this, + [this]{ setCurrentIndex(m_currentIndex - 1); }); + connect(m_carousel, &BlogCarousel::nextPressed, this, + [this]{ setCurrentIndex(m_currentIndex + 1); }); + connect(m_carousel, &BlogCarousel::thumbnailPressed, this, + [this]{ + if (m_items.isEmpty()) + return; + const auto overviewItem = + dynamic_cast<OverviewItem*>(m_items.at(m_currentIndex)); + QTC_ASSERT(overviewItem, return); + OverviewItem::handleClicked(overviewItem); + }); + + updateItems(); + } + + ~BlogWidget() + { + clearItems(); + } + +private: + void updateItems() + { + clearItems(); + m_items = OverviewItem::items({OverviewItem::Blogpost}); + m_pageIndicator->setPagesCount(m_items.count()); + + setCurrentIndex(0); + } + + void setCurrentIndex(int current) + { + if (m_items.isEmpty()) + return; + m_currentIndex = (m_items.count() + current) % m_items.count(); + m_pageIndicator->setCurrentPage(m_currentIndex); + const auto item = dynamic_cast<OverviewItem*>(m_items.at(m_currentIndex)); + QTC_ASSERT(item, return); + m_carousel->setThumbnail(FilePath::fromUserInput(item->imageUrl)); + m_carousel->setToolTip(item->id); + m_title->setText(item->name); + } + + void clearItems() + { + qDeleteAll(m_items); + m_items.clear(); + } + + QList<ListItem *> m_items; + int m_currentIndex = -1; + BlogCarousel *m_carousel; + QLabel *m_title; + QtcPageIndicator *m_pageIndicator; +}; + +class OverviewItemDelegate : public ListItemDelegate +{ +protected: + void clickAction(const ListItem *item) const override + { + const auto overviewItem = dynamic_cast<const OverviewItem*>(item); + QTC_ASSERT(overviewItem, return); + OverviewItem::handleClicked(overviewItem); + } + + void drawPixmapOverlay(const ListItem *item, QPainter *painter, + [[maybe_unused]] const QStyleOptionViewItem &option, + [[maybe_unused]] const QRect ¤tPixmapRect) const override + { + const auto overviewItem = dynamic_cast<const OverviewItem*>(item); + QTC_ASSERT(overviewItem, return); + const QString badgeText = OverviewItem::displayName(overviewItem->type); + constexpr TextFormat badgeTF + {Theme::Token_Basic_White, UiElement::UiElementLabelSmall}; + const QFont font = badgeTF.font(); + const int textWidth = QFontMetrics(font).horizontalAdvance(badgeText); + const QRectF badgeR(1, 1, SpacingTokens::PaddingHS + textWidth + SpacingTokens::PaddingHS, + SpacingTokens::PaddingVXs + badgeTF.lineHeight() + + SpacingTokens::PaddingVXs); + drawCardBg(painter, badgeR, creatorColor(Theme::Token_Notification_Neutral_Muted), + Qt::NoPen, radiusL); + painter->setFont(font); + painter->setPen(badgeTF.color()); + painter->drawText(badgeR, Qt::AlignCenter, badgeText); + } +}; + +class RecommendationsWidget final : public QWidget +{ +public: + RecommendationsWidget(QWidget *parent = nullptr) + : QWidget(parent) + { + m_view = new GridView; + m_view->setModel(&m_model); + m_view->setItemDelegate(&m_delegate); + m_model.setPixmapFunction(pixmapFromFile); + + using namespace Layouting; + Column { + m_view, + noMargin, + }.attachTo(this); + + updateModel(); + connect(QtVersionManager::instance(), &QtVersionManager::qtVersionsChanged, + this, &RecommendationsWidget::updateModel); + connect(&settings(), &BaseAspect::changed, + this, &RecommendationsWidget::updateModel); + } + +private: + static QPixmap pixmapFromFile(const QString &url) + { + const QString path = FilePath::fromUserInput(url).toFSPathString(); + const qreal dpr = qApp->devicePixelRatio(); + const QString key = QLatin1String(Q_FUNC_INFO) % path % QString::number(dpr); + QPixmap pixmap; + if (QPixmapCache::find(key, &pixmap)) + return pixmap; + pixmap = QPixmap(path).scaled(WelcomePageHelpers::WelcomeThumbnailSize * dpr, + Qt::KeepAspectRatio, Qt::SmoothTransformation); + pixmap.setDevicePixelRatio(dpr); + QPixmapCache::insert(key, pixmap); + return pixmap; + } + + void updateModel() + { + m_model.clear(); + const QList<ListItem *> items = OverviewItem::items( + {OverviewItem::Course, OverviewItem::Example, OverviewItem::Tutorial}); + m_model.appendItems(items); + qCDebug(qtWelcomeOverviewLog) << "Loaded" << m_model.rowCount() << "items. User flags:" + << settings().userFlags(); + } + + ListModel m_model; + GridView *m_view; + OverviewItemDelegate m_delegate; +}; + +class OverviewWelcomePageWidget final : public QWidget +{ +public: + OverviewWelcomePageWidget() = default; + + void showEvent(QShowEvent *event) override + { + if (!m_uiInitialized) { + initializeUi(); + m_uiInitialized = true; + } + QWidget::showEvent(event); + } + +private: + static QWidget *recentProjectsPanel() + { + QWidget *projects = ProjectExplorer::ProjectExplorerPlugin::createRecentProjectsView(); + projects->setMinimumWidth(100); + projects->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Minimum); + + using namespace Layouting; + return QtcWidgets::Rectangle { + radius(radiusL), fillBrush(rectFill()), strokePen(rectStroke()), + customMargins(SpacingTokens::PaddingHXxl, SpacingTokens::PaddingVXl, + rectStroke().width(), SpacingTokens::PaddingVXl), + Column { + tfLabel(Tr::tr("Recent Projects"), titleTf), + projects, + noMargin, spacing(SpacingTokens::GapVM), + }, + }.emerge(); + } + + static QWidget *blogPostsPanel() + { + using namespace Layouting; + return QtcWidgets::Rectangle { + radius(radiusL), fillBrush(rectFill()), strokePen(rectStroke()), + customMargins(SpacingTokens::PaddingHXxl, SpacingTokens::PaddingVXl, + SpacingTokens::PaddingHXxl, SpacingTokens::PaddingVXl), + Column { + tfLabel(Tr::tr("Highlights"), titleTf), + new BlogWidget, + noMargin, spacing(SpacingTokens::GapVM), + }, + }.emerge(); + } + + void initializeUi() + { + auto settingsToolButton = new QPushButton; + settingsToolButton->setIcon(Icons::SETTINGS.icon()); + settingsToolButton->setFlat(true); + settingsToolButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + + using namespace Layouting; + Column { + Row { + recentProjectsPanel(), + blogPostsPanel(), + Space(SpacingTokens::PaddingHXxl), + customMargins(SpacingTokens::PaddingVXxl, 0, 0, 0), + }, + Widget { + Column { + Row { + tfLabel(Tr::tr("Recommended for you"), titleTf), + settingsToolButton, + st, + }, + new RecommendationsWidget, + spacing(SpacingTokens::GapVM), + noMargin, + }, + customMargins(SpacingTokens::PaddingVXxl, 0, 0, 0), + }, + customMargins(0, SpacingTokens::PaddingHXxl, 0, 0), + spacing(SpacingTokens::PaddingVXxl), + }.attachTo(this); + + QWidget *optionsOverlay = createOnboardingWizard(this); + optionsOverlay->setVisible(settings().showWizardOnStart()); + + connect(settingsToolButton, &QAbstractButton::clicked, optionsOverlay, &QWidget::show); + } + + static QBrush rectFill() + { + return Qt::transparent; + } + + static QPen rectStroke() + { + return creatorColor(Core::WelcomePageHelpers::cardDefaultStroke); + } + + static constexpr TextFormat titleTf { + .themeColor = Theme::Token_Text_Muted, + .uiElement = StyleHelper::UiElementH5, + }; + bool m_uiInitialized = false; +}; + +class OverviewWelcomePage final : public IWelcomePage +{ +public: + OverviewWelcomePage() = default; + + QString title() const final { return Tr::tr("Overview"); } + int priority() const final { return 1; } + Id id() const final { return "Overview"; } + QWidget *createWidget() const final { return new OverviewWelcomePageWidget; } +}; + +void setupOverviewWelcomePage(QObject *guard) +{ + auto page = new OverviewWelcomePage; + page->setParent(guard); +} + +#ifdef WITH_TESTS +void LearningTest::testFlagsMatching() +{ + QFETCH(QStringList, userFlags); + QFETCH(QStringList, itemFlags); + QFETCH(bool, isMatch); + + const bool actualMatch = OverviewItem::validByFlags(userFlags, itemFlags); + QCOMPARE(actualMatch, isMatch); +} + +void LearningTest::testFlagsMatching_data() +{ + QTest::addColumn<QStringList>("userFlags"); + QTest::addColumn<QStringList>("itemFlags"); + QTest::addColumn<bool>("isMatch"); + + const QString targetDesktop = QLatin1String(TARGET_PREFIX) + TARGET_DESKTOP; + const QString targetiOS = QLatin1String(TARGET_PREFIX) + TARGET_IOS; + const QString targetAndroid = QLatin1String(TARGET_PREFIX) + TARGET_ANDROID; + const QString expBasic = QLatin1String(EXPERIENCE_PREFIX) + EXPERIENCE_BASIC; + const QString expAdvanced = QLatin1String(EXPERIENCE_PREFIX) + EXPERIENCE_ADVANCED; + + QTest::newRow("no_user_flags") << QStringList() + << QStringList({targetDesktop, expBasic}) + << true; + QTest::newRow("no_item_flags") << QStringList({targetDesktop, expBasic}) + << QStringList() + << true; + QTest::newRow("identical_flags") << QStringList({targetiOS, expBasic}) + << QStringList({targetiOS, expBasic}) + << true; + QTest::newRow("no_user_or_item_flags") << QStringList() + << QStringList() + << true; + QTest::newRow("user_basic_item_advanced") << QStringList({expBasic}) + << QStringList({expAdvanced}) + << false; + QTest::newRow("user_basic_item_both") << QStringList({expBasic}) + << QStringList({expBasic, expAdvanced}) + << true; + QTest::newRow("user_basic_item_undefiend") << QStringList({expBasic}) + << QStringList() + << true; + QTest::newRow("user_undefined_item_ios") << QStringList({expBasic}) + << QStringList({targetiOS}) + << false; +} +#endif // WITH_TESTS + +} // namespace Learning::Internal + +#include "overviewwelcomepage.moc" diff --git a/src/plugins/learning/overviewwelcomepage.h b/src/plugins/learning/overviewwelcomepage.h new file mode 100644 index 00000000000..f85ebe9684d --- /dev/null +++ b/src/plugins/learning/overviewwelcomepage.h @@ -0,0 +1,21 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include <QObject> + +namespace Learning::Internal { + +void setupOverviewWelcomePage(QObject *guard); + +#ifdef WITH_TESTS +class LearningTest final : public QObject { + Q_OBJECT +private slots: + void testFlagsMatching(); + void testFlagsMatching_data(); +}; +#endif // WITH_TESTS + +} // namespace Learning::Internal diff --git a/src/plugins/learning/scripts/.gitignore b/src/plugins/learning/scripts/.gitignore new file mode 100644 index 00000000000..cab7a8223eb --- /dev/null +++ b/src/plugins/learning/scripts/.gitignore @@ -0,0 +1,8 @@ +# This file is used to ignore intermediate files +# ---------------------------------------------------------------------------- + +*.gif +*.png +*.webp +*.jpg +.qtcreator/ diff --git a/src/plugins/learning/scripts/pyproject.toml b/src/plugins/learning/scripts/pyproject.toml new file mode 100644 index 00000000000..82e60a03574 --- /dev/null +++ b/src/plugins/learning/scripts/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "updatethumbnails" + +[tool.pyside6-project] +files = ["updatethumbnails.py"] diff --git a/src/plugins/learning/scripts/updatethumbnails.py b/src/plugins/learning/scripts/updatethumbnails.py new file mode 100644 index 00000000000..4d0ee08ee64 --- /dev/null +++ b/src/plugins/learning/scripts/updatethumbnails.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import json +import os +import shutil +import subprocess +import sys +import urllib.request + +from pathlib import Path + + +def recommendations_root(): + return (Path(__file__).parent / "../overview").resolve() + + +def recommendations_json(): + return recommendations_root() / "recommendations.json" + + +def download_file(url, file_name): + if not os.path.isfile(file_name): + print(f"Fetching {url}") + urllib.request.urlretrieve(url, file_name) + + +def magick(): + magick_tool = shutil.which("magick") + if magick_tool is None: + magick_tool = shutil.which("convert") + if magick_tool is None: + sys.exit("ImageMagick was not found in Path.") + return magick_tool + + +def convert_thumbnail(thumbnail_file_name, url, thumbnail_type): + temp_file_name = Path(thumbnail_file_name).stem + Path(url).suffix + download_file(url, temp_file_name) + + is_blog_pic = thumbnail_type == "blogpost" + is_course_pic = thumbnail_type == "course" + size = ( + # Learning::Internal::blogThumbSize + {"w": 450 * 2, "h": 192 * 2} if is_blog_pic else + # WelcomePageHelpers::WelcomeThumbnailSize + {"w": 214 * 2, "h": 160 * 2} + ) + size_str = f"{size['w']}x{size['h']}" + + command = [magick(), f"{temp_file_name}[0]"] + command.extend([ + # https://imagemagick.org/script/command-line-options.php#filter + "-filter", "Mitchell", + # https://usage.imagemagick.org/resize/#fill + "-resize", f"{size_str}^", + "-gravity", "north", + "-extent", size_str, + ]) + if is_course_pic: + command.extend([ + "+dither", + "-colors", "15", + # https://imagemagick.org/script/webp.php + "-define", "webp:lossless=true", + ]) + else: + command.extend([ + # https://imagemagick.org/script/webp.php + "-define", "webp:use-sharp-yuv=1", + "-define", "webp:method=6", + ]) + command.append(recommendations_root() / thumbnail_file_name) + + print(command) + try: + subprocess.check_call(command) + except subprocess.CalledProcessError: + print(f"Failed to convert to {thumbnail_file_name}.") + + +def main(): + with open(recommendations_json(), encoding="utf-8") as json_data: + data = json.load(json_data) + for item in data["items"]: + if "thumbnailurl" in item: + convert_thumbnail(item["thumbnail"], item["thumbnailurl"], + item["type"]) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/plugins/projectexplorer/selectablefilesmodel.cpp b/src/plugins/projectexplorer/selectablefilesmodel.cpp index 546c3a0fcb1..bb04d9ee4b6 100644 --- a/src/plugins/projectexplorer/selectablefilesmodel.cpp +++ b/src/plugins/projectexplorer/selectablefilesmodel.cpp @@ -18,10 +18,12 @@ #include <QDialogButtonBox> #include <QDir> #include <QGridLayout> +#include <QLabel> #include <QLineEdit> #include <QPushButton> #include <QTreeView> +using namespace Tasking; using namespace Utils; namespace ProjectExplorer { @@ -30,92 +32,38 @@ const char HIDE_FILE_FILTER_DEFAULT[] = "Makefile*; *.o; *.lo; *.la; *.obj; *~; " *.config; *.creator; *.user*; *.includes; *.autosave"; const char SELECT_FILE_FILTER_DEFAULT[] = "*.c; *.cc; *.cpp; *.cp; *.cxx; *.c++; *.h; *.hh; *.hpp; *.hxx;"; +using ResultType = std::shared_ptr<Tree>; + SelectableFilesModel::SelectableFilesModel(QObject *parent) : QAbstractItemModel(parent) { - m_root = new Tree; + m_root.reset(new Tree); } -void SelectableFilesModel::setInitialMarkedFiles(const Utils::FilePaths &files) +void SelectableFilesModel::setInitialMarkedFiles(const FilePaths &files) { m_files = Utils::toSet(files); } -void SelectableFilesFromDirModel::startParsing(const Utils::FilePath &baseDir) -{ - m_watcher.cancel(); - m_watcher.waitForFinished(); - - m_baseDir = baseDir; - // Build a tree in a future - m_rootForFuture = new Tree; - m_rootForFuture->name = baseDir.toUserOutput(); - m_rootForFuture->fullPath = baseDir; - m_rootForFuture->isDir = true; - - m_watcher.setFuture(Utils::asyncRun(&SelectableFilesFromDirModel::run, this)); -} - -void SelectableFilesFromDirModel::run(QPromise<void> &promise) -{ - m_futureCount = 0; - buildTree(m_baseDir, m_rootForFuture, promise, 5); -} - -void SelectableFilesFromDirModel::buildTreeFinished() -{ - beginResetModel(); - delete m_root; - m_root = m_rootForFuture; - m_rootForFuture = nullptr; - m_outOfBaseDirFiles - = Utils::filtered(m_files, [this](const Utils::FilePath &fn) { return !fn.isChildOf(m_baseDir); }); - - endResetModel(); - emit parsingFinished(); -} - -void SelectableFilesFromDirModel::cancel() -{ - m_watcher.cancel(); - m_watcher.waitForFinished(); -} - -SelectableFilesModel::FilterState SelectableFilesModel::filter(Tree *t) -{ - if (t->isDir) - return FilterState::SHOWN; - if (m_files.contains(t->fullPath)) - return FilterState::CHECKED; - - auto matchesTreeName = [t](const Glob &g) { - return g.isMatch(t->name); - }; - - if (Utils::anyOf(m_selectFilesFilter, matchesTreeName)) - return FilterState::CHECKED; - - return Utils::anyOf(m_hideFilesFilter, matchesTreeName) ? FilterState::HIDDEN : FilterState::SHOWN; -} - -void SelectableFilesFromDirModel::buildTree(const Utils::FilePath &baseDir, Tree *tree, - QPromise<void> &promise, int symlinkDepth) +static void buildTree(QPromise<ResultType> &promise, const FilePath &baseDir, + const SelectableFilesModel::FilterData &filterData, Tree *tree, + int symlinkDepth, int &futureCount) { if (symlinkDepth == 0) return; - const QFileInfoList fileInfoList = QDir(baseDir.toUrlishString()).entryInfoList(QDir::Files | - QDir::Dirs | - QDir::NoDotAndDotDot); + const QFileInfoList fileInfoList = QDir(baseDir.toUrlishString()) + .entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); bool allChecked = true; bool allUnchecked = true; for (const QFileInfo &fileInfo : fileInfoList) { - Utils::FilePath fn = Utils::FilePath::fromFileInfo(fileInfo); - if ((m_futureCount % 100) == 0) { - emit parsingProgress(fn); + const FilePath fn = FilePath::fromFileInfo(fileInfo); + if ((futureCount % 100) == 0) { + promise.setProgressRange(0, futureCount); + promise.setProgressValueAndText(futureCount, fn.toUserOutput()); if (promise.isCanceled()) return; } - ++m_futureCount; + ++futureCount; if (fileInfo.isDir()) { if (fileInfo.isSymLink()) { const FilePath target = FilePath::fromString(fileInfo.symLinkTarget()); @@ -127,7 +75,7 @@ void SelectableFilesFromDirModel::buildTree(const Utils::FilePath &baseDir, Tree t->name = fileInfo.fileName(); t->fullPath = fn; t->isDir = true; - buildTree(fn, t, promise, symlinkDepth - fileInfo.isSymLink()); + buildTree(promise, fn, filterData, t, symlinkDepth - fileInfo.isSymLink(), futureCount); allChecked &= t->checked == Qt::Checked; allUnchecked &= t->checked == Qt::Unchecked; tree->childDirectories.append(t); @@ -135,15 +83,15 @@ void SelectableFilesFromDirModel::buildTree(const Utils::FilePath &baseDir, Tree auto t = new Tree; t->parent = tree; t->name = fileInfo.fileName(); - const FilterState state = filter(t); - t->checked = ((m_files.isEmpty() && state == FilterState::CHECKED) - || m_files.contains(fn)) ? Qt::Checked : Qt::Unchecked; + const SelectableFilesModel::FilterState state = SelectableFilesModel::filter(filterData, t); + t->checked = ((filterData.files.isEmpty() && state == SelectableFilesModel::FilterState::CHECKED) + || filterData.files.contains(fn)) ? Qt::Checked : Qt::Unchecked; t->fullPath = fn; t->isDir = false; allChecked &= t->checked == Qt::Checked; allUnchecked &= t->checked == Qt::Unchecked; tree->files.append(t); - if (state != FilterState::HIDDEN) + if (state != SelectableFilesModel::FilterState::HIDDEN) tree->visibleFiles.append(t); } } @@ -157,9 +105,60 @@ void SelectableFilesFromDirModel::buildTree(const Utils::FilePath &baseDir, Tree tree->checked = Qt::PartiallyChecked; } -SelectableFilesModel::~SelectableFilesModel() +static void buildTreeRoot(QPromise<ResultType> &promise, const FilePath &baseDir, + const SelectableFilesModel::FilterData &filterData) { - delete m_root; + int futureCount = 0; + const ResultType root(new Tree); + root->name = baseDir.toUserOutput(); + root->fullPath = baseDir; + root->isDir = true; + buildTree(promise, baseDir, filterData, root.get(), 5, futureCount); + promise.addResult(root); +} + +void SelectableFilesFromDirModel::startParsing(const FilePath &baseDir) +{ + const auto onSetup = [this, baseDir, filterData = filterData()](Async<ResultType> &task) { + task.setConcurrentCallData(buildTreeRoot, baseDir, filterData); + connect(&task, &AsyncBase::progressTextChanged, + this, &SelectableFilesFromDirModel::parsingProgress); + }; + const auto onDone = [this, baseDir](const Async<ResultType> &task) { + beginResetModel(); + m_root = task.result(); + m_outOfBaseDirFiles = Utils::filtered(m_files, [this, baseDir](const FilePath &fn) { + return !fn.isChildOf(baseDir); + }); + endResetModel(); + emit parsingFinished(); + }; + m_taskTreeRunner.start({AsyncTask<ResultType>(onSetup, onDone, CallDone::OnSuccess)}); +} + +void SelectableFilesFromDirModel::cancel() +{ + m_taskTreeRunner.reset(); +} + +SelectableFilesModel::FilterState SelectableFilesModel::filter(const FilterData &filterData, Tree *t) +{ + if (t->isDir) + return FilterState::SHOWN; + if (filterData.files.contains(t->fullPath)) + return FilterState::CHECKED; + + const auto matchesTreeName = [name = t->name](const Glob &g) { return g.isMatch(name); }; + + if (Utils::anyOf(filterData.selectFilesFilter, matchesTreeName)) + return FilterState::CHECKED; + + return Utils::anyOf(filterData.hideFilesFilter, matchesTreeName) ? FilterState::HIDDEN : FilterState::SHOWN; +} + +SelectableFilesModel::FilterState SelectableFilesModel::filter(Tree *t) const +{ + return filter(filterData(), t); } int SelectableFilesModel::columnCount(const QModelIndex &parent) const @@ -179,7 +178,7 @@ int SelectableFilesModel::rowCount(const QModelIndex &parent) const QModelIndex SelectableFilesModel::index(int row, int column, const QModelIndex &parent) const { if (!parent.isValid()) - return createIndex(row, column, m_root); + return createIndex(row, column, m_root.get()); auto parentT = static_cast<Tree *>(parent.internalPointer()); if (row < parentT->childDirectories.size()) return createIndex(row, column, parentT->childDirectories.at(row)); @@ -216,7 +215,7 @@ QVariant SelectableFilesModel::data(const QModelIndex &index, int role) const return t->checked; if (role == Qt::DecorationRole) { if (t->icon.isNull()) - t->icon = Utils::FileIconProvider::icon(t->fullPath); + t->icon = FileIconProvider::icon(t->fullPath); return t->icon; } return {}; @@ -288,14 +287,14 @@ Qt::ItemFlags SelectableFilesModel::flags(const QModelIndex &index) const return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsUserCheckable; } -Utils::FilePaths SelectableFilesModel::selectedPaths() const +FilePaths SelectableFilesModel::selectedPaths() const { - Utils::FilePaths result; - collectPaths(m_root, &result); + FilePaths result; + collectPaths(m_root.get(), &result); return result; } -void SelectableFilesModel::collectPaths(Tree *root, Utils::FilePaths *result) const +void SelectableFilesModel::collectPaths(Tree *root, FilePaths *result) const { if (root->checked == Qt::Unchecked) return; @@ -304,14 +303,14 @@ void SelectableFilesModel::collectPaths(Tree *root, Utils::FilePaths *result) c collectPaths(t, result); } -Utils::FilePaths SelectableFilesModel::selectedFiles() const +FilePaths SelectableFilesModel::selectedFiles() const { - Utils::FilePaths result = Utils::toList(m_outOfBaseDirFiles); - collectFiles(m_root, &result); + FilePaths result = Utils::toList(m_outOfBaseDirFiles); + collectFiles(m_root.get(), &result); return result; } -Utils::FilePaths SelectableFilesModel::preservedFiles() const +FilePaths SelectableFilesModel::preservedFiles() const { return Utils::toList(m_outOfBaseDirFiles); } @@ -321,7 +320,7 @@ bool SelectableFilesModel::hasCheckedFiles() const return m_root->checked != Qt::Unchecked; } -void SelectableFilesModel::collectFiles(Tree *root, Utils::FilePaths *result) const +void SelectableFilesModel::collectFiles(Tree *root, FilePaths *result) const { if (root->checked == Qt::Unchecked) return; @@ -367,12 +366,12 @@ void SelectableFilesModel::applyFilter(const QString &selectFilesfilter, const Q m_hideFilesFilter = filter; if (mustApply) - applyFilter(createIndex(0, 0, m_root)); + applyFilter(createIndex(0, 0, m_root.get())); } void SelectableFilesModel::selectAllFiles() { - selectAllFiles(m_root); + selectAllFiles(m_root.get()); } void SelectableFilesModel::selectAllFiles(Tree *root) @@ -507,27 +506,23 @@ Qt::CheckState SelectableFilesModel::applyFilter(const QModelIndex &idx) // SelectableFilesWidget ////////// -namespace { - enum class SelectableFilesWidgetRows { BaseDirectory, SelectFileFilter, HideFileFilter, ApplyButton, View, Progress, PreservedInformation }; -} // namespace - -SelectableFilesWidget::SelectableFilesWidget(QWidget *parent) : - QWidget(parent), - m_baseDirChooser(new Utils::PathChooser), - m_baseDirLabel(new QLabel), - m_startParsingButton(new QPushButton), - m_selectFilesFilterLabel(new QLabel), - m_selectFilesFilterEdit(new Utils::FancyLineEdit), - m_hideFilesFilterLabel(new QLabel), - m_hideFilesFilterEdit(new Utils::FancyLineEdit), - m_applyFiltersButton(new QPushButton), - m_view(new QTreeView), - m_preservedFilesLabel(new QLabel), - m_progressLabel(new QLabel) +SelectableFilesWidget::SelectableFilesWidget(QWidget *parent) + : QWidget(parent) + , m_baseDirChooser(new PathChooser) + , m_baseDirLabel(new QLabel) + , m_startParsingButton(new QPushButton) + , m_selectFilesFilterLabel(new QLabel) + , m_selectFilesFilterEdit(new FancyLineEdit) + , m_hideFilesFilterLabel(new QLabel) + , m_hideFilesFilterEdit(new FancyLineEdit) + , m_applyFiltersButton(new QPushButton) + , m_view(new QTreeView) + , m_preservedFilesLabel(new QLabel) + , m_progressLabel(new QLabel) { const QString selectFilter = Core::ICore::settings()->value("GenericProject/ShowFileFilter", @@ -547,7 +542,7 @@ SelectableFilesWidget::SelectableFilesWidget(QWidget *parent) : layout->addWidget(m_baseDirChooser->buttonAtIndex(0), static_cast<int>(SelectableFilesWidgetRows::BaseDirectory), 2); layout->addWidget(m_startParsingButton, static_cast<int>(SelectableFilesWidgetRows::BaseDirectory), 3); - connect(m_baseDirChooser, &Utils::PathChooser::validChanged, + connect(m_baseDirChooser, &PathChooser::validChanged, this, &SelectableFilesWidget::baseDirectoryChanged); connect(m_startParsingButton, &QAbstractButton::clicked, this, [this] { startParsing(m_baseDirChooser->filePath()); }); @@ -578,9 +573,9 @@ SelectableFilesWidget::SelectableFilesWidget(QWidget *parent) : layout->addWidget(m_progressLabel, static_cast<int>(SelectableFilesWidgetRows::Progress), 0, 1, 4); } -SelectableFilesWidget::SelectableFilesWidget(const Utils::FilePath &path, - const Utils::FilePaths &files, QWidget *parent) : - SelectableFilesWidget(parent) +SelectableFilesWidget::SelectableFilesWidget(const FilePath &path, const FilePaths &files, + QWidget *parent) + : SelectableFilesWidget(parent) { resetModel(path, files); } @@ -602,14 +597,14 @@ void SelectableFilesWidget::setBaseDirEditable(bool edit) m_startParsingButton->setVisible(edit); } -Utils::FilePaths SelectableFilesWidget::selectedFiles() const +FilePaths SelectableFilesWidget::selectedFiles() const { - return m_model ? m_model->selectedFiles() : Utils::FilePaths(); + return m_model ? m_model->selectedFiles() : FilePaths(); } -Utils::FilePaths SelectableFilesWidget::selectedPaths() const +FilePaths SelectableFilesWidget::selectedPaths() const { - return m_model ? m_model->selectedPaths() : Utils::FilePaths(); + return m_model ? m_model->selectedPaths() : FilePaths(); } bool SelectableFilesWidget::hasFilesSelected() const @@ -617,7 +612,7 @@ bool SelectableFilesWidget::hasFilesSelected() const return m_model ? m_model->hasCheckedFiles() : false; } -void SelectableFilesWidget::resetModel(const Utils::FilePath &path, const Utils::FilePaths &files) +void SelectableFilesWidget::resetModel(const FilePath &path, const FilePaths &files) { m_view->setModel(nullptr); @@ -676,7 +671,7 @@ void SelectableFilesWidget::baseDirectoryChanged(bool validState) m_startParsingButton->setEnabled(validState); } -void SelectableFilesWidget::startParsing(const Utils::FilePath &baseDir) +void SelectableFilesWidget::startParsing(const FilePath &baseDir) { if (!m_model) return; @@ -686,9 +681,9 @@ void SelectableFilesWidget::startParsing(const Utils::FilePath &baseDir) m_model->startParsing(baseDir); } -void SelectableFilesWidget::parsingProgress(const Utils::FilePath &fileName) +void SelectableFilesWidget::parsingProgress(const QString &progress) { - m_progressLabel->setText(Tr::tr("Generating file list...\n\n%1").arg(fileName.toUserOutput())); + m_progressLabel->setText(Tr::tr("Generating file list...\n\n%1").arg(progress)); } void SelectableFilesWidget::parsingFinished() @@ -696,9 +691,9 @@ void SelectableFilesWidget::parsingFinished() if (!m_model) return; - smartExpand(m_model->index(0,0, QModelIndex())); + smartExpand(m_model->index(0, 0, {})); - const Utils::FilePaths preservedFiles = m_model->preservedFiles(); + const FilePaths preservedFiles = m_model->preservedFiles(); m_preservedFilesLabel->setText(Tr::tr("Not showing %n files that are outside of the base directory.\n" "These files are preserved.", nullptr, preservedFiles.count())); @@ -722,11 +717,11 @@ void SelectableFilesWidget::smartExpand(const QModelIndex &idx) // SelectableFilesDialogs ////////// -SelectableFilesDialogEditFiles::SelectableFilesDialogEditFiles(const Utils::FilePath &path, - const Utils::FilePaths &files, - QWidget *parent) : - QDialog(parent), - m_filesWidget(new SelectableFilesWidget(path, files)) +SelectableFilesDialogEditFiles::SelectableFilesDialogEditFiles(const FilePath &path, + const FilePaths &files, + QWidget *parent) + : QDialog(parent) + , m_filesWidget(new SelectableFilesWidget(path, files)) { setWindowTitle(Tr::tr("Edit Files")); @@ -739,28 +734,24 @@ SelectableFilesDialogEditFiles::SelectableFilesDialogEditFiles(const Utils::File auto buttonBox = new QDialogButtonBox(Qt::Horizontal, this); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(buttonBox, &QDialogButtonBox::accepted, - this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, - this, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(buttonBox); } -Utils::FilePaths SelectableFilesDialogEditFiles::selectedFiles() const +FilePaths SelectableFilesDialogEditFiles::selectedFiles() const { return m_filesWidget->selectedFiles(); } - ////////// // SelectableFilesDialogAddDirectory ////////// - -SelectableFilesDialogAddDirectory::SelectableFilesDialogAddDirectory(const Utils::FilePath &path, - const Utils::FilePaths &files, - QWidget *parent) : - SelectableFilesDialogEditFiles(path, files, parent) +SelectableFilesDialogAddDirectory::SelectableFilesDialogAddDirectory(const FilePath &path, + const FilePaths &files, + QWidget *parent) + : SelectableFilesDialogEditFiles(path, files, parent) { setWindowTitle(Tr::tr("Add Existing Directory")); @@ -770,20 +761,10 @@ SelectableFilesDialogAddDirectory::SelectableFilesDialogAddDirectory(const Utils SelectableFilesFromDirModel::SelectableFilesFromDirModel(QObject *parent) : SelectableFilesModel(parent) { - connect(&m_watcher, &QFutureWatcherBase::finished, - this, &SelectableFilesFromDirModel::buildTreeFinished); - connect(this, &SelectableFilesFromDirModel::dataChanged, - this, [this] { emit checkedFilesChanged(); }); + this, &SelectableFilesModel::checkedFilesChanged); connect(this, &SelectableFilesFromDirModel::modelReset, - this, [this] { emit checkedFilesChanged(); }); -} - -SelectableFilesFromDirModel::~SelectableFilesFromDirModel() -{ - cancel(); + this, &SelectableFilesModel::checkedFilesChanged); } } // namespace ProjectExplorer - - diff --git a/src/plugins/projectexplorer/selectablefilesmodel.h b/src/plugins/projectexplorer/selectablefilesmodel.h index b8ce51111ed..19ba46bff04 100644 --- a/src/plugins/projectexplorer/selectablefilesmodel.h +++ b/src/plugins/projectexplorer/selectablefilesmodel.h @@ -5,22 +5,27 @@ #include "projectexplorer_export.h" +#include <solutions/tasking/tasktreerunner.h> + #include <utils/filepath.h> #include <utils/storekey.h> #include <QAbstractItemModel> #include <QDialog> -#include <QFutureWatcher> -#include <QLabel> #include <QRegularExpression> #include <QSet> -#include <QTreeView> namespace Utils { class FancyLineEdit; class PathChooser; } +QT_BEGIN_NAMESPACE +class QLabel; +class QPushButton; +class QTreeView; +QT_END_NAMESPACE + namespace ProjectExplorer { class Tree @@ -66,7 +71,7 @@ public: return false; } - bool operator == (const Glob &other) const + bool operator==(const Glob &other) const { return (mode == other.mode) && (matchString == other.matchString) @@ -80,7 +85,6 @@ class PROJECTEXPLORER_EXPORT SelectableFilesModel : public QAbstractItemModel public: SelectableFilesModel(QObject *parent); - ~SelectableFilesModel() override; void setInitialMarkedFiles(const Utils::FilePaths &files); @@ -104,7 +108,17 @@ public: void selectAllFiles(); enum class FilterState { HIDDEN, SHOWN, CHECKED }; - FilterState filter(Tree *t); + + struct FilterData + { + QSet<Utils::FilePath> files; + QList<Glob> hideFilesFilter; + QList<Glob> selectFilesFilter; + }; + + FilterData filterData() const { return {m_files, m_hideFilesFilter, m_selectFilesFilter}; } + static FilterState filter(const FilterData &filterData, Tree *t); + FilterState filter(Tree *t) const; signals: void checkedFilesChanged(); @@ -123,7 +137,7 @@ private: protected: QSet<Utils::FilePath> m_outOfBaseDirFiles; QSet<Utils::FilePath> m_files; - Tree *m_root = nullptr; + std::shared_ptr<Tree> m_root; private: QList<Glob> m_hideFilesFilter; @@ -136,26 +150,16 @@ class PROJECTEXPLORER_EXPORT SelectableFilesFromDirModel : public SelectableFile public: SelectableFilesFromDirModel(QObject *parent); - ~SelectableFilesFromDirModel() override; void startParsing(const Utils::FilePath &baseDir); void cancel(); signals: void parsingFinished(); - void parsingProgress(const Utils::FilePath &fileName); + void parsingProgress(const QString &progress); private: - void buildTree(const Utils::FilePath &baseDir, Tree *tree, QPromise<void> &promise, - int symlinkDepth); - void run(QPromise<void> &promise); - void buildTreeFinished(); - - // Used in the future thread need to all not used after calling startParsing - Utils::FilePath m_baseDir; - QFutureWatcher<void> m_watcher; - Tree *m_rootForFuture = nullptr; - int m_futureCount = 0; + Tasking::SingleTaskTreeRunner m_taskTreeRunner; }; class PROJECTEXPLORER_EXPORT SelectableFilesWidget : public QWidget @@ -189,7 +193,7 @@ private: void baseDirectoryChanged(bool validState); void startParsing(const Utils::FilePath &baseDir); - void parsingProgress(const Utils::FilePath &fileName); + void parsingProgress(const QString &progress); void parsingFinished(); void smartExpand(const QModelIndex &idx); diff --git a/src/plugins/projectexplorer/simpleprojectwizard.cpp b/src/plugins/projectexplorer/simpleprojectwizard.cpp index 7fe45cd10d4..c775890b6bc 100644 --- a/src/plugins/projectexplorer/simpleprojectwizard.cpp +++ b/src/plugins/projectexplorer/simpleprojectwizard.cpp @@ -28,6 +28,7 @@ #include <QDebug> #include <QDir> #include <QFileInfo> +#include <QLabel> #include <QLineEdit> #include <QPainter> #include <QPixmap> diff --git a/src/plugins/python/pyside.cpp b/src/plugins/python/pyside.cpp index d338623d14d..c279098cba8 100644 --- a/src/plugins/python/pyside.cpp +++ b/src/plugins/python/pyside.cpp @@ -21,7 +21,6 @@ #include <texteditor/textdocument.h> #include <utils/algorithm.h> -#include <utils/async.h> #include <utils/checkablemessagebox.h> #include <utils/infobar.h> #include <utils/mimeconstants.h> @@ -32,11 +31,13 @@ #include <QComboBox> #include <QDesktopServices> #include <QDialogButtonBox> +#include <QPointer> #include <QRegularExpression> -#include <QTextCursor> +#include <QVersionNumber> -using namespace Utils; using namespace ProjectExplorer; +using namespace Tasking; +using namespace Utils; namespace Python::Internal { @@ -46,8 +47,7 @@ void PySideInstaller::checkPySideInstallation(const FilePath &python, TextEditor::TextDocument *document) { document->infoBar()->removeInfo(installPySideInfoBarId); - if (QPointer<QFutureWatcher<bool>> watcher = m_futureWatchers.value(document)) - watcher->cancel(); + m_taskTreeRunner.resetKey(document); if (!python.exists()) return; const QString pySide = usedPySide(document->plainText(), document->mimeType()); @@ -55,22 +55,8 @@ void PySideInstaller::checkPySideInstallation(const FilePath &python, runPySideChecker(python, pySide, document); } -bool PySideInstaller::missingPySideInstallation(const FilePath &pythonPath, - const QString &pySide) -{ - QTC_ASSERT(!pySide.isEmpty(), return false); - static QMap<FilePath, QSet<QString>> pythonWithPyside; - if (pythonWithPyside[pythonPath].contains(pySide)) - return false; - - Process pythonProcess; - pythonProcess.setCommand({pythonPath, {"-c", "import " + pySide}}); - pythonProcess.runBlocking(); - const bool missing = pythonProcess.result() != ProcessResult::FinishedWithSuccess; - if (!missing) - pythonWithPyside[pythonPath].insert(pySide); - return missing; -} +using PythonMap = QMap<FilePath, QSet<QString>>; +Q_GLOBAL_STATIC(PythonMap, s_pythonWithPyside) QString PySideInstaller::usedPySide(const QString &text, const QString &mimeType) { @@ -108,9 +94,9 @@ PySideInstaller::~PySideInstaller() void PySideInstaller::installPySide(const FilePath &python, const QString &pySide, bool quiet) { - QMap<QVersionNumber, Utils::FilePath> availablePySides; + QMap<QVersionNumber, FilePath> availablePySides; - const Utils::QtcSettings *settings = Core::ICore::settings(QSettings::SystemScope); + const QtcSettings *settings = Core::ICore::settings(QSettings::SystemScope); const FilePaths requirementsList = Utils::transform(settings->value("Python/PySideWheelsRequirements").toStringList(), @@ -187,7 +173,7 @@ void PySideInstaller::installPySide(const FilePath &python, const QString &pySid dialogLayout->addWidget(new QLabel(Tr::tr("Select which version to install:"))); QComboBox *pySideSelector = new QComboBox(); pySideSelector->addItem(Tr::tr("Latest PySide from the PyPI")); - for (const Utils::FilePath &version : std::as_const(availablePySides)) { + for (const FilePath &version : std::as_const(availablePySides)) { const FilePath dir = version.parentDir(); const QString text = Tr::tr("PySide %1 Wheel (%2)").arg(dir.fileName(), dir.toUserOutput()); @@ -253,30 +239,26 @@ void PySideInstaller::handleDocumentOpened(Core::IDocument *document) PySideInstaller::instance().checkPySideInstallation(pythonBuildConfig->python(), textDocument); } -void PySideInstaller::runPySideChecker(const FilePath &python, +void PySideInstaller::runPySideChecker(const FilePath &pythonPath, const QString &pySide, TextEditor::TextDocument *document) { - using CheckPySideWatcher = QFutureWatcher<bool>; - - QPointer<CheckPySideWatcher> watcher = new CheckPySideWatcher(); + QTC_ASSERT(!pySide.isEmpty(), return); + if ((*s_pythonWithPyside())[pythonPath].contains(pySide)) + return; - // cancel and delete watcher after a 10 second timeout - QTimer::singleShot(10000, this, [watcher]() { - if (watcher) - watcher->cancel(); - }); - connect(watcher, &CheckPySideWatcher::resultReadyAt, this, - [this, watcher, python, pySide, document = QPointer<TextEditor::TextDocument>(document)] { - if (watcher->result()) - handlePySideMissing(python, pySide, document); - }); - connect(watcher, &CheckPySideWatcher::finished, watcher, &CheckPySideWatcher::deleteLater); - connect(watcher, &CheckPySideWatcher::finished, this, [this, document]{ - m_futureWatchers.remove(document); - }); - watcher->setFuture(Utils::asyncRun(&missingPySideInstallation, python, pySide)); - m_futureWatchers[document] = watcher; + const auto onSetup = [pythonPath, pySide](Process &process) { + process.setCommand({pythonPath, {"-c", "import " + pySide}}); + }; + const auto onDone = [this, pythonPath, pySide, + document = QPointer<TextEditor::TextDocument>(document)](DoneWith result) { + if (result == DoneWith::Success) + (*s_pythonWithPyside())[pythonPath].insert(pySide); + else + handlePySideMissing(pythonPath, pySide, document); + }; + using namespace std::chrono_literals; + m_taskTreeRunner.start(document, {ProcessTask(onSetup, onDone).withTimeout(10s)}); } PySideInstaller &PySideInstaller::instance() diff --git a/src/plugins/python/pyside.h b/src/plugins/python/pyside.h index f6f8f25f1e3..7c8d6c2043a 100644 --- a/src/plugins/python/pyside.h +++ b/src/plugins/python/pyside.h @@ -3,16 +3,12 @@ #pragma once -#include <utils/filepath.h> +#include <solutions/tasking/tasktreerunner.h> -#include <QCoreApplication> -#include <QFutureWatcher> -#include <QPointer> -#include <QTextDocument> +#include <utils/filepath.h> namespace Core { class IDocument; } namespace TextEditor { class TextDocument; } -namespace ProjectExplorer { class RunConfiguration; } namespace Python::Internal { @@ -36,7 +32,7 @@ public slots: void installPySide(const QUrl &url); signals: - void pySideInstalled(const Utils::FilePath &python, const QString &pySide); + void pySideInstalled(const Utils::FilePath &pythonPath, const QString &pySide); private: PySideInstaller(); @@ -50,11 +46,10 @@ private: void runPySideChecker(const Utils::FilePath &python, const QString &pySide, TextEditor::TextDocument *document); - static bool missingPySideInstallation(const Utils::FilePath &python, const QString &pySide); static QString usedPySide(const QString &text, const QString &mimeType); QHash<Utils::FilePath, QList<TextEditor::TextDocument *>> m_infoBarEntries; - QHash<TextEditor::TextDocument *, QPointer<QFutureWatcher<bool>>> m_futureWatchers; + Tasking::MappedTaskTreeRunner<TextEditor::TextDocument *> m_taskTreeRunner; }; } // Python::Internal diff --git a/src/plugins/qmakeprojectmanager/qmakeparsernodes.cpp b/src/plugins/qmakeprojectmanager/qmakeparsernodes.cpp index 95b347ae3bd..9a3b9246082 100644 --- a/src/plugins/qmakeprojectmanager/qmakeparsernodes.cpp +++ b/src/plugins/qmakeprojectmanager/qmakeparsernodes.cpp @@ -47,6 +47,7 @@ using namespace ProjectExplorer; using namespace QmakeProjectManager; using namespace QmakeProjectManager::Internal; using namespace QMakeInternal; +using namespace Tasking; using namespace Utils; namespace QmakeProjectManager { @@ -1151,35 +1152,10 @@ QmakeProFile::QmakeProFile(const FilePath &filePath) : QmakePriFile(filePath) { QmakeProFile::~QmakeProFile() { qDeleteAll(m_extraCompilers); - cleanupFutureWatcher(); + m_taskTreeRunner.reset(); cleanupProFileReaders(); } -void QmakeProFile::cleanupFutureWatcher() -{ - if (!m_parseFutureWatcher) - return; - - m_parseFutureWatcher->disconnect(); - m_parseFutureWatcher->cancel(); - m_parseFutureWatcher->waitForFinished(); - m_parseFutureWatcher->deleteLater(); - m_parseFutureWatcher = nullptr; - m_buildSystem->decrementPendingEvaluateFutures(); -} - -void QmakeProFile::setupFutureWatcher() -{ - QTC_ASSERT(!m_parseFutureWatcher, return); - - m_parseFutureWatcher = new QFutureWatcher<Internal::QmakeEvalResultPtr>; - QObject::connect(m_parseFutureWatcher, &QFutureWatcherBase::finished, [this] { - applyEvaluate(m_parseFutureWatcher->result()); - cleanupFutureWatcher(); - }); - m_buildSystem->incrementPendingEvaluateFutures(); -} - bool QmakeProFile::isParent(QmakeProFile *node) { while ((node = dynamic_cast<QmakeProFile *>(node->parent()))) { @@ -1273,16 +1249,35 @@ void QmakeProFile::scheduleUpdate(QmakeProFile::AsyncUpdateDelay delay) void QmakeProFile::asyncUpdate() { - cleanupFutureWatcher(); - setupFutureWatcher(); setupReader(); if (!includedInExactParse()) m_readerExact->setExact(false); - QmakeEvalInput input = evalInput(); - QFuture<QmakeEvalResultPtr> future = Utils::asyncRun(ProjectExplorerPlugin::sharedThreadPool(), - QThread::LowestPriority, - &QmakeProFile::asyncEvaluate, this, input); - m_parseFutureWatcher->setFuture(future); + + struct EvaluationController + { + EvaluationController(const QPointer<QmakeBuildSystem> &bs) + : m_buildSystem(bs) + { m_buildSystem->incrementPendingEvaluateFutures(); } + ~EvaluationController() + { m_buildSystem->decrementPendingEvaluateFutures(); } + private: + QPointer<QmakeBuildSystem> m_buildSystem; + }; + + const auto onSetup = [input = evalInput()](Async<QmakeEvalResultPtr> &task) { + task.setFutureSynchronizer(nullptr); // Reason: m_readerExact and m_readerCumulative magic. + task.setThreadPool(ProjectExplorerPlugin::sharedThreadPool()); + task.setPriority(QThread::LowestPriority); + task.setConcurrentCallData(&QmakeProFile::evaluate, input); + }; + const auto onDone = [this](const Async<QmakeEvalResultPtr> &task) { + if (task.isResultAvailable()) + applyEvaluate(task.result()); + }; + m_taskTreeRunner.start({ + Storage<EvaluationController>(m_buildSystem), + AsyncTask<QmakeEvalResultPtr>(onSetup, onDone) + }); } bool QmakeProFile::isFileFromWildcard(const QString &filePath) const @@ -1360,8 +1355,10 @@ static bool evaluateOne(const QmakeEvalInput &input, ProFile *pro, return true; } -QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) +void QmakeProFile::evaluate(QPromise<QmakeEvalResultPtr> &promise, const QmakeEvalInput &input) { + if (promise.isCanceled()) + return; QmakeEvalResultPtr result(new QmakeEvalResult); QtSupport::ProFileReader *exactBuildPassReader = nullptr; QtSupport::ProFileReader *cumulativeBuildPassReader = nullptr; @@ -1376,8 +1373,18 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) result->state = QmakeEvalResult::EvalFail; } - if (result->state == QmakeEvalResult::EvalFail) - return result; + const auto cleanup = qScopeGuard([input, exactBuildPassReader, cumulativeBuildPassReader] { + if (exactBuildPassReader && exactBuildPassReader != input.readerExact) + delete exactBuildPassReader; + if (cumulativeBuildPassReader && cumulativeBuildPassReader != input.readerCumulative) + delete cumulativeBuildPassReader; + + }); + + if (result->state == QmakeEvalResult::EvalFail) { + promise.addResult(result); + return; + } result->includedFiles.proFile = pro; result->includedFiles.name = input.projectFilePath; @@ -1396,6 +1403,8 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) result->errors.append(errors); for (const FilePath &subDirName : subDirs) { + if (promise.isCanceled()) + return; auto subDir = new QmakeIncludedPriFile; subDir->proFile = nullptr; subDir->name = subDirName; @@ -1409,11 +1418,15 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) QHash<ProFile *, QList<ProFile *>> includeFiles = input.readerExact->includeFiles(); QList<QmakeIncludedPriFile *> toBuild = {&result->includedFiles}; while (!toBuild.isEmpty()) { + if (promise.isCanceled()) + return; QmakeIncludedPriFile *current = toBuild.takeFirst(); if (!current->proFile) continue; // Don't attempt to map subdirs here const QList<ProFile *> children = includeFiles.value(current->proFile); for (ProFile *child : children) { + if (promise.isCanceled()) + return; const FilePath childName = child->fullName(); auto it = current->children.find(childName); if (it == current->children.end()) { @@ -1428,9 +1441,14 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) } } + if (promise.isCanceled()) + return; + if (result->projectType == ProjectType::SubDirsTemplate) { const FilePaths subDirs = subDirsPaths(input.readerCumulative, input.projectDir, nullptr, nullptr); for (const FilePath &subDirName : subDirs) { + if (promise.isCanceled()) + return; auto it = result->includedFiles.children.find(subDirName); if (it == result->includedFiles.children.end()) { auto subDir = new QmakeIncludedPriFile; @@ -1445,11 +1463,15 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) QHash<ProFile *, QList<ProFile *>> includeFiles = input.readerCumulative->includeFiles(); QList<QmakeIncludedPriFile *> toBuild = {&result->includedFiles}; while (!toBuild.isEmpty()) { + if (promise.isCanceled()) + return; QmakeIncludedPriFile *current = toBuild.takeFirst(); if (!current->proFile) continue; // Don't attempt to map subdirs here const QList<ProFile *> children = includeFiles.value(current->proFile); for (ProFile *child : children) { + if (promise.isCanceled()) + return; const FilePath childName = child->fullName(); auto it = current->children.find(childName); if (it == current->children.end()) { @@ -1464,6 +1486,9 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) toBuild.append(current->children.values()); } + if (promise.isCanceled()) + return; + auto exactReader = exactBuildPassReader ? exactBuildPassReader : input.readerExact; auto cumulativeReader = cumulativeBuildPassReader ? cumulativeBuildPassReader : input.readerCumulative; @@ -1477,9 +1502,13 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) = baseVPaths(cumulativeReader, input.projectDir.path(), input.buildDirectory.path()); for (int i = 0; i < static_cast<int>(FileType::FileTypeSize); ++i) { + if (promise.isCanceled()) + return; const auto type = static_cast<FileType>(i); const QStringList qmakeVariables = varNames(type, exactReader); for (const QString &qmakeVariable : qmakeVariables) { + if (promise.isCanceled()) + return; QHash<ProString, bool> handled; if (result->state == QmakeEvalResult::EvalOk) { const QStringList vPathsExact = fullVPaths( @@ -1509,6 +1538,9 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) input.buildDirectory.path()); extractInstalls(device, proToResult, &result->includedFiles.result, result->installsList); + if (promise.isCanceled()) + return; + if (result->state == QmakeEvalResult::EvalOk) { result->targetInformation = targetInformation(input.readerExact, exactBuildPassReader, input.buildDirectory, input.projectFilePath); @@ -1569,28 +1601,31 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) QList<QmakeIncludedPriFile *> toExtract = {&result->includedFiles}; while (!toExtract.isEmpty()) { + if (promise.isCanceled()) + return; QmakeIncludedPriFile *current = toExtract.takeFirst(); processValues(current->result); toExtract.append(current->children.values()); } } - if (exactBuildPassReader && exactBuildPassReader != input.readerExact) - delete exactBuildPassReader; - if (cumulativeBuildPassReader && cumulativeBuildPassReader != input.readerCumulative) - delete cumulativeBuildPassReader; - QList<QPair<QmakePriFile *, QmakeIncludedPriFile *>> toCompare{{nullptr, &result->includedFiles}}; while (!toCompare.isEmpty()) { + if (promise.isCanceled()) + return; QmakePriFile *pn = toCompare.first().first; QmakeIncludedPriFile *tree = toCompare.first().second; toCompare.pop_front(); // Loop prevention: Make sure that exact same node is not in our parent chain for (QmakeIncludedPriFile *priFile : std::as_const(tree->children)) { + if (promise.isCanceled()) + return; bool loop = input.parentFilePaths.contains(priFile->name); for (const QmakePriFile *n = pn; n && !loop; n = n->parent()) { + if (promise.isCanceled()) + return; if (n->filePath() == priFile->name) loop = true; } @@ -1620,13 +1655,9 @@ QmakeEvalResultPtr QmakeProFile::evaluate(const QmakeEvalInput &input) } } } - - return result; -} - -void QmakeProFile::asyncEvaluate(QPromise<QmakeEvalResultPtr> &promise, QmakeEvalInput input) -{ - promise.addResult(evaluate(input)); + if (promise.isCanceled()) + return; + promise.addResult(result); } bool sortByParserNodes(Node *a, Node *b) diff --git a/src/plugins/qmakeprojectmanager/qmakeparsernodes.h b/src/plugins/qmakeprojectmanager/qmakeparsernodes.h index a00665d17ae..711ad224f45 100644 --- a/src/plugins/qmakeprojectmanager/qmakeparsernodes.h +++ b/src/plugins/qmakeprojectmanager/qmakeparsernodes.h @@ -8,16 +8,21 @@ #include "proparser/profileevaluator.h" #include <coreplugin/idocument.h> + #include <cppeditor/generatedcodemodelsupport.h> + #include <projectexplorer/projectnodes.h> + +#include <solutions/tasking/tasktreerunner.h> + #include <utils/textfileformat.h> -#include <QFutureWatcher> #include <QHash> #include <QLoggingCategory> #include <QMap> #include <QPair> #include <QPointer> +#include <QPromise> #include <QStringList> #include <memory> @@ -94,7 +99,7 @@ namespace Internal { Q_DECLARE_LOGGING_CATEGORY(qmakeNodesLog) class QmakeEvalInput; class QmakeEvalResult; -using QmakeEvalResultPtr = std::shared_ptr<QmakeEvalResult>; // FIXME: Use unique_ptr once we require Qt 6 +using QmakeEvalResultPtr = std::shared_ptr<QmakeEvalResult>; class QmakePriFileEvalResult; } // namespace Internal; @@ -324,20 +329,16 @@ public: bool isFileFromWildcard(const QString &filePath) const; private: - void cleanupFutureWatcher(); - void setupFutureWatcher(); - void setParseInProgress(bool b); void setValidParseRecursive(bool b); void setupReader(); Internal::QmakeEvalInput evalInput() const; - static Internal::QmakeEvalResultPtr evaluate(const Internal::QmakeEvalInput &input); + static void evaluate(QPromise<Internal::QmakeEvalResultPtr> &promise, + const Internal::QmakeEvalInput &input); void applyEvaluate(const Internal::QmakeEvalResultPtr &parseResult); - void asyncEvaluate(QPromise<Internal::QmakeEvalResultPtr> &promise, - Internal::QmakeEvalInput input); void cleanupProFileReaders(); void updateGeneratedFiles(const Utils::FilePath &buildDir); @@ -376,9 +377,9 @@ private: QMap<QString, QStringList> m_wildcardDirectoryContents; // Async stuff - QFutureWatcher<Internal::QmakeEvalResultPtr> *m_parseFutureWatcher = nullptr; QtSupport::ProFileReader *m_readerExact = nullptr; QtSupport::ProFileReader *m_readerCumulative = nullptr; + Tasking::SingleTaskTreeRunner m_taskTreeRunner; }; } // namespace QmakeProjectManager diff --git a/src/plugins/qmljseditor/qmllsclient.cpp b/src/plugins/qmljseditor/qmllsclient.cpp index 6a181451903..a7ebbb45015 100644 --- a/src/plugins/qmljseditor/qmllsclient.cpp +++ b/src/plugins/qmljseditor/qmllsclient.cpp @@ -41,12 +41,6 @@ Q_LOGGING_CATEGORY(qmllsLog, "qtc.qmlls.client", QtWarningMsg); namespace QmlJSEditor { -static QHash<FilePath, QmllsClient *> &qmllsClients() -{ - static QHash<FilePath, QmllsClient *> clients; - return clients; -} - QMap<QString, int> QmllsClient::semanticTokenTypesMap() { QMap<QString, int> result; @@ -209,10 +203,7 @@ QmllsClient::QmllsClient(StdIOClientInterface *interface) setQuickFixAssistProvider(new QmllsQuickFixAssistProvider(this)); } -QmllsClient::~QmllsClient() -{ - qmllsClients().remove(qmllsClients().key(this)); -} +QmllsClient::~QmllsClient() {} void QmllsClient::startImpl() { diff --git a/src/plugins/qmljseditor/qmllsclientsettings.cpp b/src/plugins/qmljseditor/qmllsclientsettings.cpp index 52987435c37..4f74585c9fb 100644 --- a/src/plugins/qmljseditor/qmllsclientsettings.cpp +++ b/src/plugins/qmljseditor/qmllsclientsettings.cpp @@ -483,10 +483,8 @@ QmllsClientSettingsWidget::QmllsClientSettingsWidget( m_ignoreMinimumQmllsVersion->setChecked(settings->m_ignoreMinimumQmllsVersion); m_useQmllsSemanticHighlighting->setChecked(settings->m_useQmllsSemanticHighlighting); - QObject::connect(m_overrideExecutable, &QCheckBox::toggled, m_executable, [this](bool checked) { - m_executable->setEnabled(checked); - }); - + QObject::connect( + m_overrideExecutable, &QCheckBox::toggled, m_executable, &PathChooser::setEnabled); m_executable->setFilePath(settings->m_executable); m_executable->setExpectedKind(Utils::PathChooser::File); m_executable->setEnabled(m_overrideExecutable->isChecked()); diff --git a/src/plugins/qmljseditor/qmllsclientsettings.h b/src/plugins/qmljseditor/qmllsclientsettings.h index 267dba407c6..b7d616b381e 100644 --- a/src/plugins/qmljseditor/qmllsclientsettings.h +++ b/src/plugins/qmljseditor/qmllsclientsettings.h @@ -33,7 +33,6 @@ public: const Utils::FilePath &file) const; ExecutableSelection m_executableSelection = FromQtKit; - bool m_useLatestQmlls = false; bool m_ignoreMinimumQmllsVersion = false; bool m_useQmllsSemanticHighlighting = false; bool m_disableBuiltinCodemodel = false; diff --git a/src/tools/wininterrupt/CMakeLists.txt b/src/tools/wininterrupt/CMakeLists.txt index c7903c3c41a..56ad24ff861 100644 --- a/src/tools/wininterrupt/CMakeLists.txt +++ b/src/tools/wininterrupt/CMakeLists.txt @@ -66,6 +66,7 @@ if (NOT QT_CREATOR_API_DEFINED) CMAKE_GENERATOR "${generator}" CMAKE_GENERATOR_PLATFORM "${arch}" LIST_SEPARATOR | + USES_TERMINAL_INSTALL TRUE CMAKE_ARGS -D${PROJECT_NAME}-MultiBuild=ON -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH_ALT_SEP} |