diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000000000000000000000000000000000..2d6d4493d074f56e6c590af70cd22d059ede58ab --- /dev/null +++ b/.clang-format @@ -0,0 +1,118 @@ +# .clang-format for Qt Creator +# +# This is for clang-format >= 5.0. +# +# The configuration below follows the Qt Creator Coding Rules [1] as closely as +# possible. For documentation of the options, see [2]. +# +# Use ../../tests/manual/clang-format-for-qtc/test.cpp for documenting problems +# or testing changes. +# +# In case you update this configuration please also update the qtcStyle() in src\plugins\clangformat\clangformatutils.cpp +# +# [1] https://doc-snapshots.qt.io/qtcreator-extending/coding-style.html +# [2] https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - forever # avoids { wrapped to next line + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: '^<Q.*' + Priority: 200 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertBraces: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +# Do not add QT_BEGIN_NAMESPACE/QT_END_NAMESPACE as this will indent lines in between. +MacroBlockBegin: "" +MacroBlockEnd: "" +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 88 +PenaltyBreakBeforeFirstCallParameter: 300 +PenaltyBreakComment: 500 +PenaltyBreakFirstLessLess: 400 +PenaltyBreakString: 600 +PenaltyExcessCharacter: 50 +PenaltyReturnTypeOnItsOwnLine: 300 +PointerAlignment: Right +ReflowComments: false +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++17 +TabWidth: 4 +UseTab: Never diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f2b1e1c0d1cabbdf1df01c35b138b5145c641d7..75f7fdcf0f8c1f6a2644a5987cd25221641c58b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,8 @@ qt_add_executable(${PROJECT_NAME} src/importdummy.qml src/main.cpp src/backend.cpp src/backend.h + src/serviceConnector.cpp src/serviceConnector.h + src/projectManager.cpp src/projectManager.h ui/main.qml ui/resources.qrc ) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index e7327790a04a506609bb0bc45a7d638128e7dfc8..151df223e244cefe1594a29331cc2efcf25c20ae 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,10 +1,14 @@ <?xml version="1.0"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.qt.qtdesignviewer" - android:installLocation="auto" android:versionCode="11" android:versionName="1.2"> + android:installLocation="auto" android:versionCode="13" android:versionName="1.2"> <!-- %%INSERT_PERMISSIONS --> <!-- %%INSERT_FEATURES --> - <supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/> - <application android:name="org.qtproject.qt.android.bindings.QtApplication" android:extractNativeLibs="true" android:hardwareAccelerated="true" android:label="Qt Design Viewer" android:requestLegacyExternalStorage="true" android:allowNativeHeapPointerTagging="false"> + <supports-screens android:anyDensity="true" android:largeScreens="true" + android:normalScreens="true" android:smallScreens="true" /> + <application android:name="org.qtproject.qt.android.bindings.QtApplication" + android:extractNativeLibs="true" android:hardwareAccelerated="true" + android:label="Qt Design Viewer" android:requestLegacyExternalStorage="true" + android:allowNativeHeapPointerTagging="false" android:icon="@mipmap/app_icon"> <activity android:name="org.qtproject.qt.android.bindings.QtActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:label="Qt Design Viewer" android:launchMode="singleTop" android:screenOrientation="unspecified" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> diff --git a/src/backend.cpp b/src/backend.cpp index 206cfcacae906065482a68e76329351a27339d09..0fd27c4bdb923cd0cdb3eb7cee457fbd09f47323 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -28,323 +28,58 @@ #if !defined(Q_OS_WASM) -#include <QBuffer> #include <QDesktopServices> -#include <QDirIterator> -#include <QEventLoop> #include <QFileInfo> -#include <QGuiApplication> #include <QJsonArray> -#include <QJsonDocument> #include <QJsonObject> #include <QMessageBox> -#include <QNetworkReply> -#include <QQuickItem> -#include <QRandomGenerator> -#include <QRegularExpression> -#include <QResource> #include <QSettings> #include <QSslSocket> -#include <QTemporaryDir> -#include <QTemporaryFile> -#include <QtCore/private/qandroidextras_p.h> -#include <QtGui/private/qzipreader_p.h> -#define QSTRN QString::number - -Backend::Backend(QObject *parent) : QObject(parent) +Backend::Backend(QObject *parent) + : QObject(parent) { + // This will allow us to open the app with the QR code QDesktopServices::setUrlHandler("qtdesignviewer", this, "registerUser"); +} +void Backend::initialize() +{ const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); - - printLog("Qt Design Viewer"); - printLog("System information:"); - printLog("-- Qt version: " + QString(QT_VERSION_STR)); - printLog("-- OpenSSL support: " + QVariant(QSslSocket::supportsSsl()).toString()); - printLog("-- Screen height: " + QSTRN(screenGeometry.height())); - printLog("-- Screen width: " + QSTRN(screenGeometry.width())); + qDebug() << "Qt Design Viewer"; + qDebug() << "System information:"; + qDebug() << "-- Qt version: " << QT_VERSION_STR; + qDebug() << "-- OpenSSL support: " << QVariant(QSslSocket::supportsSsl()).toString(); + qDebug() << "-- Screen height: " << QString::number(screenGeometry.height()); + qDebug() << "-- Screen width: " << QString::number(screenGeometry.width()); #ifdef QT_DEBUG - QString buildType = "Debug"; + const QString buildType = "Debug"; #else - QString buildType = "Release"; + const QString buildType = "Release"; #endif - m_buildInfo = QCoreApplication::applicationVersion() + - "\nTechnology Preview - " + QString(GIT_VERSION) + - "\nQt " + QString(QT_VERSION_STR) + " - " + buildType + " Build" + - "\nQt Quick Components " + QString(QT_QUICK_COMPONENTS_VERSION) + - "\nOpenSSL support: " + QVariant(QSslSocket::supportsSsl()).toString(); - emit buildInfoChanged(); + const QString buildInfo = QCoreApplication::applicationVersion() + "\nTechnology Preview - " + + QString(GIT_VERSION) + "\nQt " + QString(QT_VERSION_STR) + " - " + + buildType + " Build" + "\nQt Quick Components " + + QString(QT_QUICK_COMPONENTS_VERSION) + "\nOpenSSL support: " + + QVariant(QSslSocket::supportsSsl()).toString(); + emit buildInfoChanged(buildInfo); QSettings settings; m_userHash = settings.value("user/hash").toString(); if (m_userHash.isEmpty()) - printLog("User Hash is not registered. Scan QR code to register."); + qDebug("User Hash is not registered. Scan QR code to register."); else { - printLog("User Hash: " + m_userHash); + qDebug() << "User Hash: " << m_userHash; updateUserProjectList(); } - printLog("Initialization complete"); -} - -void Backend::printLog(const QString &log) -{ - qDebug() << log; - m_logs += log + "\n"; - emit logsChanged(); -} - -void Backend::printWarn(const QString &warn) -{ - printLog("WARN: " + warn); -} - -void Backend::printError(const QString &error, const QString &fileName, int line) -{ - printLog(QString(error) - .prepend("ERROR: ") - .append(" (") - .append(fileName) - .append(":") - .append(QSTRN(line)) - .append(")")); -} - -QString Backend::unpackProject(const QByteArray &project, bool extractZip) -{ - QTemporaryDir tempDir("qmlprojector"); - QString projectLocation = tempDir.path(); - - if (extractZip) { - QDir().mkpath(projectLocation); - QBuffer buffer; - buffer.setData(project); - buffer.open(QIODevice::ReadOnly); - QZipReader reader(&buffer); - reader.extractAll(projectLocation); - } - - printLog("Initial project location: " + projectLocation); - - QDir projectLocationDir(projectLocation); - - // maybe it was not a zip file so try it as resource binary - if (projectLocationDir.isEmpty()) { - if (extractZip) - printLog("File could not be extracted. Trying to open it as a resource file."); - - const uchar *data; - if (m_projectData.size()) { - printLog("Unregistering the previous data from QRC system. Path: " + m_projectPath - + " Size: " + QString::number(m_projectData.size()) + " bytes."); - data = reinterpret_cast<const uchar *>(m_projectData.data()); - if (!QResource::unregisterResource(data, m_projectPath)) { - printErr("Cannot unregister the previous resource data."); - } - } - - m_projectData = project; - data = reinterpret_cast<const uchar *>(m_projectData.data()); - printLog("Registering resource data. Size: " + QString::number(m_projectData.size())); - - const QString resourcePath{"/" + QString::number(QRandomGenerator::global()->generate())}; - m_projectPath = resourcePath; - - if (!QDir(resourcePath).removeRecursively()) { - printLog("Could not remove resource path: " + resourcePath); - } - - if (!QResource::registerResource(data, resourcePath)) { - printErr("Can not load the resource data."); - return ""; - } - - projectLocation = ":" + resourcePath; - } - - return projectLocation; -} - -QString Backend::findFile(const QString &dir, const QString &filter) -{ - QDirIterator it(dir, {filter}, QDir::Files, QDirIterator::Subdirectories); - return it.next(); -} - -void Backend::parseQmlprojectFile(const QString &fileName, - QString *mainFile, - QStringList *importPaths) -{ - /* if filename comes from a resource, then qml need qrc:/ at the mainfile and importPaths. - * But all other c++ call like QFileInfo::exists do not understand that, there we - * need to keep the only ":" character at the beginning of the string - */ - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - printErr("Could not open Qml Project file! " + fileName + ": " + file.errorString()); - return; - } - - const QString text = QString::fromUtf8(file.readAll()); - - const QRegularExpression mainFileRegExp("mainFile:\\s*\"(.*)\""); - const QRegularExpressionMatch mainFileMatch = mainFileRegExp.match(text); - - if (!mainFileMatch.hasMatch()) { - printErr("No main file found in " + fileName); - return; - } - - printLog("Found main file: " + mainFileMatch.captured(1)); - QString basePath = QFileInfo(fileName).path() + "/"; - - *mainFile = basePath + mainFileMatch.captured(1); - if (mainFile->startsWith(QLatin1String(":/"))) - *mainFile = "qrc:" + mainFile->mid(1); - - const QRegularExpression qt6ProjectRegExp("qt6Project:\\s*true"); - const QRegularExpressionMatch qt6ProjectMatch = qt6ProjectRegExp.match(text); - if (!qt6ProjectMatch.hasMatch()) { - printWarn("This is not a Qt6 project.\nQt5 projects might work, but they are not " - "officially supported."); - } - - const QRegularExpression importPathsRegExp("importPaths:\\s*\\[\\s*(.*)\\s*\\]"); - const QRegularExpressionMatch importPathsMatch = importPathsRegExp.match(text); - - if (importPathsMatch.hasMatch()) { - for (const QString &path : importPathsMatch.captured(1).split(",")) { - QString cleanedPath = path.trimmed(); - cleanedPath = basePath + cleanedPath.mid(1, cleanedPath.length() - 2); - if (QFileInfo::exists(cleanedPath)) { - if (cleanedPath.startsWith(QLatin1String(":/"))) - cleanedPath = "qrc:" + cleanedPath.mid(1); - importPaths->append(cleanedPath); - } - } - } -} - -bool Backend::runProject(const QByteArray &projectData, const QString &projectName) -{ - emit popupTextChanged("Unpacking project..."); - emit popupProgressIndeterminateChanged(true); - QEventLoop().processEvents(QEventLoop::AllEvents, 1000); - - const QString projectLocation = unpackProject(projectData); - const QString newProjectName = QString(projectName).remove(".qmlrc").remove("#"); - printLog("Final project location: " + projectLocation); - printLog("Project name: " + projectName); - printLog("New project name: " + newProjectName); - - QString mainQmlFilePath; - QStringList importPaths; - - printLog("Looking for qmlproject file in " + projectLocation); - const QString qmlProjectFile = findFile(projectLocation, newProjectName + "*.qmlproject"); - if (!qmlProjectFile.isEmpty()) { - printLog("Found qmlproject file: " + qmlProjectFile); - parseQmlprojectFile(qmlProjectFile, &mainQmlFilePath, &importPaths); - } else { - printWarn("Not found: \"*.qmlproject\". Looking for main.qml.."); - mainQmlFilePath = findFile(projectLocation, "main.qml"); - - if (mainQmlFilePath.isEmpty()) { - printWarn("Not found: \"main.qml\". Looking for \"" + newProjectName + ".qml\".."); - mainQmlFilePath = findFile(projectLocation, newProjectName + ".qml"); - } - } - - if (mainQmlFilePath.isEmpty()) { - printErr("No \"*.qmlproject\", \"main.qml\" or \"" + newProjectName + ".qml\" found in \"" + projectLocation + "\"."); - return false; - } - - printLog("Found mainQmlFile: " + mainQmlFilePath); - - QUrl mainQmlUrl = QUrl::fromUserInput(mainQmlFilePath); - QFile file(mainQmlUrl.path().prepend(":")); - if (!file.open(QIODevice::ReadOnly)) { - printErr("Could not open mainQmlfile for reading! " + file.fileName() + ": " - + file.errorString()); - return false; - } - - printLog("Looking for qtquickcontrols2File in " + projectLocation); - const QString qtquickcontrols2File = findFile(projectLocation, "qtquickcontrols2.conf"); - - if (!qtquickcontrols2File.isEmpty()) { - printLog("Found qtquickcontrols2File: " + qtquickcontrols2File); - qputenv("QT_QUICK_CONTROLS_CONF", qtquickcontrols2File.toLatin1()); - } - - printLog("Adding import paths"); - for (const QString &importPath : importPaths) { - printLog("-- Import path: " + importPath); - m_qmlEngine.addImportPath(importPath); - } - - QObject::connect(&m_qmlEngine, - &QQmlEngine::warnings, - this, - [&](const QList<QQmlError> &warnings) { - for (const auto &warning : warnings) { - printWarn(warning.toString()); - } - }); - - emit popupTextChanged("Loading project..."); - QEventLoop().processEvents(QEventLoop::AllEvents, 1000); - - printLog("Loading mainQmlUrl: " + mainQmlUrl.toString()); - m_qmlComponent.reset(new QQmlComponent(&m_qmlEngine)); - m_qmlComponent->loadUrl(mainQmlUrl); - - printLog("Waiting for qmlComponent to load"); - while (m_qmlComponent->isLoading()) - QCoreApplication::processEvents(); - - printLog("Checking if m_qmlComponent is ready"); - if (!m_qmlComponent->isReady()) - { - printErr("m_qmlComponent is not ready. Reason: " + m_qmlComponent->errorString()); - return false; - } - printLog("Creating top level object"); - QObject *topLevel = m_qmlComponent->create(); - if (!topLevel && m_qmlComponent->isError()) - { - printErr("Error while creating Qml m_qmlComponent:" + m_qmlComponent->errorString()); - return false; - } - - emit popupTextChanged("Setting up the quickWindow..."); - QEventLoop().processEvents(QEventLoop::AllEvents, 1000); - - printLog("Setting up the quickWindow"); - m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel)); - if (m_quickWindow) { - printLog("Running with incubator controller"); - m_qmlEngine.setIncubationController(m_quickWindow->incubationController()); - } else { - printWarn("Top level object is not a QQuickWindow. Trying QQuickView..."); - - QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel); - if (!contentItem) { - printErr("Top level object cannot be casted to QQuickItem. Aborting."); - return false; - } - - printLog("Initializing QQuickView"); - QQuickView *view = new QQuickView(&m_qmlEngine, nullptr); - m_quickWindow.reset(view); - view->setContent(mainQmlUrl, m_qmlComponent.data(), contentItem); - view->setResizeMode(QQuickView::SizeViewToRootObject); - m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height())); - } - return true; + connect(&m_serviceConnector, &ServiceConnector::downloadProgress, this, &Backend::downloadProgress); + connect(&m_projectManager, + &ProjectManager::projectStateChanged, + this, + &Backend::popupTextChanged); + qDebug("Initialization complete"); } void Backend::showWarning(const QString &message) @@ -353,134 +88,78 @@ void Backend::showWarning(const QString &message) msg.exec(); } -QSharedPointer<QNetworkReply> Backend::fetchResource(const QString &url) -{ - printLog("Fetching resource from " + url); - - QNetworkRequest request(url); - request.setRawHeader("Authorization", "test"); - QSharedPointer<QNetworkReply> reply(m_nam.get(request)); - QObject::connect(reply.data(), - &QNetworkReply::sslErrors, - this, - [&](const QList<QSslError> &errors) { - printErr(errors.first().errorString()); - }); - - QEventLoop loop; - QObject::connect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit); - QObject::connect(reply.data(), - &QNetworkReply::downloadProgress, - this, - [&](qint64 bytesReceived, qint64 bytesTotal) { - float percentage = roundf((float) bytesReceived / (float) bytesTotal * 100); - printLog("Download progress " + QSTRN(percentage) + "% - " - + QSTRN(bytesReceived) + "/" + QSTRN(bytesTotal)); - m_downloadProgress = percentage; - emit downloadProgressChanged(); - }); - loop.exec(); - - if (reply->error() != QNetworkReply::NoError) { - printErr(reply->errorString()); - } else { - printLog("Resource fetched successfully"); - } - - return reply; -} - -void Backend::orientateWindow(Qt::ScreenOrientation orientation) +void Backend::downloadAndRun(const QString &url) { - QQuickItem *contentItem = m_quickWindow->contentItem(); - QQuickItem *childItem{contentItem->childItems().at(0)}; - QQuickItem *grandChildItem{childItem->childItems().at(0)}; - QQuickItem *targetItem = childItem; - - const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); - const QString orientationString = (orientation == Qt::LandscapeOrientation ? "landscape" - : "portrait"); - printLog("Adapting orientation to " + orientationString + " mode"); - printLog("Initial sizing:"); - printLog("-- Screen size: " + QSTRN(screenGeometry.height()) + " x " + QSTRN(screenGeometry.width())); - printLog("-- Quick window size: " + QSTRN(m_quickWindow->height()) + " x " - + QSTRN(m_quickWindow->width())); - printLog("-- Child size: " + QSTRN(targetItem->height()) + " x " + QSTRN(targetItem->width())); - printLog("-- Child pos: " + QSTRN(targetItem->x()) + ", " + QSTRN(targetItem->y())); - printLog("-- Child scale: " + QSTRN(targetItem->scale())); - - printLog("Calculating the new size and scale..."); + updatePopup("Downloading project...", false); + emit popupOpen(); - const QSizeF newContentSize = targetItem->size().scaled(screenGeometry.size().toSizeF(), - Qt::AspectRatioMode::KeepAspectRatio); + qDebug() << "Fetching project from " << url << "..."; + QByteArray project = m_serviceConnector.fetchProject(url); - const qreal newScale = newContentSize.height() / targetItem->size().height(); - const qreal newX = (targetItem->width() - screenGeometry.width()) / -2.0f; - const qreal newY = (targetItem->height() - screenGeometry.height()) / -2.0f; + updatePopup("Unpacking project..."); - targetItem->setScale(newScale); - targetItem->setPosition(QPointF(newX, newY)); + const QString projectPath = m_projectManager.unpackProject(project); - printLog("-- Calculated item height: " + QSTRN(newContentSize.height())); - printLog("-- Calculated item width: " + QSTRN(newContentSize.width())); - printLog("-- Calculated item scale: " + QSTRN(newScale)); - printLog("-- Calculated item pos..: " + QSTRN(newX) + "," + QSTRN(newY)); + updatePopup("Running project..."); + if (!m_projectManager.runProject(projectPath, QFileInfo(url).baseName())) + qCritical("Could not run project"); + else + m_projectManager.showAppWindow(); - printLog("Final Sizing:"); - printLog("-- Child height: " + QSTRN(targetItem->height())); - printLog("-- Child width: " + QSTRN(targetItem->width())); - printLog("-- Child scale: " + QSTRN(targetItem->scale())); - printLog("-- Child pos-x: " + QSTRN(targetItem->x())); - printLog("-- Child pos-y: " + QSTRN(targetItem->y())); + emit popupClose(); } -void Backend::showAppWindow() +void Backend::updatePopup(const QString &text, bool indeterminate) { - QScreen *screen = QGuiApplication::primaryScreen(); - QObject::connect(screen, &QScreen::orientationChanged, this, &Backend::orientateWindow); - orientateWindow(screen->orientation()); - - printLog("Initializing and showing the QML app window"); - // m_quickWindow->showMaximized(); - m_quickWindow->show(); + emit popupTextChanged(text); + emit popupProgressIndeterminateChanged(indeterminate); + QEventLoop().processEvents(QEventLoop::AllEvents, 100); } -void Backend::downloadAndRun(const QString &url) +void Backend::runUserProject(const QString &url) { - emit popupTextChanged("Downloading project..."); - emit popupProgressIndeterminateChanged(false); + updatePopup("Running user project"); emit popupOpen(); - printLog("========================="); - printLog("Fetching a new project..."); - - QString projectUrl = url; - if (projectUrl.startsWith("https://designviewer.qt.io/#")) { - projectUrl = projectUrl.split("#").at(1); - projectUrl.prepend("https://designviewer.qt.io/qmlprojects/"); + qDebug("Checking if project is cached. Getting list of available projects..."); + const QJsonArray projectList = m_serviceConnector.fetchUserProjectList(m_userHash); + const QString projectName = QFileInfo(url).baseName(); + QString projectLastModified; + QString projectId; + QJsonObject projectInfo; + for (auto project : projectList) { + if (projectName == project.toObject().value("appName").toString()) { + projectInfo = project.toObject(); + break; + } } - auto reply = fetchResource(projectUrl); - if (reply->error() != QNetworkReply::NoError) { - printErr("Could not fetch project"); - emit popupClose(); - return; - } + const bool projectCached = m_projectManager.isProjectCached(projectInfo); + + if (!projectCached) { + qDebug("Project is not cached. Downloading..."); + updatePopup("Project is not cached. Downloading...", false); + QByteArray projectData = m_serviceConnector.fetchProject(url); - if (!runProject(reply->readAll(), QFileInfo(url).baseName())) { - printErr("Could not run project"); - emit popupClose(); - return; + updatePopup("Caching user project..."); + m_projectManager.cacheProject(projectData, projectInfo); + } else { + qDebug("Project is cached. Running cached project..."); } + updatePopup("Running cached project..."); + if (!m_projectManager.runCachedProject(projectInfo)) + qCritical("Could not run project"); + else + m_projectManager.showAppWindow(); + emit popupClose(); - showAppWindow(); } void Backend::registerUser(const QUrl &url) { const QString userHash = url.toString().remove("qtdesignviewer://"); - printLog("Registering User Hash: " + userHash); + qDebug() << "Registering User Hash: " << userHash; m_userHash = userHash; QSettings().setValue("user/hash", m_userHash); emit userRegistered(); @@ -489,26 +168,14 @@ void Backend::registerUser(const QUrl &url) void Backend::updateUserProjectList() { - printLog("Fetching available project list for user: " + m_userHash); - auto reply = fetchResource("https://designviewer.qt.io/api/v1/qmlrc/list/" + m_userHash - + "?key=818815"); - if (reply->error() != QNetworkReply::NoError) { - printErr("Could not fetch available project list"); - return; - } - - QJsonArray projectList = QJsonDocument::fromJson(reply->readAll()) - .object() - .value("data") - .toObject() - .value("packages") - .toArray(); + qDebug() << "Fetching available project list for user: " << m_userHash; + QJsonArray projectList = m_serviceConnector.fetchUserProjectList(m_userHash); m_projectList.clear(); - printLog("List of available projects fetched:"); + qDebug("List of available projects fetched:"); for (const auto &project : projectList) { const QString projectName{project.toObject().value("appName").toString()}; - printLog("-- " + projectName); + qDebug() << "--" << projectName; m_projectList << projectName; } emit projectListChanged(); diff --git a/src/backend.h b/src/backend.h index f8defbb802ef059852d0aefa431ba105d11ae713..af0e18176b64d0540cf63cbac7e928052b34f56d 100644 --- a/src/backend.h +++ b/src/backend.h @@ -26,69 +26,48 @@ #ifndef DV_ANDROID_H #define DV_ANDROID_H -#include <QNetworkAccessManager> -#include <QQmlComponent> -#include <QQmlEngine> -#include <QQuickView> -#include <QQuickWindow> -#include <QWidget> - -#define printErr(x) printError(x, __FILE_NAME__, __LINE__) +#include "projectManager.h" +#include "serviceConnector.h" class Backend : public QObject { Q_OBJECT - Q_PROPERTY(QString logs READ logs NOTIFY logsChanged) - Q_PROPERTY(QString buildInfo READ buildInfo NOTIFY buildInfoChanged) - Q_PROPERTY(int downloadProgress READ downloadProgress NOTIFY downloadProgressChanged FINAL) Q_PROPERTY(QStringList projectList READ projectList NOTIFY projectListChanged FINAL) Q_PROPERTY(QString userHash READ userHash FINAL) public: explicit Backend(QObject *parent = nullptr); - QString logs() const { return m_logs; } - QString buildInfo() const { return m_buildInfo; } - int downloadProgress() const { return m_downloadProgress; } + void initialize(); + void setLogs(const QString &logs) + { + m_logs = logs; + qDebug() << "emitting logs changed signal"; + emit logsChanged(m_logs); + } + QStringList projectList() const { return m_projectList; } QString userHash() const { return m_userHash; } private: // UI data QString m_logs; - QString m_buildInfo; - int m_downloadProgress = 0; QStringList m_projectList; - // Qml related members - QQmlEngine m_qmlEngine; - QSharedPointer<QQmlComponent> m_qmlComponent; - QSharedPointer<QQuickWindow> m_quickWindow; - // Other members - QNetworkAccessManager m_nam; - QByteArray m_projectData; - QString m_projectPath; QString m_userHash; + ServiceConnector m_serviceConnector; + ProjectManager m_projectManager; - // member logger functions - void printLog(const QString &message); - void printWarn(const QString &message); - void printError(const QString &message, const QString &fileName, int line); + // member functions void showWarning(const QString &message); - - void showAppWindow(); - - QSharedPointer<QNetworkReply> fetchResource(const QString &url); - QString unpackProject(const QByteArray &project, bool extractZip = false); - QString findFile(const QString &dir, const QString &filter); - void parseQmlprojectFile(const QString &fileName, QString *mainFile, QStringList *importPaths); - bool runProject(const QByteArray &projectData, const QString &projectName); void updateUserProjectList(); + void updatePopup(const QString &text, bool indeterminate = true); signals: - void logsChanged(); - void buildInfoChanged(); - void downloadProgressChanged(); + // UI signals + void logsChanged(QString); + void buildInfoChanged(QString); + void downloadProgress(float); void projectListChanged(); void popupProgressIndeterminateChanged(bool indeterminate); void popupTextChanged(QString text); @@ -97,11 +76,9 @@ signals: void userRegistered(); public slots: + void runUserProject(const QString &url); void downloadAndRun(const QString &url); void registerUser(const QUrl &url); - -private slots: - void orientateWindow(Qt::ScreenOrientation orientation); }; #endif // DV_ANDROID_H diff --git a/src/main.cpp b/src/main.cpp index 00f8f6bc6d77c07bc0f489d1ab89cd3bd0c76935..823dc0ad62108e547a8b0a712d254ffe5922c060 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,27 +23,65 @@ ** ****************************************************************************/ +#include <android/log.h> #include <QApplication> -#include <QDebug> #include <QQmlContext> -#include <QSurfaceFormat> #include "backend.h" +Backend *backend; + +void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + QByteArray localMsg = msg.toLocal8Bit(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + QString logPrefix, logSuffix, applicationLogs; + + switch (type) + { + case QtDebugMsg: + logPrefix = QStringLiteral("Debug: "); + break; + case QtInfoMsg: + logPrefix = QStringLiteral("Info: "); + break; + case QtWarningMsg: + logPrefix = QStringLiteral("Warning: "); + break; + case QtCriticalMsg: + logPrefix = QStringLiteral("Critical: "); + break; + case QtFatalMsg: + logPrefix = QStringLiteral("Fatal: "); + logSuffix = QStringLiteral(" (%1:%2, %3)").arg(file).arg(context.line).arg(function); + break; + } + applicationLogs += logPrefix + localMsg + logSuffix + "\n"; + __android_log_print(ANDROID_LOG_DEBUG, "Qt_Design_Viewer", "%s", qPrintable(applicationLogs)); + + if (backend) + backend->setLogs(applicationLogs); +} + int main(int argc, char *argv[]) { + qInstallMessageHandler(messageHandler); QApplication app(argc, argv); QApplication::setOrganizationName("Qt"); QApplication::setApplicationName(QStringLiteral("Qt Design Viewer")); QApplication::setApplicationVersion(QString("Built on %1 %2").arg(__DATE__, __TIME__)); QQuickView view; - Backend backend; + backend = new Backend(); - view.engine()->rootContext()->setContextProperty("backend", &backend); + view.engine()->rootContext()->setContextProperty("backend", backend); view.setSource(QUrl(QStringLiteral("qrc:/main.qml"))); view.setResizeMode(QQuickView::SizeRootObjectToView); view.showMaximized(); + backend->initialize(); + backend->registerUser(QUrl("17e8907b3b84b8206d45be4f551f4e25")); + return app.exec(); } diff --git a/src/projectManager.cpp b/src/projectManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..37f3654dc9be05b6ca6746ecbe37783fbae4ce6b --- /dev/null +++ b/src/projectManager.cpp @@ -0,0 +1,448 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "projectManager.h" + +#include <QBuffer> +#include <QDir> +#include <QDirIterator> +#include <QEventLoop> +#include <QFile> +#include <QFileInfo> +#include <QGuiApplication> +#include <QJsonDocument> +#include <QQuickItem> +#include <QRandomGenerator> +#include <QRegularExpression> +#include <QResource> +#include <QStandardPaths> +#include <QTemporaryDir> +#include <QTemporaryFile> +#include <QtGui/private/qzipreader_p.h> + +#define QSTRN QString::number + +ProjectManager::ProjectManager(QObject *parent) + : QObject(parent) +{ + qDebug() << "ProjectManager created."; + m_projectCachePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); +} + +QString ProjectManager::unpackProject(const QByteArray &project, bool extractZip) +{ + QTemporaryDir tempDir("qmlprojector"); + QString projectPath = tempDir.path(); + + if (extractZip) { + QDir().mkpath(projectPath); + QBuffer buffer; + buffer.setData(project); + buffer.open(QIODevice::ReadOnly); + QZipReader reader(&buffer); + reader.extractAll(projectPath); + } + + qDebug() << "Initial project location: " << projectPath; + + QDir projectPathDir(projectPath); + + // maybe it was not a zip file so try it as resource binary + if (projectPathDir.isEmpty()) { + if (extractZip) + qDebug("File could not be extracted. Trying to open it as a resource file."); + + const uchar *data; + if (m_projectData.size()) { + qDebug() << "Unregistering the previous data from QRC system. Path: " << m_projectPath + << " Size: " << QString::number(m_projectData.size()) << " bytes."; + data = reinterpret_cast<const uchar *>(m_projectData.data()); + if (!QResource::unregisterResource(data, m_projectPath)) { + qCritical("Cannot unregister the previous resource data."); + } + } + + m_projectData = project; + data = reinterpret_cast<const uchar *>(m_projectData.data()); + qDebug() << "Registering resource data. Size: " << QString::number(m_projectData.size()); + + const QString resourcePath{"/" + QString::number(QRandomGenerator::global()->generate())}; + m_projectPath = resourcePath; + + if (!QDir(resourcePath).removeRecursively()) { + qDebug() << "Could not remove resource path: " << resourcePath; + } + + if (!QResource::registerResource(data, resourcePath)) { + qCritical("Can not load the resource data."); + return ""; + } + + projectPath = ":" + resourcePath; + } + return projectPath; +} + +QString ProjectManager::findFile(const QString &dir, const QString &filter) +{ + QDirIterator it(dir, {filter}, QDir::Files, QDirIterator::Subdirectories); + return it.next(); +} + +void ProjectManager::parseQmlProjectFile(const QString &fileName, + QString *mainFile, + QStringList *importPaths) +{ + /* if filename comes from a resource, then qml need qrc:/ at the mainfile and importPaths. + * But all other c++ call like QFileInfo::exists do not understand that, there we + * need to keep the only ":" character at the beginning of the string + */ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open Qml Project file! " << fileName + ": " << file.errorString(); + return; + } + + const QString text = QString::fromUtf8(file.readAll()); + + const QRegularExpression mainFileRegExp("mainFile:\\s*\"(.*)\""); + const QRegularExpressionMatch mainFileMatch = mainFileRegExp.match(text); + + if (!mainFileMatch.hasMatch()) { + qCritical() << "No main file found in " << fileName; + return; + } + + qDebug() << "Found main file: " << mainFileMatch.captured(1); + QString basePath = QFileInfo(fileName).path() + "/"; + + *mainFile = basePath + mainFileMatch.captured(1); + if (mainFile->startsWith(QLatin1String(":/"))) + *mainFile = "qrc:" + mainFile->mid(1); + + const QRegularExpression qt6ProjectRegExp("qt6Project:\\s*true"); + const QRegularExpressionMatch qt6ProjectMatch = qt6ProjectRegExp.match(text); + if (!qt6ProjectMatch.hasMatch()) { + qWarning("This is not a Qt6 project.\nQt5 projects might work, but they are not " + "officially supported."); + } + + const QRegularExpression importPathsRegExp("importPaths:\\s*\\[\\s*(.*)\\s*\\]"); + const QRegularExpressionMatch importPathsMatch = importPathsRegExp.match(text); + + if (importPathsMatch.hasMatch()) { + for (const QString &path : importPathsMatch.captured(1).split(",")) { + QString cleanedPath = path.trimmed(); + cleanedPath = basePath + cleanedPath.mid(1, cleanedPath.length() - 2); + if (QFileInfo::exists(cleanedPath)) { + if (cleanedPath.startsWith(QLatin1String(":/"))) + cleanedPath = "qrc:" + cleanedPath.mid(1); + importPaths->append(cleanedPath); + } + } + } +} + +bool ProjectManager::runProject(const QString &projectPath, const QString &projectName) +{ + const QString newProjectName = QString(projectName).remove(".qmlrc").remove("#"); + qDebug() << "Project location: " << projectPath; + qDebug() << "Project name: " << projectName; + qDebug() << "New project name: " << newProjectName; + + QString mainQmlFilePath; + QStringList importPaths; + + qDebug() << "Looking for qmlproject file in " << projectPath; + const QString qmlProjectFile = findFile(projectPath, newProjectName + "*.qmlproject"); + if (!qmlProjectFile.isEmpty()) { + qDebug() << "Found qmlproject file: " << qmlProjectFile; + parseQmlProjectFile(qmlProjectFile, &mainQmlFilePath, &importPaths); + } else { + qWarning("Not found: \"*.qmlproject\". Looking for main.qml.."); + mainQmlFilePath = findFile(projectPath, "main.qml"); + + if (mainQmlFilePath.isEmpty()) { + qWarning() << "Not found: \"main.qml\". Looking for \"" << newProjectName << ".qml\".."; + mainQmlFilePath = findFile(projectPath, newProjectName + ".qml"); + } + } + + if (mainQmlFilePath.isEmpty()) { + qCritical() << "No \"*.qmlproject\", \"main.qml\" or \"" << newProjectName + << ".qml\" found in \"" << projectPath << "\"."; + return false; + } + + qDebug() << "Found mainQmlFile: " + mainQmlFilePath; + + QUrl mainQmlUrl = QUrl::fromUserInput(mainQmlFilePath); + QFile file(mainQmlUrl.path()); + // QFile file(mainQmlUrl.path().prepend(":")); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open mainQmlfile for reading! " << file.fileName() << ": " + << file.errorString(); + return false; + } + + qDebug() << "Looking for qtquickcontrols2File in " << projectPath; + const QString qtquickcontrols2File = findFile(projectPath, "qtquickcontrols2.conf"); + + if (!qtquickcontrols2File.isEmpty()) { + qDebug() << "Found qtquickcontrols2File: " << qtquickcontrols2File; + qputenv("QT_QUICK_CONTROLS_CONF", qtquickcontrols2File.toLatin1()); + } + + qDebug("Adding import paths"); + for (const QString &importPath : importPaths) { + qDebug() << "-- Import path: " << importPath; + m_qmlEngine.addImportPath(importPath); + } + + QObject::connect(&m_qmlEngine, + &QQmlEngine::warnings, + this, + [&](const QList<QQmlError> &warnings) { + for (const auto &warning : warnings) { + qWarning() << warning.toString(); + } + }); + + emit projectStateChanged("Loading project..."); + QEventLoop().processEvents(QEventLoop::AllEvents, 1000); + + qDebug() << "Loading mainQmlUrl: " << mainQmlUrl.toString(); + m_qmlComponent.reset(new QQmlComponent(&m_qmlEngine)); + m_qmlComponent->loadUrl(mainQmlUrl); + + qDebug() << "Waiting for qmlComponent to load"; + while (m_qmlComponent->isLoading()) + QCoreApplication::processEvents(); + + qDebug() << "Checking if m_qmlComponent is ready"; + if (!m_qmlComponent->isReady()) { + qCritical() << "m_qmlComponent is not ready. Reason:" << m_qmlComponent->errorString(); + return false; + } + qDebug() << "Creating top level object"; + QObject *topLevel = m_qmlComponent->create(); + if (!topLevel && m_qmlComponent->isError()) { + qCritical() << "Error while creating Qml m_qmlComponent:" << m_qmlComponent->errorString(); + return false; + } + + emit projectStateChanged("Setting up the quickWindow..."); + QEventLoop().processEvents(QEventLoop::AllEvents, 1000); + + qDebug() << "Setting up the quickWindow"; + m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel)); + if (m_quickWindow) { + qDebug() << "Running with incubator controller"; + m_qmlEngine.setIncubationController(m_quickWindow->incubationController()); + } else { + qWarning() << "Top level object is not a QQuickWindow. Trying QQuickView..."; + + QQuickItem *contentItem = qobject_cast<QQuickItem *>(topLevel); + if (!contentItem) { + qCritical() << "Top level object cannot be casted to QQuickItem. Aborting."; + return false; + } + + qDebug() << "Initializing QQuickView"; + QQuickView *view = new QQuickView(&m_qmlEngine, nullptr); + m_quickWindow.reset(view); + view->setContent(mainQmlUrl, m_qmlComponent.data(), contentItem); + view->setResizeMode(QQuickView::SizeViewToRootObject); + m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height())); + } + return true; +} + +bool ProjectManager::cacheProject(const QByteArray &projectData, const QJsonObject &projectInfo) +{ + // sample project info + /* + { + "appName": "Testmcu", + "id": "0sadf8fa9s8df67s8998690a7sdf", + "owner": "", + "passwordHash": "", + "qdsIsEnterprise": true, + "qdsVersion": "Qt Design Studio 4.2.0", + "ttlDays": 31, + "uploadTime": "2023-10-27T13:57:22", + "userHash": "12038740912873462987" + }, + */ + + const QString projectId = projectInfo.value("id").toString(); + + qDebug() << "Caching project " << projectId << " with last modified " + << projectInfo.value("uploadTime"); + const QString cachePath = m_projectCachePath + "/" + projectId; + + // remove old cache + if (QDir(cachePath).exists()) { + qDebug() << "Removing old cache for project " << projectId; + if (!QDir(cachePath).removeRecursively()) { + qCritical() << "Could not remove old cache for project " << projectId; + return false; + } + } + + QDir().mkpath(cachePath); + const QString tempProjectPath = unpackProject(projectData); + + // copy all files from tempProjectPath to cachePath + QDirIterator it(tempProjectPath, QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + const QString filePath = it.filePath(); + const QString relativeFilePath = filePath.mid(tempProjectPath.length()); + const QString newFilePath = cachePath + relativeFilePath; + qDebug() << "Copying " << filePath << " to " << newFilePath; + if (QFileInfo(filePath).isDir()) { + QDir().mkpath(newFilePath); + } else { + QFile::copy(filePath, newFilePath); + } + } + + // write project info to cache + QFile lastModifiedFile(cachePath + "/projectInfo.json"); + if (!lastModifiedFile.open(QIODevice::WriteOnly)) { + qCritical() << "Could not open projectInfo file for writing"; + return false; + } + + lastModifiedFile.write(QJsonDocument(projectInfo).toJson()); + return true; +} + +bool ProjectManager::isProjectCached(const QJsonObject &projectInfo) +{ + const QString projectId = projectInfo.value("id").toString(); + const QString lastModified = projectInfo.value("uploadTime").toString(); + + qDebug() << "Checking if project " << projectId << " is cached"; + const QString cachePath = m_projectCachePath + "/" + projectId; + if (!QDir(cachePath).exists()) { + qDebug() << "Project " << projectId << " is not cached"; + return false; + } + + qDebug() << "Project " << projectId << " is cached. Checking if it is up to date"; + QFile lastModifiedFile(cachePath + "/projectInfo.json"); + if (!lastModifiedFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open projectInfo file for reading"; + return false; + } + + const QJsonObject cachedProjectInfo + = QJsonDocument::fromJson(lastModifiedFile.readAll()).object(); + const QString cachedLastModified = cachedProjectInfo.value("uploadTime").toString(); + + qDebug() << "Project " << projectId << " last modified: " << lastModified; + qDebug() << "Cached project " << projectId << " last modified: " << cachedLastModified; + + if (lastModified != cachedLastModified) { + qDebug() << "Project " << projectId << " is not up to date"; + return false; + } + + qDebug() << "Project " << projectId << " is up to date"; + return true; +} + +bool ProjectManager::runCachedProject(const QJsonObject &projectInfo) +{ + const QString projectId = projectInfo.value("id").toString(); + const QString projectName = projectInfo.value("appName").toString(); + qDebug() << "Running cached project " << projectId; + + const QString projectPath = m_projectCachePath + "/" + projectId; + return runProject(projectPath, projectName); +} + +void ProjectManager::orientateWindow(Qt::ScreenOrientation orientation) +{ + QQuickItem *contentItem = m_quickWindow->contentItem(); + QQuickItem *childItem{contentItem->childItems().at(0)}; + QQuickItem *targetItem = childItem; + + const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); + const QString orientationString = (orientation == Qt::LandscapeOrientation ? "landscape" + : "portrait"); + qDebug() << "Adapting orientation to " << orientationString << " mode"; + qDebug() << "Initial sizing:"; + qDebug() << "-- Screen size: " << QSTRN(screenGeometry.height()) << " x " + << QSTRN(screenGeometry.width()); + qDebug() << "-- Quick window size: " << QSTRN(m_quickWindow->height()) << " x " + << QSTRN(m_quickWindow->width()); + qDebug() << "-- Child size: " << QSTRN(targetItem->height()) << " x " + << QSTRN(targetItem->width()); + qDebug() << "-- Child pos: " << QSTRN(targetItem->x()) << ", " << QSTRN(targetItem->y()); + qDebug() << "-- Child scale: " << QSTRN(targetItem->scale()); + + qDebug("Calculating the new size and scale..."); + + const QSizeF newContentSize = targetItem->size().scaled(screenGeometry.size().toSizeF(), + Qt::AspectRatioMode::KeepAspectRatio); + + const qreal newScale = newContentSize.height() / targetItem->size().height(); + const qreal newX = (targetItem->width() - screenGeometry.width()) / -2.0f; + const qreal newY = (targetItem->height() - screenGeometry.height()) / -2.0f; + + targetItem->setScale(newScale); + targetItem->setPosition(QPointF(newX, newY)); + + qDebug() << "-- Calculated item height: " << QSTRN(newContentSize.height()); + qDebug() << "-- Calculated item width: " << QSTRN(newContentSize.width()); + qDebug() << "-- Calculated item scale: " << QSTRN(newScale); + qDebug() << "-- Calculated item pos..: " << QSTRN(newX) << "," << QSTRN(newY); + + qDebug() << "Final Sizing:"; + qDebug() << "-- Child height: " << QSTRN(targetItem->height()); + qDebug() << "-- Child width: " << QSTRN(targetItem->width()); + qDebug() << "-- Child scale: " << QSTRN(targetItem->scale()); + qDebug() << "-- Child pos-x: " << QSTRN(targetItem->x()); + qDebug() << "-- Child pos-y: " << QSTRN(targetItem->y()); +} + +void ProjectManager::showAppWindow() +{ + QScreen *screen = QGuiApplication::primaryScreen(); + QObject::connect(screen, &QScreen::orientationChanged, this, &ProjectManager::orientateWindow); + orientateWindow(screen->orientation()); + + qDebug("Initializing and showing the QML app window"); + m_quickWindow->show(); +} + +void ProjectManager::hideAppWindow() +{ + qDebug("Hiding the QML app window"); + m_quickWindow->hide(); +} diff --git a/src/projectManager.h b/src/projectManager.h new file mode 100644 index 0000000000000000000000000000000000000000..e036e72867c0f9a90280854c4738605423f8c047 --- /dev/null +++ b/src/projectManager.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#ifndef PROJECTMANAGER_H +#define PROJECTMANAGER_H + +#include <QJsonObject> +#include <QObject> +#include <QQmlComponent> +#include <QQmlEngine> +#include <QQuickView> +#include <QQuickWindow> + +class ProjectManager : public QObject +{ + Q_OBJECT +public: + explicit ProjectManager(QObject *parent = nullptr); + QString unpackProject(const QByteArray &project, bool extractZip = false); + bool runProject(const QString &projectPath, const QString &projectName); + bool cacheProject(const QByteArray &projectData, const QJsonObject &projectInfo); + bool isProjectCached(const QJsonObject &projectInfo); + bool runCachedProject(const QJsonObject &projectInfo); + void showAppWindow(); + void hideAppWindow(); + +public slots: + void orientateWindow(Qt::ScreenOrientation orientation); + +private: + // Member variables + QByteArray m_projectData; + QString m_projectPath; + QString m_projectCachePath; + + // Qml related members + QQmlEngine m_qmlEngine; + QSharedPointer<QQmlComponent> m_qmlComponent; + QSharedPointer<QQuickWindow> m_quickWindow; + + // Member functions + QString findFile(const QString &dir, const QString &filter); + + void parseQmlProjectFile(const QString &fileName, QString *mainFile, QStringList *importPaths); + void cacheProject(const QString &projectName, const QString &projectPath); + +signals: + void projectStateChanged(const QString &state); +}; + +#endif // PROJECTMANAGER_H diff --git a/src/serviceConnector.cpp b/src/serviceConnector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe95e0565a49a3085998a4d08c60c63ee55f6419 --- /dev/null +++ b/src/serviceConnector.cpp @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "serviceConnector.h" + +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QEventLoop> +#include <QJsonDocument> +#include <QJsonObject> + +QSharedPointer<QNetworkReply> ServiceConnector::fetchResource(const QString &url) +{ + qDebug() << "Fetching resource from" << url; + + QNetworkRequest request(url); + request.setRawHeader("Authorization", "test"); + QSharedPointer<QNetworkReply> reply(m_manager.get(request)); + QObject::connect(reply.data(), + &QNetworkReply::sslErrors, + this, + [&](const QList<QSslError> &errors) + { + qCritical() << errors.first().errorString(); + }); + + QEventLoop loop; + QObject::connect(reply.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit); + QObject::connect(reply.data(), + &QNetworkReply::downloadProgress, + this, + [&](qint64 bytesReceived, qint64 bytesTotal) + { + float percentage = roundf((float)bytesReceived / (float)bytesTotal * 100); + qDebug() << "Download progress" << QString::number(percentage) << "% -" << QString::number(bytesReceived) << "/" << QString::number(bytesTotal); + emit downloadProgress(percentage); + }); + loop.exec(); + + if (reply->error() != QNetworkReply::NoError) + qCritical() << reply->errorString(); + else + qDebug() << "Resource fetched successfully"; + + return reply; +} + +QByteArray ServiceConnector::fetchProject(const QString &url) +{ + QString projectUrl = url; + if (projectUrl.startsWith("https://designviewer.qt.io/#")) + { + projectUrl = projectUrl.split("#").at(1); + projectUrl.prepend("https://designviewer.qt.io/qmlprojects/"); + } + + auto reply = fetchResource(projectUrl); + if (reply->error() != QNetworkReply::NoError) + { + qCritical("Could not fetch project"); + return QByteArray(); + } + + return reply->readAll(); +} + +QJsonArray ServiceConnector::fetchUserProjectList(const QString &userHash) +{ + auto reply = fetchResource("https://designviewer.qt.io/api/v1/qmlrc/list/" + userHash + "?key=818815"); + if (reply->error() != QNetworkReply::NoError) + { + qCritical("Could not fetch available project list"); + return QJsonArray(); + } + + QJsonArray projectList = QJsonDocument::fromJson(reply->readAll()) + .object() + .value("data") + .toObject() + .value("packages") + .toArray(); + + return projectList; +} diff --git a/src/serviceConnector.h b/src/serviceConnector.h new file mode 100644 index 0000000000000000000000000000000000000000..b552806d758fb9651861c24f7a671f45a3349b95 --- /dev/null +++ b/src/serviceConnector.h @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Viewer of the Qt Toolkit. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#ifndef SERVICECONNECTOR_H +#define SERVICECONNECTOR_H + +#include <QNetworkAccessManager> +#include <QJsonArray> + +class ServiceConnector : public QObject +{ + Q_OBJECT +public: + QByteArray fetchProject(const QString &url); + QJsonArray fetchUserProjectList(const QString &userHash); + +private: + QNetworkAccessManager m_manager; + QSharedPointer<QNetworkReply> fetchResource(const QString &url); + +signals: + void downloadProgress(float percentage); +}; + +#endif // SERVICECONNECTOR_H diff --git a/ui/AboutHeader.qml b/ui/AboutHeader.qml index 124714b3de40a9f8733ba3205f5f58dc0268a03e..882880f94c8d0fd98925778879109e4dd7eddc68 100644 --- a/ui/AboutHeader.qml +++ b/ui/AboutHeader.qml @@ -18,21 +18,24 @@ Item { Layout.fillWidth: true } - - Label { - id: label2 - text: backend.buildInfo + id: logs Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter horizontalAlignment: "AlignHCenter" + text: qsTr("Hello World") } - Item { id: item1 Layout.fillHeight: true Layout.fillWidth: true } + Connections { + target: backend + function onBuildInfoChanged(buildInfo){ + logs.text = buildInfo; + } + } } } diff --git a/ui/ExamplesPage.qml b/ui/ExamplesPage.qml index cb40cd5ffffffc3269ef30647ee7e59dd3fa25ea..c67367628c4112cdf08abbcda2b4008827b154fc 100644 --- a/ui/ExamplesPage.qml +++ b/ui/ExamplesPage.qml @@ -16,12 +16,6 @@ Item { onClicked: backend.downloadAndRun("https://designviewer.qt.io/#ClusterTutorial.qmlrc") } - Button { - id: button5 - text: qsTr("Coffee Machine") - Layout.fillWidth: true - onClicked: backend.downloadAndRun("https://designviewer.qt.io/#CoffeeMachine.qmlrc") - } Button { id: button @@ -30,20 +24,6 @@ Item { onClicked: backend.downloadAndRun("https://designviewer.qt.io/#EBikeDesign.qmlrc") } - Button { - id: button2 - text: qsTr("Side Menu") - Layout.fillWidth: true - onClicked: backend.downloadAndRun("https://designviewer.qt.io/#SideMenu.qmlrc") - } - - Button { - id: button1 - text: qsTr("Webinar Demo") - Layout.fillWidth: true - onClicked: backend.downloadAndRun("https://designviewer.qt.io/#WebinarDemo.qmlrc") - } - Button { id: button4 text: qsTr("Material Bundle") diff --git a/ui/HomePage.qml b/ui/HomePage.qml index 714d40fadd2c4aa4f706d8d582581a47e0bef2d6..515b68cb07068e049ab5907cb02d52053945ac18 100644 --- a/ui/HomePage.qml +++ b/ui/HomePage.qml @@ -83,12 +83,10 @@ Item { RowLayout { id: rowLayout - anchors.left: parent.left - anchors.right: parent.right Button { id: downloadButton text: qsTr("Download and Run") - onClicked: backend.downloadAndRun(urlTextField.text) + onClicked: backend.runUserProject(urlTextField.text) Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter } @@ -101,7 +99,6 @@ Item { Layout.fillHeight: true Layout.fillWidth: true Layout.preferredHeight: 10 - Layout.preferredWidth: 10 } diff --git a/ui/Logs.qml b/ui/Logs.qml index 695223c69b1f72c6d3dbc784bd8cce966b5a93e8..44527d5c8a254e83d7a2294414331b190d054e54 100644 --- a/ui/Logs.qml +++ b/ui/Logs.qml @@ -15,23 +15,23 @@ Rectangle { anchors.fill: parent topPadding: 13.1 ScrollBar.vertical.policy: ScrollBar.AlwaysOn - Connections { - target: backend - function onLogsChanged() { - logTextArea.text = backend.logs - scrollArea.ScrollBar.vertical.position = 1.0 - scrollArea.ScrollBar.vertical.size - } - } + TextArea { clip: false id: logTextArea wrapMode: TextEdit.Wrap - anchors.fill: parent - text: backend.logs rightInset: 20 readOnly: true anchors.topMargin: 5 placeholderText: qsTr("Application Logs") } + + Connections { + target: backend + function onLogsChanged(logs) { + logTextArea.text = logs + scrollArea.ScrollBar.vertical.position = 1.0 - scrollArea.ScrollBar.vertical.size + } + } } } diff --git a/ui/main.qml b/ui/main.qml index ad11c1a0a1dc6819f4237d10f3f38b6bd127aede..256b60a42d59cb7a005f504c87cd7f707a35ee67 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -47,7 +47,6 @@ Rectangle { anchors.fill: parent Text { id: popupText - text: backend.popupText } Item { id: name @@ -55,9 +54,7 @@ Rectangle { ProgressBar { id: popupProgressBar Layout.fillWidth: true - value: backend.downloadProgress to: 100 - indeterminate: backend.popupProgressIndeterminate } } @@ -75,6 +72,9 @@ Rectangle { function onPopupProgressIndeterminateChanged(status) { popupProgressBar.indeterminate = status } + function onDownloadProgress(progress) { + popupProgressBar.value = progress + } } } @@ -148,7 +148,9 @@ Rectangle { Connections { target: home - onClicked: stackLayout.currentIndex = 0 + function onClicked(){ + stackLayout.currentIndex = 0 + } } } @@ -162,7 +164,9 @@ Rectangle { Connections { target: examples - onClicked: stackLayout.currentIndex = 1 + function onClicked(){ + stackLayout.currentIndex = 1 + } } } @@ -176,7 +180,9 @@ Rectangle { Connections { target: logs - onClicked: stackLayout.currentIndex = 2 + function onClicked(){ + stackLayout.currentIndex = 2 + } } } @@ -190,13 +196,11 @@ Rectangle { Connections { target: about - onClicked: stackLayout.currentIndex = 3 + function onClicked(){ + stackLayout.currentIndex = 3 + } } } - - - - } } }