diff --git a/src/app/ComponentTestWindow.qml b/src/app/ComponentTestWindow.qml new file mode 100644 index 0000000000000000000000000000000000000000..7729d58634af1a2859ca8b7f65628ec122a106a8 --- /dev/null +++ b/src/app/ComponentTestWindow.qml @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtQuick.Controls + +import net.jami.Adapters 1.1 + +// A window into which we can load a QML file for testing. +ApplicationWindow { + id: appWindow + visible: true + width: testWidth || loader.implicitWidth || 800 + height: testHeight || loader.implicitHeight || 600 + title: testComponentURI + + // WARNING: The following currently must be maintained in tandem with MainApplicationWindow.qml + // Used to manage full screen mode and save/restore window geometry. + readonly property bool useFrameless: false + property bool isRTL: UtilsAdapter.isRTL + LayoutMirroring.enabled: isRTL + LayoutMirroring.childrenInherit: isRTL + property LayoutManager layoutManager: LayoutManager { + appContainer: null + } + // Used to manage dynamic view loading and unloading. + property ViewManager viewManager: ViewManager {} + // Used to manage the view stack and the current view. + property ViewCoordinator viewCoordinator: ViewCoordinator {} + + Loader { + id: loader + source: Qt.resolvedUrl(testComponentURI) + onStatusChanged: { + console.log("Status changed to:", loader.status) + if (loader.status == Loader.Error || loader.status == Loader.Null) { + console.error("Couldn't load component:", source) + Qt.exit(1); + } else if (loader.status == Loader.Ready) { + console.info("Loaded component:", source); + // If any of the dimensions are not set, set them to the appWindow's dimensions + item.width = item.width || Qt.binding(() => appWindow.width); + item.height = item.height || Qt.binding(() => appWindow.height); + viewCoordinator.init(item); + } + } + } + + // Closing this window should always exit the application. + onClosing: Qt.quit() +} diff --git a/src/app/LayoutManager.qml b/src/app/LayoutManager.qml index f36b2b685f127889d42dff3599716eebafb3771e..97017c2409bb0f84afca8016748bf30823e6810d 100644 --- a/src/app/LayoutManager.qml +++ b/src/app/LayoutManager.qml @@ -41,6 +41,20 @@ QtObject { // Used to store if a OngoingCallPage component is fullscreened. property bool isCallFullscreen: false + // QWK: Provide spacing for widgets that may be occluded by the system buttons. + property QtObject qwkSystemButtonSpacing: QtObject { + id: qwkSystemButtonSpacing + readonly property bool isMacOS: Qt.platform.os.toString() === "osx" + // macOS buttons are on the left. + readonly property real left: { + appWindow.useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0 + } + // Windows and Linux buttons are on the right. + readonly property real right: { + appWindow.useFrameless && !isMacOS && !root.isFullscreen ? sysBtnsLoader.width + 24 : 0 + } + } + // Restore a visible windowed mode. function restoreApp() { if (isHidden) { diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml index 31b9dee728b6f1a04cf74f0f8bcc24e6f0c41087..b94475ad25ce1e1f488d04878230d5f2489635bb 100644 --- a/src/app/MainApplicationWindow.qml +++ b/src/app/MainApplicationWindow.qml @@ -41,14 +41,11 @@ import QWindowKit ApplicationWindow { id: appWindow + readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow) property bool isRTL: UtilsAdapter.isRTL - LayoutMirroring.enabled: isRTL LayoutMirroring.childrenInherit: isRTL - // This needs to be set from the start. - readonly property bool useFrameless: UtilsAdapter.getAppValue(Settings.Key.UseFramelessWindow) - onActiveFocusItemChanged: { focusOverlay.margin = -5; if (activeFocusItem && ((activeFocusItem.focusReason === Qt.TabFocusReason) || (activeFocusItem.focusReason === Qt.BacktabFocusReason))) { @@ -94,16 +91,10 @@ ApplicationWindow { id: layoutManager appContainer: fullscreenContainer } - // Used to manage dynamic view loading and unloading. - ViewManager { - id: viewManager - } - + property ViewManager viewManager: ViewManager {} // Used to manage the view stack and the current view. - ViewCoordinator { - id: viewCoordinator - } + property ViewCoordinator viewCoordinator: ViewCoordinator {} // Used to prevent the window from being visible until the // window geometry has been restored and the view stack has @@ -234,17 +225,6 @@ ApplicationWindow { anchors.fill: parent } - // QWK: Provide spacing for widgets that may be occluded by the system buttons. - QtObject { - id: qwkSystemButtonSpacing - readonly property bool isMacOS: Qt.platform.os.toString() === "osx" - readonly property bool isFullscreen: layoutManager.isFullScreen - // macOS buttons are on the left. - readonly property real left: useFrameless && isMacOS && viewCoordinator.isInSinglePaneMode ? 80 : 0 - // Windows and Linux buttons are on the right. - readonly property real right: useFrameless && !isMacOS && !isFullscreen ? sysBtnsLoader.width + 24 : 0 - } - // QWK: Window Title bar Item { id: titleBar diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index 48b4fed51bc52a268cdb01f8b98d151c592c7722..8398bbf1b6c2ad56ff129706d315d09195c67b9b 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -335,49 +335,59 @@ MainApplication::parseArguments() } } - QCommandLineParser parser; - parser.addHelpOption(); - parser.addVersionOption(); + parser_.addHelpOption(); + parser_.addVersionOption(); QCommandLineOption webDebugOption(QStringList() << "remote-debugging-port", "Web debugging port.", "port"); - parser.addOption(webDebugOption); + parser_.addOption(webDebugOption); QCommandLineOption minimizedOption({"m", "minimized"}, "Start minimized."); - parser.addOption(minimizedOption); + parser_.addOption(minimizedOption); QCommandLineOption debugOption({"d", "debug"}, "Debug out."); - parser.addOption(debugOption); + parser_.addOption(debugOption); QCommandLineOption logFileOption({"f", "file"}, "Debug to <file>.", "file"); - parser.addOption(logFileOption); + parser_.addOption(logFileOption); #ifdef Q_OS_WINDOWS QCommandLineOption updateUrlOption({"u", "url"}, "<url> for debugging version queries.", "url"); - parser.addOption(updateUrlOption); + parser_.addOption(updateUrlOption); #endif QCommandLineOption terminateOption({"t", "term"}, "Terminate all instances."); - parser.addOption(terminateOption); + parser_.addOption(terminateOption); QCommandLineOption muteDaemonOption({"q", "quiet"}, "Mute daemon logging. (only if debug)"); - parser.addOption(muteDaemonOption); + parser_.addOption(muteDaemonOption); - parser.process(*this); +#ifdef QT_DEBUG + // In debug mode, add an option to test a specific QML component via its name. + // e.g. ./jami --test AccountComboBox + parser_.addOption(QCommandLineOption("test", "Test a QML component via its name.", "uri")); + // We may need to force the test window dimensions in the case that the component to test + // does not specify its own dimensions and is dependent on parent/sibling dimensions. + // e.g. ./jami --test AccountComboBox -w 200 + parser_.addOption(QCommandLineOption("width", "Width for the test window.", "width")); + parser_.addOption(QCommandLineOption("height", "Height for the test window.", "height")); +#endif - runOptions_[Option::StartMinimized] = parser.isSet(minimizedOption); - runOptions_[Option::Debug] = parser.isSet(debugOption); - if (parser.isSet(logFileOption)) { - auto logFileValue = parser.value(logFileOption); + parser_.process(*this); + + runOptions_[Option::StartMinimized] = parser_.isSet(minimizedOption); + runOptions_[Option::Debug] = parser_.isSet(debugOption); + if (parser_.isSet(logFileOption)) { + auto logFileValue = parser_.value(logFileOption); auto logFile = logFileValue.isEmpty() ? Utils::getDebugFilePath() : logFileValue; qputenv("JAMI_LOG_FILE", logFile.toStdString().c_str()); } #ifdef Q_OS_WINDOWS - runOptions_[Option::UpdateUrl] = parser.value(updateUrlOption); + runOptions_[Option::UpdateUrl] = parser_.value(updateUrlOption); #endif - runOptions_[Option::TerminationRequested] = parser.isSet(terminateOption); - runOptions_[Option::MuteDaemon] = parser.isSet(muteDaemonOption); + runOptions_[Option::TerminationRequested] = parser_.isSet(terminateOption); + runOptions_[Option::MuteDaemon] = parser_.isSet(muteDaemonOption); } void @@ -393,6 +403,35 @@ MainApplication::setApplicationFont() setFont(font); } +QString +findResource(const QString& targetBasename, const QString& basePath = ":/") +{ + QDir dir(basePath); + // List all entries in the directory excluding special entries '.' and '..' + QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + QDir::DirsFirst); + + Q_FOREACH (const QString& entry, entries) { + QString fullPath = basePath + "/" + entry; + QFileInfo fileInfo(fullPath); + + if (fileInfo.isDir()) { + // Recursively search in subdirectories + QString found = findResource(targetBasename, fullPath); + if (!found.isEmpty()) { + return found; // Return the first match found in any subdirectory + } + } else if (fileInfo.isFile() + && fileInfo.fileName().contains(targetBasename, Qt::CaseInsensitive)) { + // Match found, return the full path but remove the leading ":/". + return fileInfo.absoluteFilePath().mid(2); + } + } + + // No match found in this directory or its subdirectories + return QString(); +} + void MainApplication::initQmlLayer() { @@ -406,7 +445,30 @@ MainApplication::initQmlLayer() &screenInfo_, this); - engine_->load(QUrl(QStringLiteral("qrc:/MainApplicationWindow.qml"))); + QUrl url; + if (parser_.isSet("test")) { + // List the QML files in the project source tree. + const auto targetTestComponent = findResource(parser_.value("test")); + if (targetTestComponent.isEmpty()) { + C_FATAL << "Failed to find QML component:" << parser_.value("test"); + } + engine_->rootContext()->setContextProperty("testComponentURI", targetTestComponent); + // Log the width and height values for the test window. + const auto testWidth = parser_.isSet("width") ? parser_.value("width").toInt() : 0; + const auto testHeight = parser_.isSet("height") ? parser_.value("height").toInt() : 0; + engine_->rootContext()->setContextProperty("testWidth", testWidth); + engine_->rootContext()->setContextProperty("testHeight", testHeight); + url = u"qrc:/ComponentTestWindow.qml"_qs; + } else { + url = u"qrc:/MainApplicationWindow.qml"_qs; + } + QObject::connect( + engine_.get(), + &QQmlApplicationEngine::objectCreationFailed, + this, + [url]() { C_FATAL << "Failed to load QML component:" << url; }, + Qt::QueuedConnection); + engine_->load(url); // Report the render interface used. C_DBG << "Main window loaded using" << getRenderInterfaceString(); diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h index d7117d6870061eadba56827fbb7b73239413f0dc..e318ad73cdf54e63e99902c5da689e90e2d1f3a2 100644 --- a/src/app/mainapplication.h +++ b/src/app/mainapplication.h @@ -29,6 +29,7 @@ #include <QQmlEngine> #include <QScreen> #include <QWindow> +#include <QCommandLineParser> #include <memory> @@ -122,6 +123,6 @@ private: SystemTray* systemTray_; AppSettingsManager* settingsManager_; PreviewEngine* previewEngine_; - ScreenInfo screenInfo_; + QCommandLineParser parser_; }; diff --git a/src/app/mainview/components/AccountComboBox.qml b/src/app/mainview/components/AccountComboBox.qml index aa455bd20dd29b3354dc6694a3f2e1dd483f569f..c2f7c3b1c8861e6f0cce8cf9358743a698877371 100644 --- a/src/app/mainview/components/AccountComboBox.qml +++ b/src/app/mainview/components/AccountComboBox.qml @@ -29,7 +29,7 @@ Label { property alias popup: comboBoxPopup - width: parent ? parent.width : o + width: parent ? parent.width : 0 height: JamiTheme.accountListItemHeight property bool inSettings: viewCoordinator.currentViewName === "SettingsView" diff --git a/src/app/mainview/components/ChatViewHeader.qml b/src/app/mainview/components/ChatViewHeader.qml index d33ab24cc6c72cf6d2ecdc4d0b2f3e26d955f293..69c02d7f77c9c0ed5ed8e6ffaaa6dadcdf83a73c 100644 --- a/src/app/mainview/components/ChatViewHeader.qml +++ b/src/app/mainview/components/ChatViewHeader.qml @@ -75,8 +75,8 @@ Rectangle { anchors.fill: parent // QWK: spacing - anchors.leftMargin: qwkSystemButtonSpacing.left - anchors.rightMargin: 10 + qwkSystemButtonSpacing.right + anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left + anchors.rightMargin: 10 + layoutManager.qwkSystemButtonSpacing.right spacing: 16 JamiPushButton { QWKSetParentHitTestVisible {} diff --git a/src/app/mainview/components/MainOverlay.qml b/src/app/mainview/components/MainOverlay.qml index aabf85e5421c815c2e4437cb213d3ef776758511..455ecb347d5f67be8770bf9cbf9ed04a458a0844 100644 --- a/src/app/mainview/components/MainOverlay.qml +++ b/src/app/mainview/components/MainOverlay.qml @@ -131,8 +131,8 @@ Item { anchors.left: parent.left anchors.right: parent.right // QWK: spacing - anchors.leftMargin: qwkSystemButtonSpacing.left - anchors.rightMargin: qwkSystemButtonSpacing.right + anchors.leftMargin: layoutManager.qwkSystemButtonSpacing.left + anchors.rightMargin: layoutManager.qwkSystemButtonSpacing.right RowLayout { anchors.fill: parent diff --git a/tests/qml/src/TestWrapper.qml b/tests/qml/src/TestWrapper.qml index d8f4510d4b70aef954429bb585d9c826b5ac92d9..0f50f3f8af329f70d12fdabb3509cab034cdec63 100644 --- a/tests/qml/src/TestWrapper.qml +++ b/tests/qml/src/TestWrapper.qml @@ -19,6 +19,8 @@ import QtQuick import QtQuick.Controls import QtTest +import net.jami.Adapters 1.1 + import "../../../src/app/" // The purpose of this component is to fake the ApplicationWindow and prevent @@ -38,11 +40,23 @@ Item { Component.onCompleted: viewCoordinator.init(this) - // These are our fake app management objects. The caveat is that they - // must be maintained in sync with the actual objects in the app for now. - // The benefit, is that this should be the single place where we need to - // sync them. + property int visibility: 0 + Binding { + tw.visibility: uut.Window.window.visibility + when: QTestRootObject.windowShown + } + + // WARNING: The following currently must be maintained in tandem with MainApplicationWindow.qml + // Used to manage full screen mode and save/restore window geometry. + property bool isRTL: UtilsAdapter.isRTL + LayoutMirroring.enabled: isRTL + LayoutMirroring.childrenInherit: isRTL + property LayoutManager layoutManager: LayoutManager { + appContainer: null + } + // Used to manage dynamic view loading and unloading. property ViewManager viewManager: ViewManager {} + // Used to manage the view stack and the current view. property ViewCoordinator viewCoordinator: ViewCoordinator {} property QtObject appWindow: QtObject { property bool useFrameless: false