diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 94a050ff7083..cfb4d1df0c5f 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -75,6 +75,11 @@ runs: shell: bash run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" print-packages + - name: Normalize Ubuntu apt mirrors (Linux ARM64) + if: runner.os == 'Linux' && runner.arch == 'ARM64' + shell: bash + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" normalize-apt-sources + - name: Enable universe repository (Linux) if: runner.os == 'Linux' shell: bash @@ -82,21 +87,37 @@ runs: - name: Install apt packages (Linux) if: runner.os == 'Linux' - # Retry: a slow apt mirror can stall the download past the job timeout; fast-fail and retry. - uses: nick-fields/retry@v4 + shell: bash env: APT_PACKAGES: ${{ steps.linux-deps.outputs.packages }} - with: - timeout_minutes: 20 - max_attempts: 3 - shell: bash - # Acquire::*::Timeout aborts a stalled mirror connection so retries can fire. - command: | - APT_OPTS="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30" - # shellcheck disable=SC2086 - sudo apt-get $APT_OPTS update -qq - # shellcheck disable=SC2086 - sudo apt-get $APT_OPTS install -y --no-install-recommends $APT_PACKAGES + run: | + # shellcheck disable=SC2153 # Supplied through the step environment. + read -r -a apt_packages <<< "$APT_PACKAGES" + + for attempt in 1 2 3; do + # Run timeout as root so it can terminate the privileged apt process tree. + if sudo timeout --signal=TERM --kill-after=30s 20m bash -c ' + set -euo pipefail + apt_opts=( + -o Acquire::Retries=3 + -o Acquire::ForceIPv4=true + -o Acquire::http::Timeout=30 + -o Acquire::https::Timeout=30 + ) + apt-get "${apt_opts[@]}" update -qq + apt-get "${apt_opts[@]}" install -y --no-install-recommends "$@" + ' -- "${apt_packages[@]}"; then + exit 0 + fi + + if [[ "$attempt" -eq 3 ]]; then + echo "::error::apt package installation failed after $attempt attempts" + exit 1 + fi + + echo "::warning::apt package installation attempt $attempt failed; retrying in 10 seconds" + sleep 10 + done - name: Fix apt alternatives (Linux) if: runner.os == 'Linux' @@ -134,4 +155,3 @@ runs: max_attempts: 3 command: | python3 tools/setup/install_dependencies --platform macos - diff --git a/.github/scripts/install_dependencies_helper.py b/.github/scripts/install_dependencies_helper.py index 6e61f04a348d..dce3304e3d99 100644 --- a/.github/scripts/install_dependencies_helper.py +++ b/.github/scripts/install_dependencies_helper.py @@ -2,6 +2,7 @@ """Post-install fixups for CI dependency caching on Linux. Handles: +- Normalizing official Ubuntu apt mirrors to reliable HTTPS endpoints - Enabling the Ubuntu universe repository - Repairing apt alternatives after cache-apt-pkgs-action restore - Installing optional packages @@ -23,6 +24,10 @@ from common.proc import run_captured APT_OPTS = ["-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3"] +UBUNTU_APT_URL_PATTERN = re.compile( + r"http://(?P[a-z0-9.-]+\.ubuntu\.com)(?P(?::\d+)?(?:/[^\s#]*)?)", + re.IGNORECASE, +) def _sudo(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess: @@ -40,6 +45,63 @@ def _get_multiarch() -> str: return result.stdout.strip() if result.returncode == 0 else "" +def _normalize_ubuntu_apt_urls(text: str) -> str: + """Use canonical HTTPS endpoints for official Ubuntu apt repositories.""" + + def replace_url(match: re.Match[str]) -> str: + host = match.group("host").lower() + suffix = match.group("suffix") + + if host.endswith(".ec2.ports.ubuntu.com"): + host = "ports.ubuntu.com" + elif host.endswith(".ec2.archive.ubuntu.com"): + host = "archive.ubuntu.com" + elif host not in {"archive.ubuntu.com", "ports.ubuntu.com", "security.ubuntu.com"}: + return match.group(0) + + return f"https://{host}{suffix}" + + return UBUNTU_APT_URL_PATTERN.sub(replace_url, text) + + +def normalize_apt_sources(apt_root: Path = Path("/etc/apt")) -> None: + """Normalize Ubuntu URLs in legacy and deb822 apt source files.""" + sources = [apt_root / "sources.list"] + sources_dir = apt_root / "sources.list.d" + if sources_dir.is_dir(): + sources.extend(sorted(sources_dir.glob("*.list"))) + sources.extend(sorted(sources_dir.glob("*.sources"))) + + changed_sources = 0 + for source in sources: + if not source.is_file(): + continue + + try: + original = source.read_text(errors="replace") + except OSError as error: + gh_warning(f"Unable to read apt source {source}: {error}") + continue + + normalized = _normalize_ubuntu_apt_urls(original) + if normalized == original: + continue + + try: + source.write_text(normalized) + except PermissionError: + subprocess.run( + ["sudo", "tee", str(source)], + check=True, + input=normalized, + stdout=subprocess.DEVNULL, + text=True, + ) + changed_sources += 1 + + print(f"Normalized {changed_sources} apt source file(s)") + + def enable_universe() -> None: """Enable the Ubuntu universe repository if not already present.""" sources_dirs = [Path("/etc/apt/sources.list"), Path("/etc/apt/sources.list.d")] @@ -150,6 +212,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("normalize-apt-sources") sub.add_parser("enable-universe") sub.add_parser("fix-apt-alternatives") sub.add_parser("install-optional") @@ -158,6 +221,7 @@ def main() -> None: args = parser.parse_args() commands = { + "normalize-apt-sources": normalize_apt_sources, "enable-universe": enable_universe, "fix-apt-alternatives": fix_apt_alternatives, "install-optional": install_optional_packages, diff --git a/.github/scripts/tests/test_install_dependencies_helper.py b/.github/scripts/tests/test_install_dependencies_helper.py index 4704e7db1aa4..f3aaf13600e6 100644 --- a/.github/scripts/tests/test_install_dependencies_helper.py +++ b/.github/scripts/tests/test_install_dependencies_helper.py @@ -7,6 +7,61 @@ import install_dependencies_helper +class TestNormalizeAptSources: + """Tests for Ubuntu apt mirror normalization.""" + + def test_normalizes_legacy_ec2_ports_mirror(self): + """Remaps the ARM64 EC2 mirror to the canonical HTTPS endpoint.""" + source = "deb http://us-west-2.ec2.ports.ubuntu.com/ubuntu-ports noble main universe\n" + + assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == ( + "deb https://ports.ubuntu.com/ubuntu-ports noble main universe\n" + ) + + def test_normalizes_deb822_official_mirrors(self): + """Normalizes official archive and security URLs in deb822 sources.""" + source = ( + "Types: deb\n" + "URIs: http://archive.ubuntu.com/ubuntu http://security.ubuntu.com/ubuntu\n" + "Suites: noble noble-updates noble-security\n" + ) + + assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == ( + "Types: deb\n" + "URIs: https://archive.ubuntu.com/ubuntu https://security.ubuntu.com/ubuntu\n" + "Suites: noble noble-updates noble-security\n" + ) + + def test_preserves_third_party_and_unknown_ubuntu_mirrors(self): + """Does not rewrite repositories whose HTTPS support is unknown.""" + source = ( + "deb http://packages.example.com/ubuntu noble main\n" + "deb http://azure.archive.ubuntu.com/ubuntu noble main\n" + ) + + assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == source + + def test_updates_legacy_and_deb822_files(self, tmp_path, capsys): + """Discovers and updates both supported apt source file formats.""" + sources_dir = tmp_path / "sources.list.d" + sources_dir.mkdir() + legacy_source = tmp_path / "sources.list" + deb822_source = sources_dir / "ubuntu.sources" + ignored_source = sources_dir / "ignored.conf" + legacy_source.write_text("deb http://ports.ubuntu.com/ubuntu-ports noble main\n") + deb822_source.write_text("URIs: http://eu-west-1.ec2.archive.ubuntu.com/ubuntu\n") + ignored_source.write_text("deb http://archive.ubuntu.com/ubuntu noble main\n") + + install_dependencies_helper.normalize_apt_sources(tmp_path) + + assert legacy_source.read_text() == ( + "deb https://ports.ubuntu.com/ubuntu-ports noble main\n" + ) + assert deb822_source.read_text() == "URIs: https://archive.ubuntu.com/ubuntu\n" + assert ignored_source.read_text() == "deb http://archive.ubuntu.com/ubuntu noble main\n" + assert capsys.readouterr().out == "Normalized 2 apt source file(s)\n" + + class TestDetectPythonVersion: """Tests for detect_python_version function.""" diff --git a/CMakeLists.txt b/CMakeLists.txt index 56513f894fae..3954d9a0fe5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,6 +213,7 @@ find_package(Qt6 Core Gui HttpServer + LabsStyleKit LinguistTools Location LocationPrivate @@ -373,16 +374,6 @@ qt_add_translations(${CMAKE_PROJECT_NAME} MERGE_QT_TRANSLATIONS ) -# ---------------------------------------------------------------------------- -# Qt Quick Controls Configuration -# ---------------------------------------------------------------------------- -qgc_set_qt_resource_alias("${CMAKE_SOURCE_DIR}/resources/qtquickcontrols2.conf") - -qt_add_resources(${CMAKE_PROJECT_NAME} "qgcresources_cmake" - PREFIX "/" - FILES "${CMAKE_SOURCE_DIR}/resources/qtquickcontrols2.conf" -) - # ---------------------------------------------------------------------------- # Qt Plugin Configuration # ---------------------------------------------------------------------------- diff --git a/deploy/docker/install-sysroot-aarch64.sh b/deploy/docker/install-sysroot-aarch64.sh index e750fa495259..4c4a1df20f7a 100755 --- a/deploy/docker/install-sysroot-aarch64.sh +++ b/deploy/docker/install-sysroot-aarch64.sh @@ -19,7 +19,7 @@ set -euo pipefail SYSROOT="${SYSROOT:-/opt/sysroot}" UBUNTU_SUITE="${UBUNTU_SUITE:-noble}" -MIRROR="${MIRROR:-http://ports.ubuntu.com/ubuntu-ports}" +MIRROR="${MIRROR:-https://ports.ubuntu.com/ubuntu-ports}" # Reject newlines/junk in overrides — they land verbatim in the apt sources heredoc. if [[ ! "$UBUNTU_SUITE" =~ ^[a-z][-a-z0-9]*$ ]]; then @@ -96,7 +96,13 @@ EOF } configure_apt -apt_retry apt-get -o Acquire::Retries=3 update -y --quiet +# apt-get update normally exits successfully when an index download fails. Make +# partial updates fatal so apt_retry can recover instead of continuing with no +# arm64 package metadata and producing an empty dependency closure. +apt_retry apt-get \ + -o APT::Update::Error-Mode=any \ + -o Acquire::Retries=3 \ + update -y --quiet # Target-side libraries are single-sourced from install_dependencies # (DEBIAN_PACKAGES["cross_arm64"]) so they can't drift from the native build; diff --git a/resources/qtquickcontrols2.conf b/resources/qtquickcontrols2.conf deleted file mode 100644 index db9486764e72..000000000000 --- a/resources/qtquickcontrols2.conf +++ /dev/null @@ -1,2 +0,0 @@ -[Controls] -Style=Basic diff --git a/src/AppSettings/LinkConfigurationManager.qml b/src/AppSettings/LinkConfigurationManager.qml index 1079208aae85..2aada85656a3 100644 --- a/src/AppSettings/LinkConfigurationManager.qml +++ b/src/AppSettings/LinkConfigurationManager.qml @@ -92,7 +92,7 @@ SettingsGroupLayout { buttonText: qsTr("Add") onClicked: { - var editingConfig = _linkManager.createConfiguration(ScreenTools.isSerialAvailable ? LinkConfiguration.TypeSerial : LinkConfiguration.TypeUdp, "") + const editingConfig = _root._linkManager.createConfiguration() linkDialogFactory.open({ editingConfig: editingConfig, originalConfig: null }) } } diff --git a/src/AppSettings/QmlTest.qml b/src/AppSettings/QmlTest.qml index 024971fcd3af..d5d2e2d34d21 100644 --- a/src/AppSettings/QmlTest.qml +++ b/src/AppSettings/QmlTest.qml @@ -4,7 +4,6 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls - Rectangle { id: _root anchors.fill: parent diff --git a/src/AppSettings/pages/General.SettingsUI.json b/src/AppSettings/pages/General.SettingsUI.json index c43322da8ba2..f9a954c9c46d 100644 --- a/src/AppSettings/pages/General.SettingsUI.json +++ b/src/AppSettings/pages/General.SettingsUI.json @@ -7,7 +7,7 @@ "groups": [ { "heading": "General", - "keywords": ["language", "locale", "color scheme", "dark mode", "theme", "palette", "follow me", "audio", "volume", "sound", "gstreamer", "debug level", "ui scale", "font size", "zoom", "save path", "storage", "reset", "clear settings", "factory reset"], + "keywords": ["language", "locale", "color scheme", "dark mode", "theme", "palette", "follow me", "audio", "volume", "sound", "gstreamer", "debug level", "ui scale", "font size", "zoom", "accessibility", "contrast", "motion", "touch", "pointer", "density", "save path", "storage", "reset", "clear settings", "factory reset"], "controls": [ { "setting": "appSettings.qLocaleLanguage", @@ -40,6 +40,16 @@ "setting": "appSettings.uiScalePercent", "control": "scaler" }, + { + "setting": "appSettings.forceHighContrast" + }, + { + "setting": "appSettings.reducedMotion" + }, + { + "setting": "appSettings.touchModeOverride", + "control": "combobox" + }, { "setting": "appSettings.savePath", "control": "browse", diff --git a/src/AppSettings/pages/SettingsPages.json b/src/AppSettings/pages/SettingsPages.json index a6cd61ba8f64..7fcdba8a2655 100644 --- a/src/AppSettings/pages/SettingsPages.json +++ b/src/AppSettings/pages/SettingsPages.json @@ -107,19 +107,19 @@ "name": "Mock Link", "qml": "MockLink.qml", "icon": "qrc:/InstrumentValueIcons/drone.svg", - "visible": "ScreenTools.isDebug" + "visible": "QGroundControl.isDebugBuild" }, { "name": "Debug", "qml": "DebugWindow.qml", "icon": "qrc:/InstrumentValueIcons/bug.svg", - "visible": "ScreenTools.isDebug" + "visible": "QGroundControl.isDebugBuild" }, { "name": "Palette Test", "qml": "QmlTest.qml", "icon": "qrc:/InstrumentValueIcons/photo.svg", - "visible": "ScreenTools.isDebug" + "visible": "QGroundControl.isDebugBuild" } ] } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8d06f300eca7..8fd90a94114b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -106,6 +106,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME} Qt6::Gui Qt6::GuiPrivate Qt6::HttpServer + Qt6::LabsStyleKit Qt6::Network Qt6::StateMachine Qt6::Svg @@ -140,6 +141,8 @@ target_link_libraries(${CMAKE_PROJECT_NAME} FlightMapModule PlanViewModule QGroundControlControlsModule + QGCStyle + QGCStyleplugin QGroundControlModule ToolbarModule VehicleSetupModule diff --git a/src/Comms/LinkManager.h b/src/Comms/LinkManager.h index 739b2a521a78..0447e920f2b9 100644 --- a/src/Comms/LinkManager.h +++ b/src/Comms/LinkManager.h @@ -51,7 +51,8 @@ class LinkManager : public QObject void init(); /// Create/Edit Link Configuration - Q_INVOKABLE LinkConfiguration *createConfiguration(int type, const QString &name); + /// Default type is the first LinkType enum entry (serial when available, otherwise UDP) + Q_INVOKABLE LinkConfiguration *createConfiguration(int type = 0, const QString &name = QString()); Q_INVOKABLE LinkConfiguration *startConfigurationEditing(LinkConfiguration *config); Q_INVOKABLE void cancelConfigurationEditing(LinkConfiguration *config) const { delete config; } Q_INVOKABLE void endConfigurationEditing(LinkConfiguration *config, LinkConfiguration *editedConfig); diff --git a/src/FlyView/FlyViewGripperDropPanel.qml b/src/FlyView/FlyViewGripperDropPanel.qml index aeeaf286b943..57e48dbf88ba 100644 --- a/src/FlyView/FlyViewGripperDropPanel.qml +++ b/src/FlyView/FlyViewGripperDropPanel.qml @@ -5,7 +5,7 @@ import QGroundControl import QGroundControl.Controls ColumnLayout { - spacing: ScreenTools.defaultFontHeight / 2 + spacing: ScreenTools.defaultFontPixelHeight / 2 property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle property var _buttonTitles: [qsTr("Release"), qsTr("Grab"), qsTr("Hold")] diff --git a/src/FlyView/GuidedActionsController.qml b/src/FlyView/GuidedActionsController.qml index deac9137e8c9..29f3d38a7900 100644 --- a/src/FlyView/GuidedActionsController.qml +++ b/src/FlyView/GuidedActionsController.qml @@ -153,7 +153,7 @@ Item { property var _corePlugin: QGroundControl.corePlugin property var _corePluginOptions: QGroundControl.corePlugin.options - property bool _guidedActionsEnabled: (!ScreenTools.isDebug && _corePluginOptions.guidedActionsRequireRCRSSI && _activeVehicle) ? _rcRSSIAvailable : _activeVehicle + property bool _guidedActionsEnabled: _activeVehicle && (!_corePluginOptions.guidedActionsRequireRCRSSI || _rcRSSIAvailable) property string _flightMode: _activeVehicle ? _activeVehicle.flightMode : "" property bool _missionAvailable: missionController.containsItems property bool _missionActive: _activeVehicle ? _vehicleArmed && (_vehicleInLandMode || _vehicleInRTLMode || _vehicleInMissionMode) : false diff --git a/src/MainWindow/MainWindow.qml b/src/MainWindow/MainWindow.qml index 691c94b74ab5..731678ebcdd5 100644 --- a/src/MainWindow/MainWindow.qml +++ b/src/MainWindow/MainWindow.qml @@ -20,11 +20,19 @@ ApplicationWindow { // The special casing for android prevents white bars from showing up on the edges of the screen with newer android versions flags: Qt.Window | (ScreenTools.isAndroid ? Qt.ExpandedClientAreaHint | Qt.NoTitleBarBackgroundHint : 0) + readonly property var _qgcStyleEnvironment: QGCStyleEnvironment + Component.onCompleted: { // Start the sequence of first run prompt(s) firstRunPromptManager.nextPrompt() } + Binding { + target: ScreenTools + property: "_windowScreen" + value: mainWindow.screen + } + /// Saves main window position and size and re-opens it in the same position and size next time MainWindowSavedState { window: mainWindow diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index 271ade825829..38a5db115bdc 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include "QGCLoggingCategory.h" #include "QGCLoggingCategoryManager.h" #include "QGCNetworkHelper.h" +#include "QGCPalette.h" #include "SettingsManager.h" #include "Vehicle.h" #include "VideoManager.h" @@ -53,7 +55,6 @@ QGCApplication::QGCApplication(int& argc, char* argv[], const QGCCommandLinePars : QGuiApplication(argc, argv), _runningUnitTests(cli.runningUnitTests), _simpleBootTest(cli.simpleBootTest), - _fakeMobile(cli.fakeMobile), _logOutput(cli.logOutput), _systemId(cli.systemId.value_or(0)) { @@ -183,16 +184,6 @@ void QGCApplication::setLanguage() if (possibleLocale != QLocale::AnyLanguage) { _locale = QLocale(possibleLocale); } - //-- We have specific fonts for Korean - if (_locale == QLocale::Korean) { - qCDebug(QGCApplicationLog) << "Loading Korean fonts" << _locale.name(); - if (QFontDatabase::addApplicationFont(":/fonts/NanumGothic-Regular") < 0) { - qCWarning(QGCApplicationLog) << "Could not load /fonts/NanumGothic-Regular font"; - } - if (QFontDatabase::addApplicationFont(":/fonts/NanumGothic-Bold") < 0) { - qCWarning(QGCApplicationLog) << "Could not load /fonts/NanumGothic-Bold font"; - } - } qCDebug(QGCApplicationLog) << "Loading localizations for" << _locale.name(); removeTranslator(JsonParsing::translator()); removeTranslator(&_qgcTranslatorSourceCode); @@ -216,6 +207,10 @@ void QGCApplication::setLanguage() } } + if (_applicationFontsLoaded) { + _setApplicationFont(); + } + if (_qmlAppEngine) { _qmlAppEngine->retranslate(); } @@ -235,15 +230,8 @@ void QGCApplication::init() LogManager::instance()->init(); - // Although this should really be in _initForNormalAppBoot putting it here allowws us to create unit tests which pop - // up more easily - if (QFontDatabase::addApplicationFont(":/fonts/opensans") < 0) { - qCWarning(QGCApplicationLog) << "Could not load /fonts/opensans font"; - } - - if (QFontDatabase::addApplicationFont(":/fonts/opensans-demibold") < 0) { - qCWarning(QGCApplicationLog) << "Could not load /fonts/opensans-demibold font"; - } + _loadApplicationFonts(); + _setApplicationFont(); if (_simpleBootTest) { // Since GStream builds are so problematic we initialize video during the simple boot test @@ -256,6 +244,49 @@ void QGCApplication::init() } } +void QGCApplication::_loadApplicationFonts() +{ + if (_applicationFontsLoaded) { + return; + } + + if (QFontDatabase::addApplicationFont(":/fonts/opensans") < 0) { + qCWarning(QGCApplicationLog) << "Could not load /fonts/opensans font"; + } + if (QFontDatabase::addApplicationFont(":/fonts/opensans-demibold") < 0) { + qCWarning(QGCApplicationLog) << "Could not load /fonts/opensans-demibold font"; + } + _applicationFontsLoaded = true; +} + +void QGCApplication::_loadKoreanFonts() +{ + if (_koreanFontsLoaded) { + return; + } + + if (QFontDatabase::addApplicationFont(":/fonts/NanumGothic-Regular") < 0) { + qCWarning(QGCApplicationLog) << "Could not load /fonts/NanumGothic-Regular font"; + } + if (QFontDatabase::addApplicationFont(":/fonts/NanumGothic-Bold") < 0) { + qCWarning(QGCApplicationLog) << "Could not load /fonts/NanumGothic-Bold font"; + } + + _koreanFontsLoaded = true; +} + +void QGCApplication::_setApplicationFont() +{ + if (_locale.language() == QLocale::Korean) { + _loadKoreanFonts(); + } + + QFont applicationFont = font(); + applicationFont.setFamily(_locale.language() == QLocale::Korean ? QStringLiteral("NanumGothic") + : QStringLiteral("Open Sans")); + setFont(applicationFont); +} + bool QGCApplication::_initVideo() { #ifdef QGC_GST_STREAMING @@ -272,8 +303,9 @@ bool QGCApplication::_initVideo() bool QGCApplication::_initQmlRootWindow() { - QQuickStyle::setStyle("Basic"); + QQuickStyle::setStyle("QGCStyle"); QGCCorePlugin::instance()->init(); + QGCPalette::initializeApplicationPalette(); MAVLinkProtocol::instance()->init(); MultiVehicleManager::instance()->init(); _qmlAppEngine = QGCCorePlugin::instance()->createQmlApplicationEngine(this); diff --git a/src/QGCApplication.h b/src/QGCApplication.h index 442fe1c38b88..d2858fc326d1 100644 --- a/src/QGCApplication.h +++ b/src/QGCApplication.h @@ -56,9 +56,6 @@ class QGCApplication : public QGuiApplication /// multiple times. void reportMissingParameter(int componentId, const QString &name); - /// @return true: Fake ui into showing mobile interface - bool fakeMobile() const { return _fakeMobile; } - void setLanguage(); QQuickWindow *mainRootWindow(); uint64_t msecsSinceBoot() const { return _msecsElapsedTime.elapsed(); } @@ -113,6 +110,9 @@ private slots: private: bool compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents) final; + void _loadApplicationFonts(); + void _loadKoreanFonts(); + void _setApplicationFont(); bool _initVideo(); @@ -126,7 +126,6 @@ private slots: bool _runningUnitTests = false; bool _simpleBootTest = false; - bool _fakeMobile = false; ///< true: Fake ui into displaying mobile interface bool _logOutput = false; ///< true: Log Qt debug output to file quint8 _systemId = 0; ///< MAVLink system ID, 0 means not set @@ -148,6 +147,8 @@ private slots: QElapsedTimer _msecsElapsedTime; bool _videoManagerInitialized = false; bool _bootTestPassed = true; + bool _applicationFontsLoaded = false; + bool _koreanFontsLoaded = false; QList> _delayedAppMessages; diff --git a/src/QmlControls/AppSettings.qml b/src/QmlControls/AppSettings.qml index 7de0a75c6c6c..9764f5a799eb 100644 --- a/src/QmlControls/AppSettings.qml +++ b/src/QmlControls/AppSettings.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls import QGroundControl.AppSettings +import QGCStyle as QGCStyle Rectangle { id: settingsView @@ -281,7 +282,7 @@ Rectangle { Layout.fillWidth: true padding: ScreenTools.defaultFontPixelWidth * 0.75 leftPadding: ScreenTools.defaultFontPixelWidth * 3 - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled property int sectionIndex: index property bool sectionChecked: pageColumn.isSelected && settingsView._selectedSectionIndex === sectionIndex diff --git a/src/QmlControls/CMakeLists.txt b/src/QmlControls/CMakeLists.txt index d3c2ff3421bd..78f468b053e2 100644 --- a/src/QmlControls/CMakeLists.txt +++ b/src/QmlControls/CMakeLists.txt @@ -58,8 +58,6 @@ target_sources(${CMAKE_PROJECT_NAME} RCChannelMonitorController.h RCToParamDialogController.cc RCToParamDialogController.h - ScreenToolsController.cc - ScreenToolsController.h TerrainProfile.cc TerrainProfile.h ToolStripAction.cc @@ -77,16 +75,22 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_ # ---------------------------------------------------------------------------- # QML Controls Module # ---------------------------------------------------------------------------- +add_subdirectory(QGCStyle) + qt_add_library(QGroundControlControlsModule STATIC) -set_source_files_properties(ScreenTools.qml PROPERTIES - QT_QML_SINGLETON_TYPE TRUE +set_source_files_properties( + QGCStyleEnvironment.qml + ScreenTools.qml + PROPERTIES QT_QML_SINGLETON_TYPE TRUE ) qt_add_qml_module(QGroundControlControlsModule URI QGroundControl.Controls VERSION 1.0 RESOURCE_PREFIX /qml + DEPENDENCIES + QGCStyle QML_FILES AltFrameCombo.qml AltFrameDialog.qml @@ -160,6 +164,7 @@ qt_add_qml_module(QGroundControlControlsModule QGCRoundButton.qml QGCSimpleMessageDialog.qml QGCSlider.qml + QGCStyleEnvironment.qml QGCSwipeView.qml QGCTabBar.qml QGCTabButton.qml diff --git a/src/QmlControls/HorizontalFactValueGrid.qml b/src/QmlControls/HorizontalFactValueGrid.qml index eb4a1589d2ad..82a51c37b261 100644 --- a/src/QmlControls/HorizontalFactValueGrid.qml +++ b/src/QmlControls/HorizontalFactValueGrid.qml @@ -109,7 +109,7 @@ HorizontalFactValueGridTemplate { leftPadding: 0 rightPadding: 0 text: qsTr("+") - enabled: (_root.width + (2 * (_rowButtonWidth + _margins))) < screen.width + enabled: (_root.width + (2 * (_rowButtonWidth + _margins))) < ScreenTools.screenWidth onClicked: appendColumn() } @@ -140,7 +140,7 @@ HorizontalFactValueGridTemplate { leftPadding: 0 rightPadding: 0 text: qsTr("+") - enabled: (_root.height + (2 * (_rowButtonHeight + _margins))) < (screen.height - ScreenTools.toolbarHeight) + enabled: (_root.height + (2 * (_rowButtonHeight + _margins))) < (ScreenTools.screenHeight - ScreenTools.toolbarHeight) onClicked: appendRow() } diff --git a/src/QmlControls/InstrumentValueValue.qml b/src/QmlControls/InstrumentValueValue.qml index 0e86235fa96e..5858a7c2653e 100644 --- a/src/QmlControls/InstrumentValueValue.qml +++ b/src/QmlControls/InstrumentValueValue.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle ColumnLayout { property var instrumentValueData: null @@ -25,6 +26,7 @@ ColumnLayout { id: label Layout.alignment: Qt.AlignVCenter font.pointSize: _fontSize + font.features: QGCStyle.StyleTypography.tabularNumberFeatures color: instrumentValueData.isValidColor(instrumentValueData.currentColor) ? instrumentValueData.currentColor : qgcPal.text text: valueText() diff --git a/src/QmlControls/QGCButton.qml b/src/QmlControls/QGCButton.qml index 768a11309416..f58a2220e0bd 100644 --- a/src/QmlControls/QGCButton.qml +++ b/src/QmlControls/QGCButton.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle /// Standard push button control: /// If there is both an icon and text the icon will be to the left of the text @@ -23,7 +24,7 @@ Button { property alias textColor: text.color id: control - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled topPadding: _verticalPadding bottomPadding: _verticalPadding leftPadding: _horizontalPadding diff --git a/src/QmlControls/QGCCheckBox.qml b/src/QmlControls/QGCCheckBox.qml index 4b6e3bdeae40..85229e27b233 100644 --- a/src/QmlControls/QGCCheckBox.qml +++ b/src/QmlControls/QGCCheckBox.qml @@ -3,11 +3,13 @@ import QtQuick.Controls import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle CheckBox { id: control spacing: _noText ? 0 : ScreenTools.defaultFontPixelWidth focusPolicy: Qt.ClickFocus + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled leftPadding: 0 Component.onCompleted: { @@ -32,8 +34,6 @@ CheckBox { } contentItem: Text { - //implicitWidth: _noText ? 0 : text.implicitWidth + ScreenTools.defaultFontPixelWidth * 0.25 - //implicitHeight: _noText ? 0 : Math.max(text.implicitHeight, ScreenTools.checkBoxIndicatorSize) leftPadding: control.indicator.width + control.spacing verticalAlignment: Text.AlignVCenter text: control.text diff --git a/src/QmlControls/QGCComboBox.qml b/src/QmlControls/QGCComboBox.qml index ac4d18a875a7..4a2891152385 100644 --- a/src/QmlControls/QGCComboBox.qml +++ b/src/QmlControls/QGCComboBox.qml @@ -5,6 +5,7 @@ import QtQuick.Templates as T import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle T.ComboBox { property bool sizeToContents: false @@ -96,7 +97,7 @@ T.ComboBox { anchors.rightMargin: control.padding anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - height: ScreenTools.defaultFontPixelWidth + height: QGCStyle.StyleMetrics.inlineIconSize width: height source: "/qmlimages/arrow-down.png" color: qgcPal.buttonText diff --git a/src/QmlControls/QGCDelayButton.qml b/src/QmlControls/QGCDelayButton.qml index 48a4a47a4746..3d54606653b9 100644 --- a/src/QmlControls/QGCDelayButton.qml +++ b/src/QmlControls/QGCDelayButton.qml @@ -4,10 +4,11 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle DelayButton { id: control - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled topPadding: _verticalPadding bottomPadding: _verticalPadding leftPadding: _horizontalPadding diff --git a/src/QmlControls/QGCLabel.qml b/src/QmlControls/QGCLabel.qml index f7bf259a0066..4cd01056aa0b 100644 --- a/src/QmlControls/QGCLabel.qml +++ b/src/QmlControls/QGCLabel.qml @@ -1,14 +1,5 @@ -import QtQuick import QtQuick.Controls -import QGroundControl -import QGroundControl.Controls - -Text { - font.pointSize: ScreenTools.defaultFontPointSize - font.family: ScreenTools.normalFontFamily - color: qgcPal.text - antialiasing: true - - QGCPalette { id: qgcPal; colorGroupEnabled: enabled } +Label { + antialiasing: true } diff --git a/src/QmlControls/QGCPalette.cc b/src/QmlControls/QGCPalette.cc index d667e33dbd70..b3d3f65ed066 100644 --- a/src/QmlControls/QGCPalette.cc +++ b/src/QmlControls/QGCPalette.cc @@ -2,6 +2,8 @@ #include "QGCCorePlugin.h" #include +#include +#include QList QGCPalette::_paletteObjects; @@ -16,7 +18,7 @@ QGCPalette::QGCPalette(QObject* parent) : _colorGroupEnabled(true) { if (_colorInfoMap.isEmpty()) { - _buildMap(); + initializeApplicationPalette(); } // We have to keep track of all QGCPalette objects in the system so we can signal theme change to all of them @@ -88,7 +90,15 @@ void QGCPalette::_buildMap() DECLARE_QGC_SINGLE_COLOR(mapMissionTrajectory, "#be781c") DECLARE_QGC_SINGLE_COLOR(surveyPolygonInterior, "green") DECLARE_QGC_SINGLE_COLOR(surveyPolygonTerrainCollision, "red") +} + +void QGCPalette::initializeApplicationPalette() +{ + if (_colorInfoMap.isEmpty()) { + _buildMap(); + } + _syncApplicationPalette(); } void QGCPalette::setColorGroupEnabled(bool enabled) @@ -102,10 +112,50 @@ void QGCPalette::setGlobalTheme(Theme newTheme) // Mobile build does not have themes if (_theme != newTheme) { _theme = newTheme; + _syncApplicationPalette(); _signalPaletteChangeToAll(); } } +void QGCPalette::_syncApplicationPalette() +{ + if (!QGuiApplication::instance() || _colorInfoMap.isEmpty()) { + return; + } + + QPalette palette = QGuiApplication::palette(); + const auto setColorGroup = [&palette](QPalette::ColorGroup paletteGroup, ColorGroup qgcGroup) { + const auto color = [qgcGroup](const QString& name) { return _colorInfoMap[_theme][qgcGroup][name]; }; + + palette.setColor(paletteGroup, QPalette::Window, color(QStringLiteral("window"))); + palette.setColor(paletteGroup, QPalette::WindowText, color(QStringLiteral("text"))); + palette.setColor(paletteGroup, QPalette::Base, color(QStringLiteral("textField"))); + palette.setColor(paletteGroup, QPalette::AlternateBase, color(QStringLiteral("windowShade"))); + palette.setColor(paletteGroup, QPalette::ToolTipBase, color(QStringLiteral("windowShade"))); + palette.setColor(paletteGroup, QPalette::ToolTipText, color(QStringLiteral("text"))); + palette.setColor(paletteGroup, QPalette::Text, color(QStringLiteral("textFieldText"))); + palette.setColor(paletteGroup, QPalette::Button, color(QStringLiteral("button"))); + palette.setColor(paletteGroup, QPalette::ButtonText, color(QStringLiteral("buttonText"))); + palette.setColor(paletteGroup, QPalette::BrightText, color(QStringLiteral("buttonHighlightText"))); + palette.setColor(paletteGroup, QPalette::Light, color(QStringLiteral("windowShadeLight"))); + palette.setColor(paletteGroup, QPalette::Midlight, color(QStringLiteral("windowShade"))); + palette.setColor(paletteGroup, QPalette::Mid, color(QStringLiteral("buttonBorder"))); + palette.setColor(paletteGroup, QPalette::Dark, color(QStringLiteral("windowShadeDark"))); + palette.setColor(paletteGroup, QPalette::Shadow, color(QStringLiteral("windowShadeDark"))); + palette.setColor(paletteGroup, QPalette::Highlight, color(QStringLiteral("buttonHighlight"))); + palette.setColor(paletteGroup, QPalette::HighlightedText, color(QStringLiteral("buttonHighlightText"))); + palette.setColor(paletteGroup, QPalette::Link, color(QStringLiteral("colorBlue"))); + palette.setColor(paletteGroup, QPalette::LinkVisited, color(QStringLiteral("brandingPurple"))); + palette.setColor(paletteGroup, QPalette::PlaceholderText, color(QStringLiteral("textFieldText"))); + palette.setColor(paletteGroup, QPalette::Accent, color(QStringLiteral("buttonHighlight"))); + }; + + setColorGroup(QPalette::Active, ColorGroupEnabled); + setColorGroup(QPalette::Inactive, ColorGroupEnabled); + setColorGroup(QPalette::Disabled, ColorGroupDisabled); + QGuiApplication::setPalette(palette); +} + void QGCPalette::_signalPaletteChangeToAll() { // Notify all objects of the new theme diff --git a/src/QmlControls/QGCPalette.h b/src/QmlControls/QGCPalette.h index 6588c1c3b948..889b23e485c3 100644 --- a/src/QmlControls/QGCPalette.h +++ b/src/QmlControls/QGCPalette.h @@ -59,7 +59,7 @@ c << _colorInfoMap[Dark][ColorGroupDisabled][QStringLiteral(#NAME)].name(QColor::HexRgb); \ return c; \ } \ - void SETNAME(const QColor& color) { _colorInfoMap[_theme][_colorGroupEnabled ? ColorGroupEnabled : ColorGroupDisabled][QStringLiteral(#NAME)] = color; _signalPaletteChangeToAll(); } + void SETNAME(const QColor& color) { _colorInfoMap[_theme][_colorGroupEnabled ? ColorGroupEnabled : ColorGroupDisabled][QStringLiteral(#NAME)] = color; _syncApplicationPalette(); _signalPaletteChangeToAll(); } /*! QGCPalette is used in QML ui to expose color properties for the QGC palette. There are two @@ -161,6 +161,7 @@ class QGCPalette : public QObject void setColorGroupEnabled (bool enabled); static Theme globalTheme () { return _theme; } + static void initializeApplicationPalette (); static void setGlobalTheme (Theme newTheme); signals: @@ -168,6 +169,7 @@ class QGCPalette : public QObject private: static void _buildMap (); + static void _syncApplicationPalette (); static void _signalPaletteChangeToAll (); void _signalPaletteChanged (); void _themeChanged (); diff --git a/src/QmlControls/QGCRadioButton.qml b/src/QmlControls/QGCRadioButton.qml index 3177b7fef01f..2b3c1843d6ab 100644 --- a/src/QmlControls/QGCRadioButton.qml +++ b/src/QmlControls/QGCRadioButton.qml @@ -3,11 +3,13 @@ import QtQuick.Controls import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle RadioButton { id: control font.family: ScreenTools.normalFontFamily font.pointSize: ScreenTools.defaultFontPointSize + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled property color textColor: qgcPal.text property bool _noText: text === "" diff --git a/src/QmlControls/QGCStyle/CMakeLists.txt b/src/QmlControls/QGCStyle/CMakeLists.txt new file mode 100644 index 000000000000..15baa6c668e9 --- /dev/null +++ b/src/QmlControls/QGCStyle/CMakeLists.txt @@ -0,0 +1,40 @@ +set(_qgc_style_config "${CMAKE_CURRENT_SOURCE_DIR}/qtquickcontrols2.conf") +qgc_set_qt_resource_alias("${_qgc_style_config}") + +qt_add_resources(${CMAKE_PROJECT_NAME} "qgcstyle_configuration" + PREFIX "/" + FILES "${_qgc_style_config}" +) + +qt_add_library(QGCStyle STATIC) + +set_source_files_properties( + StyleMetrics.qml + StylePreferences.qml + StyleTypography.qml + PROPERTIES QT_QML_SINGLETON_TYPE TRUE +) + +qt_add_qml_module(QGCStyle + URI QGCStyle + VERSION 1.0 + RESOURCE_PREFIX /qml + DEPENDENCIES + QtQuick + QML_FILES + LayoutProfile.qml + StyleKit/QGCStyleKit.qml + StyleMetrics.qml + StylePreferences.qml + StyleTypography.qml + Templates/ApplicationWindow.qml + Templates/Label.qml + SOURCES + impl/InputCapabilities.cc + impl/InputCapabilities.h + impl/SystemFonts.cc + impl/SystemFonts.h +) + +target_link_libraries(QGCStyle PRIVATE Qt6::Gui Qt6::LabsStyleKit) +target_include_directories(QGCStyle PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/impl") diff --git a/src/QmlControls/QGCStyle/LayoutProfile.qml b/src/QmlControls/QGCStyle/LayoutProfile.qml new file mode 100644 index 000000000000..28ef50d84577 --- /dev/null +++ b/src/QmlControls/QGCStyle/LayoutProfile.qml @@ -0,0 +1,24 @@ +import QtQml + +QtObject { + id: root + + required property real availableHeight + required property real availableWidth + required property real pixelDensity + + property bool mobileLayout: StylePreferences.platformTouchMode + + readonly property real _effectivePixelDensity: Math.max(root.pixelDensity, 1) + readonly property bool isShort: (root.availableHeight / root._effectivePixelDensity) < StyleMetrics.shortScreenMaximumHeightMillimeters + || (root.mobileLayout && root.availableWidth > 0 + && (root.availableHeight / root.availableWidth) < StyleMetrics.shortScreenMaximumAspectRatio) + readonly property bool isTiny: (root.availableWidth / root._effectivePixelDensity) < StyleMetrics.tinyScreenMaximumWidthMillimeters + readonly property real minimumTouchTarget: { + const physicalTouchTarget = Math.round(StyleMetrics.minimumTouchMillimeters * root._effectivePixelDensity) + return root.availableHeight > 0 + && physicalTouchTarget / root.availableHeight > StyleMetrics.maximumTouchTargetHeightRatio + ? StyleMetrics.fallbackTouchTargetHeight + : physicalTouchTarget + } +} diff --git a/src/QmlControls/QGCStyle/StyleKit/QGCStyleKit.qml b/src/QmlControls/QGCStyle/StyleKit/QGCStyleKit.qml new file mode 100644 index 000000000000..0afa109876af --- /dev/null +++ b/src/QmlControls/QGCStyle/StyleKit/QGCStyleKit.qml @@ -0,0 +1,222 @@ +pragma ComponentBehavior: Bound + +import Qt.labs.StyleKit as Labs + +import QtQuick + +Labs.Style { + id: rootStyle + + themeName: "QGCPalette" + + Labs.CustomTheme { + name: "QGCPalette" + theme: Labs.Theme { + } + } + + fonts { + system { + family: StyleTypography.normalFontFamily + pointSize: StyleTypography.bodyPointSize + } + } + + control { + checked.background.color: palette.mid + checked.indicator.foreground.visible: true + focused.background.border.color: palette.highlight + focused.background.border.width: StyleMetrics.focusBorderWidth + hovered.background.color: palette.midlight + padding: StyleMetrics.controlVerticalPadding + pressed.background.color: palette.mid + spacing: StyleMetrics.spacing + text.color: palette.buttonText + transition: Transition { + Labs.StyleAnimation { + animateColors: true + animateBackgroundBorder: true + duration: StyleMetrics.normalAnimationDuration + easing.type: Easing.OutCubic + } + } + + background { + border.color: palette.mid + border.width: StylePreferences.highContrast ? StyleMetrics.focusBorderWidth : StyleMetrics.borderWidth + color: palette.button + implicitHeight: StyleMetrics.controlHeight + implicitWidth: StyleMetrics.controlWidth + radius: StyleMetrics.radius + } + + handle { + border.color: palette.mid + border.width: StyleMetrics.borderWidth + color: palette.window + implicitHeight: StyleMetrics.indicatorSize + implicitWidth: StyleMetrics.indicatorSize + radius: StyleMetrics.indicatorSize / 2 + } + + indicator { + border.color: palette.mid + border.width: StyleMetrics.borderWidth + color: palette.base + foreground.color: palette.highlight + implicitHeight: StyleMetrics.indicatorSize + implicitWidth: StyleMetrics.indicatorSize + radius: StyleMetrics.radius + } + } + + Labs.StyleVariation { + name: "primary" + + control { + background.color: rootStyle.palette.highlight + checked.background.color: rootStyle.palette.highlight + hovered.background.color: Qt.lighter(rootStyle.palette.highlight, 1.05) + pressed.background.color: Qt.darker(rootStyle.palette.highlight, 1.1) + text.bold: true + text.color: rootStyle.palette.highlightedText + } + } + + Labs.StyleVariation { + name: "compact" + + control { + background.implicitHeight: StyleMetrics.toolbarIconSize + StyleMetrics.spacing + handle.implicitHeight: StyleMetrics.toolbarIconSize + handle.implicitWidth: StyleMetrics.toolbarIconSize + indicator.implicitHeight: StyleMetrics.toolbarIconSize + indicator.implicitWidth: StyleMetrics.toolbarIconSize + padding: StyleMetrics.smallSpacing + } + + button { + leftPadding: StyleMetrics.spacing + rightPadding: StyleMetrics.spacing + } + } + + button { + leftPadding: StyleMetrics.controlHorizontalPadding + rightPadding: StyleMetrics.controlHorizontalPadding + } + + checkBox { + background.visible: false + checked.indicator.foreground.visible: true + indicator.foreground.visible: false + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + comboBox { + background.implicitWidth: StyleMetrics.controlWidth + StyleMetrics.iconSize + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + itemDelegate { + background.border.width: 0 + background.color: "transparent" + background.radius: StyleMetrics.radius + hovered.background.color: palette.midlight + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + label { + background.visible: false + text.color: palette.windowText + } + + pane { + background.color: palette.window + background.radius: StyleMetrics.panelRadius + padding: StyleMetrics.contentMargin + } + + popup { + background.color: palette.window + background.implicitWidth: StyleMetrics.menuWidth + background.radius: StyleMetrics.panelRadius + padding: StyleMetrics.contentMargin + } + + progressBar { + background.visible: false + indicator.implicitHeight: StyleMetrics.progressBarHeight + indicator.implicitWidth: StyleMetrics.menuWidth + indicator.radius: StyleMetrics.progressBarHeight / 2 + } + + radioButton { + background.visible: false + checked.indicator.foreground.visible: true + indicator.foreground.margins: StyleMetrics.padding + indicator.foreground.radius: StyleMetrics.indicatorSize / 2 + indicator.foreground.visible: false + indicator.radius: StyleMetrics.indicatorSize / 2 + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + scrollBar { + background.implicitHeight: StyleMetrics.scrollBarExtent + background.visible: StylePreferences.highContrast + indicator.foreground.color: palette.mid + indicator.foreground.radius: StyleMetrics.scrollBarExtent / 2 + indicator.implicitHeight: StyleMetrics.scrollBarExtent + padding: StyleMetrics.borderWidth * 2 + + vertical { + background.implicitWidth: StyleMetrics.scrollBarExtent + indicator.implicitWidth: StyleMetrics.scrollBarExtent + } + } + + scrollIndicator { + background.implicitHeight: StyleMetrics.scrollBarExtent + background.visible: StylePreferences.highContrast + indicator.border.width: 0 + indicator.foreground.color: palette.mid + indicator.foreground.radius: StyleMetrics.scrollBarExtent / 2 + indicator.implicitHeight: StyleMetrics.scrollBarExtent + + vertical { + background.implicitWidth: StyleMetrics.scrollBarExtent + indicator.implicitWidth: StyleMetrics.scrollBarExtent + } + } + + slider { + background.visible: false + indicator.foreground.radius: StyleMetrics.progressBarHeight / 2 + indicator.implicitHeight: StyleMetrics.progressBarHeight + indicator.implicitWidth: Labs.Style.Stretch + indicator.radius: StyleMetrics.progressBarHeight / 2 + } + + switchControl { + background.visible: false + checked.indicator.foreground.color: palette.highlight + indicator.implicitHeight: StyleMetrics.switchHeight + indicator.implicitWidth: StyleMetrics.switchWidth + indicator.radius: StyleMetrics.switchHeight / 2 + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + } + + textArea { + background.color: palette.base + background.implicitHeight: StyleMetrics.controlHeight * 2 + background.implicitWidth: StyleMetrics.menuWidth + text.color: palette.text + } + + textField { + background.color: palette.base + background.implicitWidth: StyleMetrics.menuWidth + text.alignment: Qt.AlignVCenter | Qt.AlignLeft + text.color: palette.text + } +} diff --git a/src/QmlControls/QGCStyle/StyleMetrics.qml b/src/QmlControls/QGCStyle/StyleMetrics.qml new file mode 100644 index 000000000000..99dd0f512715 --- /dev/null +++ b/src/QmlControls/QGCStyle/StyleMetrics.qml @@ -0,0 +1,65 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + readonly property int animationDuration: normalAnimationDuration + readonly property real baseToolbarHeight: StyleTypography.bodyPixelHeight * 3 + readonly property int borderWidth: 1 + readonly property int busyIndicatorSize: Math.round((StylePreferences.touchMode ? 48 : 40) * StyleTypography.scaleFactor) + readonly property real checkBoxIndicatorSize: 2 * Math.floor(StyleTypography.bodyPixelHeight / 2) + 1 + readonly property real comboBoxPadding: StyleTypography.bodyPixelWidth + readonly property int contentMargin: Math.max(1, Math.round((StylePreferences.touchMode ? 16 : 12) * StyleTypography.scaleFactor)) + readonly property int controlContentHeight: Math.ceil(StyleTypography.bodyFontHeight) + readonly property int controlHeight: Math.max(minimumInteractiveHeight, controlContentHeight + controlVerticalPadding * 2) + readonly property int controlHorizontalPadding: contentMargin + readonly property int controlVerticalPadding: Math.max(1, Math.round(6 * StyleTypography.scaleFactor)) + readonly property int controlWidth: Math.round((StylePreferences.touchMode ? 120 : 100) * StyleTypography.scaleFactor) + readonly property real defaultBorderRadius: StyleTypography.bodyPixelWidth / 2 + readonly property real defaultDialogControlSpacing: Math.max(1, Math.round(StyleTypography.bodyLineSpacing / 2)) + readonly property int dialogPadding: contentMargin + readonly property real fallbackTouchTargetHeight: StyleTypography.bodyPixelHeight * 3 + readonly property int focusBorderWidth: 2 + readonly property int fastAnimationDuration: StylePreferences.animationsEnabled ? 100 : 0 + readonly property real hugeScreenMinimumWidthMillimeters: 23.5 * 25.4 + readonly property int iconSize: toolbarIconSize + readonly property real implicitButtonHeight: controlHeight + readonly property real implicitButtonWidth: Math.round(StyleTypography.bodyPixelWidth * 5) + readonly property real implicitCheckBoxHeight: Math.round(StyleTypography.bodyPixelHeight) + readonly property real implicitComboBoxHeight: implicitButtonHeight + readonly property real implicitComboBoxWidth: implicitButtonWidth + readonly property real implicitRadioButtonHeight: implicitCheckBoxHeight + readonly property real implicitSliderHeight: StyleTypography.bodyPixelHeight + readonly property real implicitTextFieldHeight: implicitButtonHeight + readonly property real implicitTextFieldWidth: StyleTypography.bodyPixelWidth * 10 + readonly property int indicatorSize: Math.round((StylePreferences.touchMode ? 32 : 28) * StyleTypography.scaleFactor) + readonly property int inlineIconSize: Math.max(1, Math.ceil(StyleTypography.bodyCapitalHeight)) + readonly property int largeIconSize: Math.round(32 * StyleTypography.scaleFactor) + readonly property real maximumTouchTargetHeightRatio: 0.15 + readonly property int menuWidth: Math.round(200 * StyleTypography.scaleFactor) + readonly property int minimumInteractiveHeight: Math.round((StylePreferences.touchMode ? 48 : 40) * StyleTypography.scaleFactor) + readonly property real minimumTouchMillimeters: 5 + readonly property int nominalTouchTarget: Math.round(48 * StyleTypography.scaleFactor) + readonly property int normalAnimationDuration: StylePreferences.animationsEnabled ? 150 : 0 + readonly property int padding: controlVerticalPadding + readonly property int panelRadius: Math.max(1, Math.round(6 * StyleTypography.scaleFactor)) + readonly property int progressBarHeight: Math.max(1, Math.round(6 * StyleTypography.scaleFactor)) + readonly property real radioButtonIndicatorSize: checkBoxIndicatorSize + readonly property int radius: Math.max(1, Math.round(4 * StyleTypography.scaleFactor)) + readonly property int scrollBarExtent: Math.max(1, Math.round((StylePreferences.touchMode ? 8 : 6) * StyleTypography.scaleFactor)) + readonly property int separatorHeight: Math.max(1, Math.round(StyleTypography.scaleFactor)) + readonly property real shortScreenMaximumAspectRatio: 0.6 + readonly property real shortScreenMaximumHeightMillimeters: 120 + readonly property int smallIconSize: Math.round(16 * StyleTypography.scaleFactor) + readonly property real smallSpacing: spacing / 2 + readonly property int slowAnimationDuration: StylePreferences.animationsEnabled ? 500 : 0 + readonly property int spacing: Math.max(1, Math.round(6 * StyleTypography.scaleFactor)) + readonly property int switchHeight: Math.round((StylePreferences.touchMode ? 32 : 28) * StyleTypography.scaleFactor) + readonly property int switchWidth: Math.round((StylePreferences.touchMode ? 64 : 56) * StyleTypography.scaleFactor) + readonly property real toolbarHeight: root.baseToolbarHeight * root.toolbarHeightMultiplier + property real toolbarHeightMultiplier: 1 + readonly property int toolbarIconSize: Math.round(24 * StyleTypography.scaleFactor) + readonly property real tinyScreenMaximumWidthMillimeters: 120 +} diff --git a/src/QmlControls/QGCStyle/StylePreferences.qml b/src/QmlControls/QGCStyle/StylePreferences.qml new file mode 100644 index 000000000000..87666adae38e --- /dev/null +++ b/src/QmlControls/QGCStyle/StylePreferences.qml @@ -0,0 +1,66 @@ +pragma Singleton + +import QtQuick + +import QGroundControl + +QtObject { + id: root + + enum TouchModePreference { + Automatic = -1, + Pointer = 0, + Touch = 1 + } + + readonly property var _appSettings: QGroundControl.settingsManager.appSettings + + readonly property bool animationsEnabled: !reducedMotion + readonly property bool detectedTouchMode: InputCapabilities.touchDeviceAvailable || platformTouchMode || touchInputObserved + readonly property bool forceHighContrast: _appSettings.forceHighContrast.rawValue + readonly property bool highContrast: forceHighContrast || Qt.styleHints.accessibility.contrastPreference === Qt.HighContrast + readonly property bool _hoverSuppressedByTouchMode: touchModeOverride === StylePreferences.Automatic + ? platformTouchMode + : touchModeOverride === StylePreferences.Touch + readonly property bool hoverEffectsEnabled: Qt.styleHints.useHoverEffects && !_hoverSuppressedByTouchMode + readonly property bool platformTouchMode: Qt.platform.os === "android" || Qt.platform.os === "ios" + readonly property int _rawTouchModeOverride: _appSettings.touchModeOverride.rawValue + readonly property bool reducedMotion: _appSettings.reducedMotion.rawValue + property bool touchInputObserved: false + readonly property bool touchMode: touchModeOverride === StylePreferences.Automatic ? detectedTouchMode : touchModeOverride === StylePreferences.Touch + readonly property int touchModeOverride: _isValidTouchModePreference(_rawTouchModeOverride) ? _rawTouchModeOverride : StylePreferences.Automatic + + Component.onCompleted: { + if (root._rawTouchModeOverride !== root.touchModeOverride) { + root._appSettings.touchModeOverride.rawValue = root.touchModeOverride + } + } + + function _isValidTouchModePreference(preference: int): bool { + return preference === StylePreferences.Automatic || preference === StylePreferences.Pointer || preference === StylePreferences.Touch + } + + function setForceHighContrast(enabled: bool) { + root._appSettings.forceHighContrast.rawValue = enabled + } + + function setReducedMotion(enabled: bool) { + root._appSettings.reducedMotion.rawValue = enabled + } + + function noteTouchInput() { + root.touchInputObserved = true + } + + function setTouchMode(enabled: bool) { + root.setTouchModePreference(enabled ? StylePreferences.Touch : StylePreferences.Pointer) + } + + function setTouchModePreference(preference: int) { + root._appSettings.touchModeOverride.rawValue = root._isValidTouchModePreference(preference) ? preference : StylePreferences.Automatic + } + + function useAutomaticTouchMode() { + root.setTouchModePreference(StylePreferences.Automatic) + } +} diff --git a/src/QmlControls/QGCStyle/StyleTypography.qml b/src/QmlControls/QGCStyle/StyleTypography.qml new file mode 100644 index 000000000000..bb754620cb6b --- /dev/null +++ b/src/QmlControls/QGCStyle/StyleTypography.qml @@ -0,0 +1,108 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + readonly property FontMetrics _bodyFontMetrics: FontMetrics { + font: root.bodyFont + } + readonly property TextMetrics _bodyTextMetrics: TextMetrics { + font: root.bodyFont + text: "X" + } + readonly property FontMetrics _captionFontMetrics: FontMetrics { + font: root.captionFont + } + readonly property TextMetrics _captionTextMetrics: TextMetrics { + font: root.captionFont + text: "X" + } + readonly property FontMetrics _headingFontMetrics: FontMetrics { + font: root.headingFont + } + readonly property FontInfo _fixedFontInfo: FontInfo { + font: SystemFonts.fixedFont + } + readonly property TextMetrics _headingTextMetrics: TextMetrics { + font: root.headingFont + text: "X" + } + readonly property FontMetrics _titleFontMetrics: FontMetrics { + font: root.titleFont + } + readonly property TextMetrics _titleTextMetrics: TextMetrics { + font: root.titleFont + text: "X" + } + readonly property font bodyFont: Qt.font({ + family: root.normalFontFamily, + pointSize: root.bodyPointSize + }) + readonly property real bodyAscent: _bodyFontMetrics.ascent + readonly property real bodyAverageCharacterWidth: _bodyFontMetrics.averageCharacterWidth + readonly property real bodyBaselineOffset: bodyAscent + readonly property real bodyCapitalHeight: _bodyFontMetrics.capitalHeight + readonly property real bodyDescent: _bodyFontMetrics.descent + readonly property real bodyFontHeight: _bodyFontMetrics.height + readonly property real bodyLineSpacing: _bodyFontMetrics.lineSpacing + readonly property real bodyPixelHeight: Math.round(_bodyFontMetrics.height / 2) * 2 + readonly property real bodyPixelWidth: Math.round(_bodyTextMetrics.advanceWidth / 2) * 2 + readonly property real bodyPointSize: platformPointSize * scaleFactor + readonly property font captionFont: Qt.font({ + family: root.normalFontFamily, + pointSize: root.captionPointSize + }) + readonly property real captionAscent: _captionFontMetrics.ascent + readonly property real captionBaselineOffset: captionAscent + readonly property real captionDescent: _captionFontMetrics.descent + readonly property real captionFontHeight: _captionFontMetrics.height + readonly property real captionLineSpacing: _captionFontMetrics.lineSpacing + readonly property real captionPixelHeight: Math.round(_captionFontMetrics.height / 2) * 2 + readonly property real captionPixelWidth: Math.round(_captionTextMetrics.advanceWidth / 2) * 2 + readonly property real captionPointSize: bodyPointSize * captionScale + readonly property real captionScale: 0.75 + readonly property font fixedFont: Qt.font({ + family: root.fixedFontFamily, + pointSize: root.bodyPointSize + }) + readonly property string fixedFontFamily: root._fixedFontInfo.family + readonly property font headingFont: Qt.font({ + family: root.normalFontFamily, + pointSize: root.headingPointSize, + weight: Font.DemiBold + }) + readonly property real headingAscent: _headingFontMetrics.ascent + readonly property real headingBaselineOffset: headingAscent + readonly property real headingDescent: _headingFontMetrics.descent + readonly property real headingFontHeight: _headingFontMetrics.height + readonly property real headingLineSpacing: _headingFontMetrics.lineSpacing + readonly property real headingPixelHeight: Math.round(_headingFontMetrics.height / 2) * 2 + readonly property real headingPixelWidth: Math.round(_headingTextMetrics.advanceWidth / 2) * 2 + readonly property real headingPointSize: bodyPointSize * headingScale + readonly property real headingScale: 1.25 + property string normalFontFamily: Qt.application.font.family + readonly property font numericFont: Qt.font({ + family: root.normalFontFamily, + features: root.tabularNumberFeatures, + pointSize: root.bodyPointSize + }) + property real platformPointSize: Qt.application.font.pointSize + property real scaleFactor: 1 + readonly property var tabularNumberFeatures: ({ "tnum": 1 }) + readonly property font titleFont: Qt.font({ + family: root.normalFontFamily, + pointSize: root.titlePointSize, + weight: Font.DemiBold + }) + readonly property real titleAscent: _titleFontMetrics.ascent + readonly property real titleBaselineOffset: titleAscent + readonly property real titleDescent: _titleFontMetrics.descent + readonly property real titleFontHeight: _titleFontMetrics.height + readonly property real titleLineSpacing: _titleFontMetrics.lineSpacing + readonly property real titlePixelHeight: Math.round(_titleFontMetrics.height / 2) * 2 + readonly property real titlePixelWidth: Math.round(_titleTextMetrics.advanceWidth / 2) * 2 + readonly property real titlePointSize: bodyPointSize * titleScale + readonly property real titleScale: 1.5 +} diff --git a/src/QmlControls/QGCStyle/Templates/ApplicationWindow.qml b/src/QmlControls/QGCStyle/Templates/ApplicationWindow.qml new file mode 100644 index 000000000000..8449069c77ed --- /dev/null +++ b/src/QmlControls/QGCStyle/Templates/ApplicationWindow.qml @@ -0,0 +1,30 @@ +import Qt.labs.StyleKit as Labs +import QtQuick +import QtQuick.Templates as T + +T.ApplicationWindow { + id: root + + Labs.StyleKit.style: qgcStyle + Labs.StyleKit.transitionsEnabled: StylePreferences.animationsEnabled + color: palette.window + font: StyleTypography.bodyFont + + PointHandler { + id: touchInputObserver + + target: null + acceptedDevices: PointerDevice.TouchScreen + + onActiveChanged: { + if (touchInputObserver.active) { + InputCapabilities.refresh() + StylePreferences.noteTouchInput() + } + } + } + + QGCStyleKit { + id: qgcStyle + } +} diff --git a/src/QmlControls/QGCStyle/Templates/Label.qml b/src/QmlControls/QGCStyle/Templates/Label.qml new file mode 100644 index 000000000000..a09374233327 --- /dev/null +++ b/src/QmlControls/QGCStyle/Templates/Label.qml @@ -0,0 +1,6 @@ +import QtQuick.Templates as T + +T.Label { + color: palette.windowText + linkColor: palette.link +} diff --git a/src/QmlControls/QGCStyle/impl/InputCapabilities.cc b/src/QmlControls/QGCStyle/impl/InputCapabilities.cc new file mode 100644 index 000000000000..1e90cc659474 --- /dev/null +++ b/src/QmlControls/QGCStyle/impl/InputCapabilities.cc @@ -0,0 +1,25 @@ +#include "InputCapabilities.h" + +#include + +InputCapabilities::InputCapabilities(QObject* parent) : QObject(parent) +{ + refresh(); +} + +void InputCapabilities::refresh() +{ + bool touchDeviceAvailable = false; + + for (const auto* const inputDevice : QInputDevice::devices()) { + if (inputDevice->type() == QInputDevice::DeviceType::TouchScreen) { + touchDeviceAvailable = true; + (void) connect(inputDevice, &QObject::destroyed, this, &InputCapabilities::refresh, Qt::UniqueConnection); + } + } + + if (_touchDeviceAvailable != touchDeviceAvailable) { + _touchDeviceAvailable = touchDeviceAvailable; + emit touchDeviceAvailableChanged(); + } +} diff --git a/src/QmlControls/QGCStyle/impl/InputCapabilities.h b/src/QmlControls/QGCStyle/impl/InputCapabilities.h new file mode 100644 index 000000000000..3be8d38062c7 --- /dev/null +++ b/src/QmlControls/QGCStyle/impl/InputCapabilities.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +class InputCapabilities : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(bool touchDeviceAvailable READ touchDeviceAvailable NOTIFY touchDeviceAvailableChanged FINAL) + +public: + explicit InputCapabilities(QObject* parent = nullptr); + ~InputCapabilities() override = default; + + bool touchDeviceAvailable() const { return _touchDeviceAvailable; } + + Q_INVOKABLE void refresh(); + +signals: + void touchDeviceAvailableChanged(); + +private: + bool _touchDeviceAvailable = false; +}; diff --git a/src/QmlControls/QGCStyle/impl/SystemFonts.cc b/src/QmlControls/QGCStyle/impl/SystemFonts.cc new file mode 100644 index 000000000000..b45d07c19530 --- /dev/null +++ b/src/QmlControls/QGCStyle/impl/SystemFonts.cc @@ -0,0 +1,10 @@ +#include "SystemFonts.h" + +#include + +SystemFonts::SystemFonts(QObject* parent) : QObject(parent) {} + +QFont SystemFonts::fixedFont() +{ + return QFontDatabase::systemFont(QFontDatabase::FixedFont); +} diff --git a/src/QmlControls/QGCStyle/impl/SystemFonts.h b/src/QmlControls/QGCStyle/impl/SystemFonts.h new file mode 100644 index 000000000000..d235c238ec63 --- /dev/null +++ b/src/QmlControls/QGCStyle/impl/SystemFonts.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +class SystemFonts : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_PROPERTY(QFont fixedFont READ fixedFont CONSTANT FINAL) + +public: + explicit SystemFonts(QObject* parent = nullptr); + ~SystemFonts() override = default; + + static QFont fixedFont(); +}; diff --git a/src/QmlControls/QGCStyle/qtquickcontrols2.conf b/src/QmlControls/QGCStyle/qtquickcontrols2.conf new file mode 100644 index 000000000000..fe725b8c1b77 --- /dev/null +++ b/src/QmlControls/QGCStyle/qtquickcontrols2.conf @@ -0,0 +1,3 @@ +[Controls] +Style=QGCStyle +FallbackStyle=Basic diff --git a/src/QmlControls/QGCStyleEnvironment.qml b/src/QmlControls/QGCStyleEnvironment.qml new file mode 100644 index 000000000000..753962869726 --- /dev/null +++ b/src/QmlControls/QGCStyleEnvironment.qml @@ -0,0 +1,40 @@ +pragma Singleton + +import QtQml + +import QGroundControl +import QGCStyle as QGCStyle + +QtObject { + id: root + + readonly property var _uiScalePercentFact: QGroundControl.settingsManager.appSettings.uiScalePercent + readonly property real _validatedUiScalePercent: { + const value = Number(root._uiScalePercentFact.value) + return value >= root._uiScalePercentFact.min && value <= root._uiScalePercentFact.max ? value : 100 + } + + readonly property Binding _platformPointSizeBinding: Binding { + target: QGCStyle.StyleTypography + property: "platformPointSize" + value: ScreenTools.platformFontPointSize + } + + readonly property Binding _scaleFactorBinding: Binding { + target: QGCStyle.StyleTypography + property: "scaleFactor" + value: root._validatedUiScalePercent / 100 + } + + readonly property Binding _toolbarHeightMultiplierBinding: Binding { + target: QGCStyle.StyleMetrics + property: "toolbarHeightMultiplier" + value: QGroundControl.corePlugin.options.toolbarHeightMultiplier + } + + Component.onCompleted: { + if (Number(root._uiScalePercentFact.value) !== root._validatedUiScalePercent) { + root._uiScalePercentFact.value = root._validatedUiScalePercent + } + } +} diff --git a/src/QmlControls/QGCTextField.qml b/src/QmlControls/QGCTextField.qml index ed6969595865..1dee83e1e6e5 100644 --- a/src/QmlControls/QGCTextField.qml +++ b/src/QmlControls/QGCTextField.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle TextField { id: control @@ -14,6 +15,7 @@ TextField { antialiasing: true font.pointSize: ScreenTools.defaultFontPointSize font.family: ScreenTools.normalFontFamily + font.features: numericValuesOnly ? QGCStyle.StyleTypography.tabularNumberFeatures : ({}) inputMethodHints: numericValuesOnly && !ScreenTools.isiOS ? Qt.ImhFormattedNumbersOnly: // Forces use of virtual numeric keyboard instead of full keyboard Qt.ImhNone // iOS numeric keyboard has no done button, we can't use it. diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index f6b9d260e3cb..4f0ba45b121e 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -91,6 +91,7 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(QString qgcVersion READ qgcVersion CONSTANT) Q_PROPERTY(QString qgcAppDate READ qgcAppDate CONSTANT) Q_PROPERTY(bool qgcDailyBuild READ qgcDailyBuild CONSTANT) + Q_PROPERTY(bool isDebugBuild READ isDebugBuild CONSTANT) Q_PROPERTY(qreal zOrderTopMost READ zOrderTopMost CONSTANT) ///< z order for top most items, toolbar, main window sub view Q_PROPERTY(qreal zOrderWidgets READ zOrderWidgets CONSTANT) ///< z order value to widgets, for example: zoom controls, hud widgetss @@ -207,6 +208,11 @@ class QGroundControlQmlGlobal : public QObject #else static bool qgcDailyBuild() { return false; } #endif +#ifdef QT_DEBUG + static bool isDebugBuild() { return true; } +#else + static bool isDebugBuild() { return false; } +#endif signals: void isMultiplexingEnabledChanged (bool enabled); diff --git a/src/QmlControls/ScreenTools.qml b/src/QmlControls/ScreenTools.qml index 3ceaba2305c1..f6d14c0c8eb1 100644 --- a/src/QmlControls/ScreenTools.qml +++ b/src/QmlControls/ScreenTools.qml @@ -1,215 +1,131 @@ pragma Singleton import QtQuick -import QtQuick.Controls -import QtQuick.Window import QGroundControl +import QGCStyle as QGCStyle /*! - The ScreenTools Singleton provides information on QGC's standard font metrics. It also provides information on screen - size which can be used to adjust user interface for varying available screen real estate. - - QGC has four standard font sizes: default, small, medium and large. The QGC controls use the default font for display and you should use this font - for most text within the system that is drawn using something other than a standard QGC control. The small font is smaller than the default font. - The medium and large fonts are larger than the default font. + ScreenTools provides QGC-specific platform and display information. Its legacy font and control metrics remain as + compatibility aliases. New visual components should use QGCStyle.StyleTypography and QGCStyle.StyleMetrics directly. Usage: - import QGroundControl.Controls + import QtQuick + import QGCStyle + Rectangle { - anchors.fill: parent - anchors.margins: ScreenTools.defaultFontPixelWidth + anchors.fill: parent + anchors.margins: StyleMetrics.contentMargin ... } */ -Item { - id: _screenTools - - //-- The point and pixel font size values are computed at runtime - - property real defaultFontPointSize: 10 - property real platformFontPointSize: 10 +QtObject { + id: root + + readonly property real defaultFontPointSize: QGCStyle.StyleTypography.bodyPointSize + readonly property real platformFontPointSize: { + const applicationPointSize = Qt.application.font.pointSize > 0 ? Qt.application.font.pointSize : 10 + if (!root.isMobile) { + return applicationPointSize + } + if (root.isiOS) { + return root.screenWidth < 570 ? 12 : 14 + } + return (root.screenWidth / root.realPixelDensity) < QGCStyle.StyleMetrics.tinyScreenMaximumWidthMillimeters ? 11 : 14 + } - readonly property real smallFontPointRatio: 0.75 - readonly property real mediumFontPointRatio: 1.25 - readonly property real largeFontPointRatio: 1.5 + readonly property real smallFontPointRatio: QGCStyle.StyleTypography.captionScale + readonly property real mediumFontPointRatio: QGCStyle.StyleTypography.headingScale + readonly property real largeFontPointRatio: QGCStyle.StyleTypography.titleScale /// You can use these properties to position ui elements in a screen resolution independent manner. Using fixed positioning values should not /// be done. All positioning should be done using anchors or a ratio of the defaultFontPixelHeight and defaultFontPixelWidth values. This way /// your ui elements will reposition themselves appropriately on varying screen sizes and resolutions. - property real defaultFontPixelHeight: 10 - property real largeFontPixelHeight: defaultFontPixelHeight * largeFontPointRatio - property real mediumFontPixelHeight: defaultFontPixelHeight * mediumFontPointRatio - property real smallFontPixelHeight: defaultFontPixelHeight * smallFontPointRatio + readonly property real defaultFontPixelHeight: QGCStyle.StyleTypography.bodyPixelHeight + readonly property real largeFontPixelHeight: QGCStyle.StyleTypography.titlePixelHeight /// You can use these properties to position ui elements in a screen resolution independent manner. Using fixed positioning values should not /// be done. All positioning should be done using anchors or a ratio of the defaultFontPixelHeight and defaultFontPixelWidth values. This way /// your ui elements will reposition themselves appropriately on varying screen sizes and resolutions. - property real defaultFontPixelWidth: 10 - property real largeFontPixelWidth: defaultFontPixelWidth * largeFontPointRatio - property real mediumFontPixelWidth: defaultFontPixelWidth * mediumFontPointRatio - property real smallFontPixelWidth: defaultFontPixelWidth * smallFontPointRatio + readonly property real defaultFontPixelWidth: QGCStyle.StyleTypography.bodyPixelWidth + readonly property real largeFontPixelWidth: QGCStyle.StyleTypography.titlePixelWidth /// QFontMetrics::descent for default font at default point size - property real defaultFontDescent: 0 + readonly property real defaultFontDescent: QGCStyle.StyleTypography.bodyDescent /// The default amount of space in between controls in a dialog - property real defaultDialogControlSpacing: defaultFontPixelHeight / 2 + readonly property real defaultDialogControlSpacing: QGCStyle.StyleMetrics.defaultDialogControlSpacing - property real smallFontPointSize: 10 - property real mediumFontPointSize: 10 - property real largeFontPointSize: 10 + readonly property real smallFontPointSize: QGCStyle.StyleTypography.captionPointSize + readonly property real mediumFontPointSize: QGCStyle.StyleTypography.headingPointSize + readonly property real largeFontPointSize: QGCStyle.StyleTypography.titlePointSize - property real toolbarHeight: 0 + readonly property real toolbarHeight: QGCStyle.StyleMetrics.toolbarHeight - property real realPixelDensity: { - //-- If a plugin defines it, just use what it tells us - if(QGroundControl.corePlugin.options.devicePixelDensity != 0) { - return QGroundControl.corePlugin.options.devicePixelDensity + property var _windowScreen: null + readonly property var _screen: root._windowScreen ?? (Qt.application.screens.length > 0 ? Qt.application.screens[0] : null) + readonly property real _screenPixelDensity: { + const density = root._screen ? root._screen.pixelDensity : 0 + return Number.isFinite(density) && density > 0 ? density : 1 + } + readonly property real realPixelDensity: { + //-- If a plugin defines a valid override, use what it tells us + const configuredPixelDensity = QGroundControl.corePlugin.options.devicePixelDensity + if (Number.isFinite(configuredPixelDensity) && configuredPixelDensity > 0) { + return configuredPixelDensity } //-- Android is rather unreliable - if(isAndroid) { + if (root.isAndroid) { // Lets assume it's unlikely you have a tablet over 300mm wide - if((Screen.width / Screen.pixelDensity) > 300) { - return Screen.pixelDensity * 2 + if ((root.screenWidth / root._screenPixelDensity) > 300) { + return root._screenPixelDensity * 2 } } //-- Let's use what the system tells us - return Screen.pixelDensity + return root._screenPixelDensity } // These properties allow us to create simulated mobile sizing for a desktop build. // This makes testing the UI for smaller mobile sizing much easier. // The 731x411 size is the size of the Herelink screen which is our target lower bound - property real screenWidth: ScreenToolsController.fakeMobile ? 731 : Screen.width - property real screenHeight: ScreenToolsController.fakeMobile ? 411 : Screen.height - - property bool isAndroid: ScreenToolsController.isAndroid - property bool isiOS: ScreenToolsController.isiOS - property bool isMobile: ScreenToolsController.isMobile - property bool isFakeMobile: ScreenToolsController.fakeMobile - property bool isWindows: ScreenToolsController.isWindows - property bool isDebug: ScreenToolsController.isDebug - property bool isMac: ScreenToolsController.isMacOS - property bool isLinux: ScreenToolsController.isLinux - property bool isTinyScreen: (Screen.width / realPixelDensity) < 120 // 120mm - property bool isShortScreen: ((Screen.height / realPixelDensity) < 120) || (ScreenToolsController.isMobile && ((Screen.height / Screen.width) < 0.6)) - property bool isHugeScreen: (Screen.width / realPixelDensity) >= (23.5 * 25.4) // 27" monitor - property bool isSerialAvailable: ScreenToolsController.isSerialAvailable - - readonly property real minTouchMillimeters: 5 ///< Minimum touch size in millimeters - property real minTouchPixels: 0 ///< Minimum touch size in pixels (calculatedd from minTouchMillimeters and realPixelDensity) - - // The implicit heights/widths for our custom control set - property real implicitButtonWidth: Math.round(defaultFontPixelWidth * 5.0) - property real implicitButtonHeight: Math.round(defaultFontPixelHeight * 1.6) - property real implicitCheckBoxHeight: Math.round(defaultFontPixelHeight * 1.0) - property real implicitRadioButtonHeight: implicitCheckBoxHeight - property real implicitTextFieldWidth: defaultFontPixelWidth * 10 - property real implicitTextFieldHeight: implicitButtonHeight - property real implicitComboBoxHeight: implicitButtonHeight - property real implicitComboBoxWidth: implicitButtonWidth - property real comboBoxPadding: defaultFontPixelWidth - property real implicitSliderHeight: defaultFontPixelHeight - property real defaultBorderRadius: defaultFontPixelWidth / 2 - - // It's not possible to centralize an even number of pixels, checkBoxIndicatorSize should be an odd number to allow centralization - property real checkBoxIndicatorSize: 2 * Math.floor(defaultFontPixelHeight / 2) + 1 - property real radioButtonIndicatorSize: checkBoxIndicatorSize - - readonly property string normalFontFamily: ScreenToolsController.normalFontFamily - readonly property string fixedFontFamily: ScreenToolsController.fixedFontFamily - /* This mostly works but for some reason, reflowWidths() in SetupView doesn't change size. - I've disabled (in release builds) until I figure out why. Changes require a restart for now. - */ - Connections { - target: QGroundControl.settingsManager.appSettings.uiScalePercent - function onValueChanged() { - var pct = QGroundControl.settingsManager.appSettings.uiScalePercent.value - _setBasePointSize(platformFontPointSize * pct / 100) - } + readonly property real screenWidth: root.isFakeMobile ? 731 : (root._screen ? root._screen.width : 0) + readonly property real screenHeight: root.isFakeMobile ? 411 : (root._screen ? root._screen.height : 0) + + readonly property QGCStyle.LayoutProfile _layoutProfile: QGCStyle.LayoutProfile { + availableHeight: root.screenHeight + availableWidth: root.screenWidth + mobileLayout: root.isMobile + pixelDensity: root.realPixelDensity } - onRealPixelDensityChanged: { - _setBasePointSize(defaultFontPointSize) - } + readonly property string _platformOs: Qt.platform.os + readonly property bool isAndroid: root._platformOs === "android" + readonly property bool isiOS: root._platformOs === "ios" + readonly property bool isFakeMobile: !root.isAndroid && !root.isiOS + && (Qt.application.arguments.includes("--fake-mobile") + || Qt.application.arguments.includes("-fake-mobile")) + readonly property bool isMobile: root.isAndroid || root.isiOS || root.isFakeMobile + readonly property bool isTinyScreen: root._layoutProfile.isTiny + readonly property bool isShortScreen: root._layoutProfile.isShort - function printScreenStats() { - console.log('ScreenTools: Screen.width: ' + Screen.width + ' Screen.height: ' + Screen.height + ' Screen.pixelDensity: ' + Screen.pixelDensity) - } + readonly property real minTouchMillimeters: QGCStyle.StyleMetrics.minimumTouchMillimeters + readonly property real minTouchPixels: root._layoutProfile.minimumTouchTarget - /// Returns the current x position of the mouse in global screen coordinates. - function mouseX() { - return ScreenToolsController.mouseX() - } - - /// Returns the current y position of the mouse in global screen coordinates. - function mouseY() { - return ScreenToolsController.mouseY() - } - - /// \private - function _setBasePointSize(pointSize) { - _textMeasure.font.pointSize = pointSize - defaultFontPointSize = pointSize - defaultFontPixelHeight = Math.round(_textMeasure.fontHeight/2.0)*2 - defaultFontPixelWidth = Math.round(_textMeasure.fontWidth/2.0)*2 - defaultFontDescent = ScreenToolsController.defaultFontDescent(defaultFontPointSize) - smallFontPointSize = defaultFontPointSize * _screenTools.smallFontPointRatio - mediumFontPointSize = defaultFontPointSize * _screenTools.mediumFontPointRatio - largeFontPointSize = defaultFontPointSize * _screenTools.largeFontPointRatio - minTouchPixels = Math.round(minTouchMillimeters * realPixelDensity) - if (minTouchPixels / Screen.height > 0.15) { - // If using physical sizing takes up too much of the vertical real estate fall back to font based sizing - minTouchPixels = defaultFontPixelHeight * 3 - } - toolbarHeight = defaultFontPixelHeight * 3 - toolbarHeight = toolbarHeight * QGroundControl.corePlugin.options.toolbarHeightMultiplier - } - - Text { - id: _defaultFont - text: "X" - } - - Text { - id: _textMeasure - text: "X" - font.family: normalFontFamily - property real fontWidth: contentWidth - property real fontHeight: contentHeight - Component.onCompleted: { - //-- First, compute platform, default size - if(ScreenToolsController.isMobile) { - //-- Check iOS really tiny screens (iPhone 4s/5/5s) - if(ScreenToolsController.isiOS) { - if(ScreenToolsController.isiOS && Screen.width < 570) { - // For iPhone 4s size we don't fit with additional tweaks to fit screen, - // we will just drop point size to make things fit. Correct size not yet determined. - platformFontPointSize = 12; // This will be lowered in a future pull - } else { - platformFontPointSize = 14; - } - } else if((Screen.width / realPixelDensity) < 120) { - platformFontPointSize = 11; - // Other Android - } else { - platformFontPointSize = 14; - } - } else { - platformFontPointSize = _defaultFont.font.pointSize; - } - //-- See if we are using a custom size - var _uiScalePercentFact = QGroundControl.settingsManager.appSettings.uiScalePercent - var pct = _uiScalePercentFact.value - //-- Sanity check - if(pct < _uiScalePercentFact.min || pct > _uiScalePercentFact.max) { - pct = 100; - _uiScalePercentFact.value = pct - } - //-- Set size saved in settings - _screenTools._setBasePointSize(platformFontPointSize * pct / 100); - } - } + // The implicit heights/widths for our custom control set + readonly property real implicitButtonWidth: QGCStyle.StyleMetrics.implicitButtonWidth + readonly property real implicitButtonHeight: QGCStyle.StyleMetrics.implicitButtonHeight + readonly property real implicitCheckBoxHeight: QGCStyle.StyleMetrics.implicitCheckBoxHeight + readonly property real implicitTextFieldWidth: QGCStyle.StyleMetrics.implicitTextFieldWidth + readonly property real implicitTextFieldHeight: QGCStyle.StyleMetrics.implicitTextFieldHeight + readonly property real implicitComboBoxHeight: QGCStyle.StyleMetrics.implicitComboBoxHeight + readonly property real implicitComboBoxWidth: QGCStyle.StyleMetrics.implicitComboBoxWidth + readonly property real comboBoxPadding: QGCStyle.StyleMetrics.comboBoxPadding + readonly property real implicitSliderHeight: QGCStyle.StyleMetrics.implicitSliderHeight + readonly property real defaultBorderRadius: QGCStyle.StyleMetrics.defaultBorderRadius + + readonly property real radioButtonIndicatorSize: QGCStyle.StyleMetrics.radioButtonIndicatorSize + + readonly property string normalFontFamily: QGCStyle.StyleTypography.normalFontFamily + readonly property string fixedFontFamily: QGCStyle.StyleTypography.fixedFontFamily } diff --git a/src/QmlControls/ScreenToolsController.cc b/src/QmlControls/ScreenToolsController.cc deleted file mode 100644 index c0ac350a627c..000000000000 --- a/src/QmlControls/ScreenToolsController.cc +++ /dev/null @@ -1,86 +0,0 @@ -#include "ScreenToolsController.h" -#include "QGCApplication.h" -#include "QGCLoggingCategory.h" -#include "SettingsManager.h" -#include "AppSettings.h" - -#include -#include -#include -#include - -#if defined(Q_OS_IOS) -#include -#endif - -QGC_LOGGING_CATEGORY(ScreenToolsControllerLog, "QMLControls.ScreenToolsController") - -ScreenToolsController::ScreenToolsController(QObject *parent) - : QObject(parent) -{ - // qCDebug(ScreenToolsControllerLog) << Q_FUNC_INFO << this; -} - -ScreenToolsController::~ScreenToolsController() -{ - // qCDebug(ScreenToolsControllerLog) << Q_FUNC_INFO << this; -} - -int ScreenToolsController::mouseX() -{ - return QCursor::pos().x(); -} - -int ScreenToolsController::mouseY() -{ - return QCursor::pos().y(); -} - -bool ScreenToolsController::hasTouch() -{ - for (const auto &inputDevice: QInputDevice::devices()) { - if (inputDevice->type() == QInputDevice::DeviceType::TouchScreen) { - return true; - } - } - return false; -} - -QString ScreenToolsController::iOSDevice() -{ -#if defined(Q_OS_IOS) - struct utsname systemInfo; - uname(&systemInfo); - return QString(systemInfo.machine); -#else - return QString(); -#endif -} - -QString ScreenToolsController::fixedFontFamily() -{ - return QFontDatabase::systemFont(QFontDatabase::FixedFont).family(); -} - -QString ScreenToolsController::normalFontFamily() -{ - //-- See App.SettinsGroup.json for index - const int langID = SettingsManager::instance()->appSettings()->qLocaleLanguage()->rawValue().toInt(); - if (langID == QLocale::Korean) { - return QStringLiteral("NanumGothic"); - } - - return QStringLiteral("Open Sans"); -} - -double ScreenToolsController::defaultFontDescent(int pointSize) -{ - return QFontMetrics(QFont(normalFontFamily(), pointSize)).descent(); -} - -#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) -bool ScreenToolsController::fakeMobile() -{ - return qgcApp()->fakeMobile(); -} -#endif diff --git a/src/QmlControls/ScreenToolsController.h b/src/QmlControls/ScreenToolsController.h deleted file mode 100644 index 7e6d3299acb8..000000000000 --- a/src/QmlControls/ScreenToolsController.h +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -#include -#include - -/// \brief This Qml control is used to return screen parameters -/// -class ScreenToolsController : public QObject -{ - Q_OBJECT - QML_ELEMENT - QML_SINGLETON - Q_PROPERTY(bool isAndroid READ isAndroid CONSTANT) - Q_PROPERTY(bool isiOS READ isiOS CONSTANT) - Q_PROPERTY(bool isMobile READ isMobile CONSTANT) - Q_PROPERTY(bool fakeMobile READ fakeMobile CONSTANT) - Q_PROPERTY(bool isDebug READ isDebug CONSTANT) - Q_PROPERTY(bool isMacOS READ isMacOS CONSTANT) - Q_PROPERTY(bool isLinux READ isLinux CONSTANT) - Q_PROPERTY(bool isWindows READ isWindows CONSTANT) - Q_PROPERTY(bool isSerialAvailable READ isSerialAvailable CONSTANT) - Q_PROPERTY(bool hasTouch READ hasTouch CONSTANT) - Q_PROPERTY(QString iOSDevice READ iOSDevice CONSTANT) - Q_PROPERTY(QString fixedFontFamily READ fixedFontFamily CONSTANT) - Q_PROPERTY(QString normalFontFamily READ normalFontFamily CONSTANT) - -public: - explicit ScreenToolsController(QObject *parent = nullptr); - ~ScreenToolsController(); - - /// Returns current mouse position - Q_INVOKABLE static int mouseX(); - Q_INVOKABLE static int mouseY(); - - // QFontMetrics::descent for default font - Q_INVOKABLE static double defaultFontDescent(int pointSize); - -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - static bool isMobile() { return true; } - static bool fakeMobile() { return false; } -#else - static bool isMobile() { return fakeMobile(); } - static bool fakeMobile(); -#endif - -#if defined (Q_OS_ANDROID) - static bool isAndroid() { return true; } - static bool isiOS() { return false; } - static bool isLinux() { return false; } - static bool isMacOS() { return false; } - static bool isWindows() { return false; } -#elif defined(Q_OS_IOS) - static bool isAndroid() { return false; } - static bool isiOS() { return true; } - static bool isLinux() { return false; } - static bool isMacOS() { return false; } - static bool isWindows() { return false; } -#elif defined(Q_OS_MACOS) - static bool isAndroid() { return false; } - static bool isiOS() { return false; } - static bool isLinux() { return false; } - static bool isMacOS() { return true; } - static bool isWindows() { return false; } -#elif defined(Q_OS_LINUX) - static bool isAndroid() { return false; } - static bool isiOS() { return false; } - static bool isLinux() { return true; } - static bool isMacOS() { return false; } - static bool isWindows() { return false; } -#elif defined(Q_OS_WIN) - static bool isAndroid() { return false; } - static bool isiOS() { return false; } - static bool isLinux() { return false; } - static bool isMacOS() { return false; } - static bool isWindows() { return true; } -#else - static bool isAndroid() { return false; } - static bool isiOS() { return false; } - static bool isLinux() { return false; } - static bool isMacOS() { return false; } - static bool isWindows() { return false; } -#endif - -#if defined(QGC_NO_SERIAL_LINK) - static bool isSerialAvailable() { return false; } -#else - static bool isSerialAvailable() { return true; } -#endif - -#ifdef QT_DEBUG - static bool isDebug() { return true; } -#else - static bool isDebug() { return false; } -#endif - - static bool hasTouch(); - static QString iOSDevice(); - static QString fixedFontFamily(); - static QString normalFontFamily(); -}; diff --git a/src/QmlControls/SettingsButton.qml b/src/QmlControls/SettingsButton.qml index 8314b2844c85..aeacfee15092 100644 --- a/src/QmlControls/SettingsButton.qml +++ b/src/QmlControls/SettingsButton.qml @@ -4,11 +4,12 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle Button { id: control padding: ScreenTools.defaultFontPixelWidth * 0.75 - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled autoExclusive: true icon.color: textColor diff --git a/src/QmlControls/SubMenuButton.qml b/src/QmlControls/SubMenuButton.qml index 6dc661d161b3..e62c5762de5b 100644 --- a/src/QmlControls/SubMenuButton.qml +++ b/src/QmlControls/SubMenuButton.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle // Important Note: SubMenuButtons must manage their checked state manually in order to support // view switch prevention. This means they can't be checkable or autoExclusive. @@ -11,7 +12,7 @@ Button { id: control text: "Button" focusPolicy: Qt.ClickFocus - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled implicitHeight: ScreenTools.defaultFontPixelHeight * 2.5 property bool setupComplete: true ///< true: setup complete indicator shows as completed diff --git a/src/QmlControls/ToolStripHoverButton.qml b/src/QmlControls/ToolStripHoverButton.qml index 07737a590721..b279f34f7d6d 100644 --- a/src/QmlControls/ToolStripHoverButton.qml +++ b/src/QmlControls/ToolStripHoverButton.qml @@ -3,13 +3,14 @@ import QtQuick.Controls import QGroundControl import QGroundControl.Controls +import QGCStyle as QGCStyle Button { id: control objectName: toolStripAction ? toolStripAction.objectName : "" width: contentLayoutItem.contentWidth + (contentMargins * 2) height: width - hoverEnabled: !ScreenTools.isMobile + hoverEnabled: QGCStyle.StylePreferences.hoverEffectsEnabled enabled: toolStripAction ? toolStripAction.enabled : true visible: toolStripAction ? toolStripAction.visible : true imageSource: (toolStripAction && modelData) ? (toolStripAction.showAlternateIcon ? modelData.alternateIconSource : modelData.iconSource) : "" diff --git a/src/Settings/App.SettingsGroup.json b/src/Settings/App.SettingsGroup.json index 0a45092c81e6..1ad59aae26e5 100644 --- a/src/Settings/App.SettingsGroup.json +++ b/src/Settings/App.SettingsGroup.json @@ -219,6 +219,35 @@ "label": "UI Scaling", "keywords": "ui scale,font size,zoom" }, + { + "name": "forceHighContrast", + "shortDesc": "Forces the high-contrast interface appearance.", + "longDesc": "Uses the high-contrast interface appearance even when the platform does not request it.", + "type": "bool", + "default": false, + "label": "High Contrast", + "keywords": "accessibility,contrast,appearance" + }, + { + "name": "reducedMotion", + "shortDesc": "Reduces interface animations and transitions.", + "longDesc": "Disables non-essential interface animations and transitions.", + "type": "bool", + "default": false, + "label": "Reduced Motion", + "keywords": "accessibility,animation,motion" + }, + { + "name": "touchModeOverride", + "shortDesc": "Selects automatic, pointer, or touch interface density.", + "longDesc": "Overrides automatic input-device detection when choosing control spacing and touch target sizes.", + "type": "int32", + "enumStrings": "Automatic,Pointer,Touch", + "enumValues": "-1,0,1", + "default": -1, + "label": "Interface Density", + "keywords": "touch,pointer,density,spacing" + }, { "name": "indoorPalette", "shortDesc": "Switches between light and dark color schemes for different lighting conditions.", diff --git a/src/Settings/AppSettings.cc b/src/Settings/AppSettings.cc index c2efdc29b0c5..4971400f9dab 100644 --- a/src/Settings/AppSettings.cc +++ b/src/Settings/AppSettings.cc @@ -141,6 +141,9 @@ DECLARE_SETTINGSFACT(AppSettings, virtualJoystick) DECLARE_SETTINGSFACT(AppSettings, virtualJoystickAutoCenterThrottle) DECLARE_SETTINGSFACT(AppSettings, virtualJoystickLeftHandedMode) DECLARE_SETTINGSFACT(AppSettings, uiScalePercent) +DECLARE_SETTINGSFACT(AppSettings, forceHighContrast) +DECLARE_SETTINGSFACT(AppSettings, reducedMotion) +DECLARE_SETTINGSFACT(AppSettings, touchModeOverride) DECLARE_SETTINGSFACT(AppSettings, savePath) DECLARE_SETTINGSFACT(AppSettings, androidDontSaveToSDCard) DECLARE_SETTINGSFACT(AppSettings, useChecklist) diff --git a/src/Settings/AppSettings.h b/src/Settings/AppSettings.h index c5827a305fca..2ab72ea3c7f1 100644 --- a/src/Settings/AppSettings.h +++ b/src/Settings/AppSettings.h @@ -32,6 +32,9 @@ class AppSettings : public SettingsGroup DEFINE_SETTINGFACT(virtualJoystickAutoCenterThrottle) DEFINE_SETTINGFACT(virtualJoystickLeftHandedMode) DEFINE_SETTINGFACT(uiScalePercent) + DEFINE_SETTINGFACT(forceHighContrast) + DEFINE_SETTINGFACT(reducedMotion) + DEFINE_SETTINGFACT(touchModeOverride) DEFINE_SETTINGFACT(indoorPalette) DEFINE_SETTINGFACT(savePath) DEFINE_SETTINGFACT(androidDontSaveToSDCard) diff --git a/test/QmlControls/CMakeLists.txt b/test/QmlControls/CMakeLists.txt index d68a6f5708cb..40cf57906a11 100644 --- a/test/QmlControls/CMakeLists.txt +++ b/test/QmlControls/CMakeLists.txt @@ -2,12 +2,35 @@ # QmlControls Unit Tests # ============================================================================ +qt_add_library(QGCStyleTesting STATIC) + +qt_add_qml_module(QGCStyleTesting + URI QGCStyle.Testing + VERSION 1.0 + RESOURCE_PREFIX /qml + DEPENDENCIES + QGCStyle + QML_FILES + QGCStyle/QGCStyleGallery.qml + QGCStyle/QGCStyleKitPreview.qml + NO_PLUGIN +) + +target_link_libraries(QGCStyleTesting PRIVATE QGCStyle Qt6::LabsStyleKit) +target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE QGCStyleTesting) + target_sources(${CMAKE_PROJECT_NAME} PRIVATE ObjectItemModelBaseTest.cc ObjectItemModelBaseTest.h ObjectListModelBaseTest.cc ObjectListModelBaseTest.h + QGCPaletteTest.cc + QGCPaletteTest.h + QGCStyleTest.cc + QGCStyleTest.h + ScreenToolsTest.cc + ScreenToolsTest.h QmlObjectListModelTest.cc QmlObjectListModelTest.h QmlObjectTreeModelTest.cc @@ -19,5 +42,34 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_ add_qgc_test(ObjectItemModelBaseTest LABELS Unit) add_qgc_test(ObjectListModelBaseTest LABELS Unit) +add_qgc_test(QGCPaletteTest LABELS Unit) +add_qgc_test(QGCStyleTest LABELS Unit) +add_qgc_test(ScreenToolsTest LABELS Unit) add_qgc_test(QmlObjectListModelTest LABELS Unit) add_qgc_test(QmlObjectTreeModelTest LABELS Unit) + +add_executable(QGCLabelBasicStyleTestRunner + QGCLabelBasicStyleTestMain.cc +) + +target_link_libraries(QGCLabelBasicStyleTestRunner + PRIVATE + Qt6::Gui + Qt6::Qml + Qt6::QuickControls2 +) + +add_test( + NAME QGCLabelBasicStyleTest + COMMAND + $ + ${CMAKE_SOURCE_DIR}/src/QmlControls/QGCLabel.qml + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) + +set_tests_properties(QGCLabelBasicStyleTest PROPERTIES + LABELS "Unit;QML" + TIMEOUT ${QGC_TEST_TIMEOUT_UNIT} + ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;LIBGL_ALWAYS_SOFTWARE=1;QT_LOGGING_RULES=*.debug=false" + FAIL_REGULAR_EXPRESSION "QML load failed;Missing Label property;Unexpected Qt Quick Controls style;Segmentation fault" +) diff --git a/test/QmlControls/QGCLabelBasicStyleTestMain.cc b/test/QmlControls/QGCLabelBasicStyleTestMain.cc new file mode 100644 index 000000000000..d6087502add3 --- /dev/null +++ b/test/QmlControls/QGCLabelBasicStyleTestMain.cc @@ -0,0 +1,50 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char* argv[]) +{ + QQuickStyle::setStyle(QStringLiteral("Basic")); + QGuiApplication app(argc, argv); + + if (app.arguments().size() != 2) { + qCritical() << "Expected the QGCLabel.qml path"; + return EXIT_FAILURE; + } + + QQmlEngine engine; + const QUrl labelUrl = QUrl::fromLocalFile(QFileInfo(app.arguments().at(1)).absoluteFilePath()); + QQmlComponent component(&engine, labelUrl); + QScopedPointer label(component.create()); + + if (!label) { + qCritical().noquote() << "QML load failed:" << component.errorString(); + return EXIT_FAILURE; + } + + if (QQuickStyle::name() != QStringLiteral("Basic")) { + qCritical() << "Unexpected Qt Quick Controls style:" << QQuickStyle::name(); + return EXIT_FAILURE; + } + + static constexpr std::array labelProperties = {"antialiasing", "color", "font", "linkColor", "text"}; + for (const char* propertyName : labelProperties) { + if (label->metaObject()->indexOfProperty(propertyName) < 0) { + qCritical() << "Missing Label property:" << propertyName; + return EXIT_FAILURE; + } + } + + if (!label->property("antialiasing").toBool()) { + qCritical() << "QGCLabel antialiasing default was not applied"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/test/QmlControls/QGCPaletteTest.cc b/test/QmlControls/QGCPaletteTest.cc new file mode 100644 index 000000000000..35ee433e5f68 --- /dev/null +++ b/test/QmlControls/QGCPaletteTest.cc @@ -0,0 +1,151 @@ +#include "QGCPaletteTest.h" + +#include +#include +#include + +namespace { +qreal linearChannel(qreal channel) +{ + return channel <= 0.04045 ? channel / 12.92 : std::pow((channel + 0.055) / 1.055, 2.4); +} + +qreal relativeLuminance(const QColor& color) +{ + return 0.2126 * linearChannel(color.redF()) + 0.7152 * linearChannel(color.greenF()) + + 0.0722 * linearChannel(color.blueF()); +} + +qreal contrastRatio(const QColor& first, const QColor& second) +{ + const qreal firstLuminance = relativeLuminance(first); + const qreal secondLuminance = relativeLuminance(second); + const qreal lighter = std::max(firstLuminance, secondLuminance); + const qreal darker = std::min(firstLuminance, secondLuminance); + + return (lighter + 0.05) / (darker + 0.05); +} +} // namespace + +void QGCPaletteTest::init() +{ + _originalTheme = QGCPalette::globalTheme(); +} + +void QGCPaletteTest::cleanup() +{ + QGCPalette::setGlobalTheme(_originalTheme); +} + +void QGCPaletteTest::_initializesApplicationPalette() +{ + QPalette applicationPalette = QGuiApplication::palette(); + applicationPalette.setColor(QPalette::Active, QPalette::Window, Qt::magenta); + QGuiApplication::setPalette(applicationPalette); + + QGCPalette::initializeApplicationPalette(); + + QGCPalette qgcPalette; + QCOMPARE(QGuiApplication::palette().color(QPalette::Active, QPalette::Window), qgcPalette.window()); +} + +void QGCPaletteTest::_syncsApplicationPalette_data() +{ + QTest::addColumn("theme"); + + QTest::newRow("light") << static_cast(QGCPalette::Light); + QTest::newRow("dark") << static_cast(QGCPalette::Dark); +} + +void QGCPaletteTest::_syncsApplicationPalette() +{ + QFETCH(int, theme); + + QGCPalette enabledPalette; + QGCPalette disabledPalette; + disabledPalette.setColorGroupEnabled(false); + + QGCPalette::setGlobalTheme(static_cast(theme)); + + const QPalette applicationPalette = QGuiApplication::palette(); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::Window), enabledPalette.window()); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::WindowText), enabledPalette.text()); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::Button), enabledPalette.button()); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::ButtonText), enabledPalette.buttonText()); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::Highlight), enabledPalette.buttonHighlight()); + QCOMPARE(applicationPalette.color(QPalette::Active, QPalette::HighlightedText), + enabledPalette.buttonHighlightText()); + QCOMPARE(applicationPalette.color(QPalette::Disabled, QPalette::WindowText), disabledPalette.text()); + QCOMPARE(applicationPalette.color(QPalette::Disabled, QPalette::Button), disabledPalette.button()); + QCOMPARE(applicationPalette.color(QPalette::Disabled, QPalette::ButtonText), disabledPalette.buttonText()); +} + +void QGCPaletteTest::_textContrast_data() +{ + QTest::addColumn("theme"); + + QTest::newRow("light") << static_cast(QGCPalette::Light); + QTest::newRow("dark") << static_cast(QGCPalette::Dark); +} + +void QGCPaletteTest::_textContrast() +{ + QFETCH(int, theme); + + QGCPalette::setGlobalTheme(static_cast(theme)); + + const QPalette palette = QGuiApplication::palette(); + const auto verifyContrast = [&palette](QPalette::ColorRole foregroundRole, QPalette::ColorRole backgroundRole, + const char* pairName) { + const qreal ratio = contrastRatio(palette.color(QPalette::Active, foregroundRole), + palette.color(QPalette::Active, backgroundRole)); + QVERIFY2(ratio >= 4.5, qPrintable(QStringLiteral("%1 contrast is %2:1; expected at least 4.5:1") + .arg(QString::fromLatin1(pairName)) + .arg(ratio, 0, 'f', 2))); + }; + + verifyContrast(QPalette::WindowText, QPalette::Window, "window text"); + verifyContrast(QPalette::ButtonText, QPalette::Button, "button text"); + verifyContrast(QPalette::Text, QPalette::Base, "field text"); +} + +void QGCPaletteTest::_semanticContrast_data() +{ + QTest::addColumn("theme"); + + QTest::newRow("light") << static_cast(QGCPalette::Light); + QTest::newRow("dark") << static_cast(QGCPalette::Dark); +} + +void QGCPaletteTest::_semanticContrast() +{ + QFETCH(int, theme); + + QGCPalette::setGlobalTheme(static_cast(theme)); + + QGCPalette enabledPalette; + QGCPalette disabledPalette; + disabledPalette.setColorGroupEnabled(false); + + const auto verifyContrast = [](const QColor& foreground, const QColor& background, qreal minimumRatio, + const char* pairName) { + const qreal ratio = contrastRatio(foreground, background); + QVERIFY2(ratio >= minimumRatio, qPrintable(QStringLiteral("%1 contrast is %2:1; expected at least %3:1") + .arg(QString::fromLatin1(pairName)) + .arg(ratio, 0, 'f', 2) + .arg(minimumRatio, 0, 'f', 1))); + }; + + verifyContrast(enabledPalette.warningText(), enabledPalette.window(), 4.5, "warning text"); + verifyContrast(enabledPalette.alertText(), enabledPalette.alertBackground(), 4.5, "alert text"); + verifyContrast(enabledPalette.statusPassedText(), enabledPalette.window(), 4.5, "success status text"); + verifyContrast(enabledPalette.statusFailedText(), enabledPalette.window(), 4.5, "failure status text"); + verifyContrast(enabledPalette.colorGreen(), enabledPalette.window(), 3., "success indicator"); + verifyContrast(enabledPalette.colorRed(), enabledPalette.textField(), 3., "validation indicator"); + + verifyContrast(disabledPalette.text(), disabledPalette.window(), 2., "disabled window text"); + verifyContrast(disabledPalette.buttonText(), disabledPalette.button(), 2., "disabled button text"); + verifyContrast(disabledPalette.textFieldText(), disabledPalette.textField(), 3., "disabled field text"); +} + +UT_REGISTER_TEST(QGCPaletteTest, TestLabel::Unit) diff --git a/test/QmlControls/QGCPaletteTest.h b/test/QmlControls/QGCPaletteTest.h new file mode 100644 index 000000000000..af42f81211f6 --- /dev/null +++ b/test/QmlControls/QGCPaletteTest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "QGCPalette.h" +#include "UnitTest.h" + +class QGCPaletteTest : public UnitTest +{ + Q_OBJECT + +private slots: + void init(); + void cleanup(); + void _initializesApplicationPalette(); + void _syncsApplicationPalette_data(); + void _syncsApplicationPalette(); + void _textContrast_data(); + void _textContrast(); + void _semanticContrast_data(); + void _semanticContrast(); + +private: + QGCPalette::Theme _originalTheme = QGCPalette::Dark; +}; diff --git a/test/QmlControls/QGCStyle/QGCStyleGallery.qml b/test/QmlControls/QGCStyle/QGCStyleGallery.qml new file mode 100644 index 000000000000..98e02b733d35 --- /dev/null +++ b/test/QmlControls/QGCStyle/QGCStyleGallery.qml @@ -0,0 +1,565 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGCStyle + +FocusScope { + id: root + + readonly property real margin: StyleMetrics.contentMargin + readonly property bool mirroredLayout: LayoutMirroring.enabled + readonly property bool narrowLayout: width < StyleMetrics.controlWidth * 6 + readonly property int uiScalePercent: Math.round(StyleTypography.scaleFactor * 100) + readonly property color validationColor: qgcPalette.window.hslLightness < 0.5 ? "#f32836" : "#b52b2b" + + implicitHeight: contentLayout.implicitHeight + margin * 2 + implicitWidth: contentLayout.implicitWidth + margin * 2 + + SystemPalette { + id: qgcPalette + + colorGroup: SystemPalette.Active + } + + FontInfo { + id: bodyFontInfo + + font: StyleTypography.bodyFont + } + + FontInfo { + id: fixedFontInfo + + font: StyleTypography.fixedFont + } + + Rectangle { + anchors.fill: parent + border.color: qgcPalette.mid + border.width: 1 + color: qgcPalette.window + radius: StyleMetrics.panelRadius + } + + ColumnLayout { + id: contentLayout + + objectName: "contentLayout" + anchors.fill: parent + anchors.margins: root.margin + spacing: StyleMetrics.spacing + + Label { + color: qgcPalette.text + font: StyleTypography.titleFont + text: qsTr("QGCStyle state gallery") + } + + Label { + Layout.fillWidth: true + color: qgcPalette.text + font: StyleTypography.bodyFont + text: qsTr("Switch themes, adjust accessibility preferences, then exercise each control state.") + wrapMode: Text.Wrap + } + + GridLayout { + objectName: "preferenceLayout" + Layout.fillWidth: true + columns: root.narrowLayout ? 1 : 4 + columnSpacing: StyleMetrics.spacing + rowSpacing: StyleMetrics.spacing + + ComboBox { + objectName: "densityComboBox" + Layout.fillWidth: root.narrowLayout + currentIndex: indexOfValue(StylePreferences.touchModeOverride) + textRole: "text" + valueRole: "value" + model: [ + { text: qsTr("Automatic density"), value: StylePreferences.Automatic }, + { text: qsTr("Pointer density"), value: StylePreferences.Pointer }, + { text: qsTr("Touch density"), value: StylePreferences.Touch } + ] + + onActivated: StylePreferences.setTouchModePreference(currentValue) + } + + Switch { + objectName: "reducedMotionSwitch" + Layout.fillWidth: root.narrowLayout + checked: StylePreferences.reducedMotion + text: qsTr("Reduced motion") + + onToggled: StylePreferences.setReducedMotion(checked) + } + + Switch { + Layout.fillWidth: root.narrowLayout + checked: StylePreferences.forceHighContrast + text: qsTr("High contrast") + + onToggled: StylePreferences.setForceHighContrast(checked) + } + + Label { + color: qgcPalette.text + text: qsTr("UI scale: %1%").arg(root.uiScalePercent) + } + } + + Flow { + Layout.fillWidth: true + spacing: StyleMetrics.spacing + + Label { + color: qgcPalette.text + font: StyleTypography.captionFont + text: qsTr("Caption") + } + + Label { + color: qgcPalette.text + font: StyleTypography.bodyFont + text: qsTr("Body") + } + + Label { + color: qgcPalette.text + font: StyleTypography.headingFont + text: qsTr("Heading") + } + + Label { + color: qgcPalette.text + font: StyleTypography.titleFont + text: qsTr("Title") + } + } + + Label { + Layout.fillWidth: true + color: qgcPalette.text + font: StyleTypography.captionFont + text: qsTr("Resolved fonts: %1; fixed: %2 (%3)") + .arg(bodyFontInfo.family) + .arg(fixedFontInfo.family) + .arg(fixedFontInfo.fixedPitch ? qsTr("fixed pitch") : qsTr("proportional")) + wrapMode: Text.Wrap + } + + GridLayout { + objectName: "controlLayout" + Layout.fillWidth: true + columnSpacing: StyleMetrics.contentMargin + columns: root.narrowLayout ? 2 : 4 + rowSpacing: StyleMetrics.spacing + + Label { + color: qgcPalette.text + text: qsTr("Default") + } + + Button { + text: down ? qsTr("Pressed") : qsTr("Button") + } + + Label { + color: qgcPalette.text + text: qsTr("Disabled") + } + + Button { + enabled: false + text: qsTr("Button") + } + + Label { + color: qgcPalette.text + text: qsTr("Keyboard focus") + } + + Button { + text: qsTr("Focused") + + Component.onCompleted: forceActiveFocus(Qt.TabFocusReason) + } + + Label { + color: qgcPalette.text + text: qsTr("Checked") + } + + Button { + checkable: true + checked: true + text: qsTr("Checked") + } + + Label { + color: qgcPalette.text + text: qsTr("Large font") + } + + Button { + font: StyleTypography.titleFont + text: qsTr("Large") + } + + Label { + color: qgcPalette.text + text: qsTr("Long text") + } + + Button { + Layout.fillWidth: root.narrowLayout + Layout.preferredWidth: root.narrowLayout ? 0 : StyleMetrics.controlWidth * 2 + text: qsTr("A deliberately long translated control label used to verify eliding and layout behavior") + } + + Label { + color: qgcPalette.text + text: qsTr("Current scale") + } + + Label { + color: qgcPalette.text + text: qsTr("%1% UI scale").arg(root.uiScalePercent) + } + + Label { + color: qgcPalette.text + text: qsTr("Right-to-left") + } + + Flow { + Layout.fillWidth: root.narrowLayout + LayoutMirroring.childrenInherit: true + LayoutMirroring.enabled: true + spacing: StyleMetrics.spacing + + Button { + icon.source: "/qmlimages/Gears.svg" + text: qsTr("RTL") + } + + Button { + ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + ToolTip.text: text + ToolTip.visible: hovered || activeFocus + display: AbstractButton.IconOnly + icon.source: "/qmlimages/Gears.svg" + text: qsTr("Settings") + } + } + + Label { + color: qgcPalette.text + text: qsTr("Inputs") + } + + Flow { + Layout.fillWidth: root.narrowLayout + spacing: StyleMetrics.spacing + + TextField { + placeholderText: qsTr("Text field") + } + + ComboBox { + model: [qsTr("Automatic"), qsTr("Manual"), qsTr("Disabled")] + } + } + + Label { + color: qgcPalette.text + text: qsTr("Validation error") + } + + TextField { + palette.mid: root.validationColor + text: qsTr("Invalid value") + } + + Label { + color: qgcPalette.text + text: qsTr("Read only") + } + + TextField { + readOnly: true + text: qsTr("Read-only value") + } + + Label { + color: qgcPalette.text + text: qsTr("Multiline") + } + + TextArea { + Layout.fillWidth: true + placeholderText: qsTr("Enter notes") + text: qsTr("Styled text area") + wrapMode: TextEdit.Wrap + } + + Label { + color: qgcPalette.text + text: qsTr("Selection") + } + + Flow { + Layout.fillWidth: root.narrowLayout + spacing: StyleMetrics.spacing + + CheckBox { + checked: true + text: qsTr("Check box") + } + + RadioButton { + checked: true + text: qsTr("Radio") + } + + Switch { + checked: true + text: qsTr("Switch") + } + + Slider { + from: 0 + to: 100 + value: 65 + } + } + + Label { + color: qgcPalette.text + text: qsTr("Menu") + } + + Button { + text: qsTr("Open menu") + + onClicked: stateMenu.popup() + + Menu { + id: stateMenu + + MenuItem { + checkable: true + checked: true + text: qsTr("Checked item") + } + + MenuItem { + text: qsTr("Regular item") + } + + MenuSeparator { + } + + MenuItem { + enabled: false + text: qsTr("Disabled item") + } + + Menu { + title: qsTr("Submenu") + + MenuItem { + text: qsTr("Nested item") + } + } + } + } + + Label { + color: qgcPalette.text + text: qsTr("Overlays") + } + + Flow { + Layout.fillWidth: root.narrowLayout + spacing: StyleMetrics.spacing + + Button { + text: qsTr("Dialog") + + onClicked: sampleDialog.open() + } + + Button { + text: qsTr("Popup") + + onClicked: samplePopup.open() + } + + Button { + text: qsTr("Drawer") + + onClicked: sampleDrawer.open() + } + } + + Label { + color: qgcPalette.text + text: qsTr("Progress") + } + + ProgressBar { + Layout.fillWidth: true + value: 0.65 + } + + Label { + color: qgcPalette.text + text: qsTr("Indeterminate") + } + + ProgressBar { + Layout.fillWidth: true + indeterminate: true + } + + Label { + color: qgcPalette.text + text: qsTr("Activity") + } + + Flow { + Layout.fillWidth: root.narrowLayout + spacing: StyleMetrics.spacing + + BusyIndicator { + running: true + } + + BusyIndicator { + enabled: false + running: true + } + + BusyIndicator { + running: false + } + } + + Label { + color: qgcPalette.text + text: qsTr("Scroll bar") + } + + ScrollView { + Layout.fillWidth: true + implicitHeight: StyleMetrics.controlHeight * 2.5 + + Column { + width: parent.width + + Repeater { + model: 6 + + ItemDelegate { + required property int index + + text: qsTr("Scrollable item %1").arg(index + 1) + width: parent.width + } + } + } + } + + Label { + color: qgcPalette.text + text: qsTr("Scroll indicator") + } + + Flickable { + id: indicatorFlickable + + Layout.fillWidth: true + clip: true + contentHeight: indicatorColumn.implicitHeight + implicitHeight: StyleMetrics.controlHeight * 2.5 + + ScrollIndicator.vertical: ScrollIndicator { + } + + Column { + id: indicatorColumn + + width: indicatorFlickable.width + + Repeater { + model: 6 + + Label { + required property int index + + color: qgcPalette.text + padding: StyleMetrics.smallSpacing + text: qsTr("Indicator row %1").arg(index + 1) + } + } + } + } + } + } + + Dialog { + id: sampleDialog + + anchors.centerIn: parent + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + title: qsTr("Styled dialog") + + Label { + text: qsTr("Dialogs share the QGCStyle palette and spacing.") + } + } + + Popup { + id: samplePopup + + x: Math.round((root.width - width) * 0.5) + y: Math.round((root.height - height) * 0.5) + + Label { + text: qsTr("Styled popup content") + } + } + + Drawer { + id: sampleDrawer + + height: root.height + width: Math.min(root.width * 0.4, StyleMetrics.controlWidth * 2) + + ColumnLayout { + anchors.fill: parent + anchors.margins: StyleMetrics.dialogPadding + + Label { + font.bold: true + text: qsTr("Styled drawer") + } + + Item { + Layout.fillHeight: true + } + + Button { + Layout.fillWidth: true + text: qsTr("Close") + + onClicked: sampleDrawer.close() + } + } + } +} diff --git a/test/QmlControls/QGCStyle/QGCStyleKitPreview.qml b/test/QmlControls/QGCStyle/QGCStyleKitPreview.qml new file mode 100644 index 000000000000..cae3defd8f66 --- /dev/null +++ b/test/QmlControls/QGCStyle/QGCStyleKitPreview.qml @@ -0,0 +1,97 @@ +import Qt.labs.StyleKit as Labs +import QtQuick +import QtQuick.Layouts + +import QGCStyle + +Labs.Pane { + id: root + + readonly property bool narrowLayout: width < StyleMetrics.controlWidth * 5 + + implicitHeight: contentLayout.implicitHeight + topPadding + bottomPadding + implicitWidth: contentLayout.implicitWidth + leftPadding + rightPadding + + ColumnLayout { + id: contentLayout + + anchors.fill: parent + + Labs.Label { + Layout.fillWidth: true + font.bold: true + text: qsTr("Qt Labs StyleKit preview") + } + + GridLayout { + objectName: "previewControlLayout" + Layout.fillWidth: true + columns: root.narrowLayout ? 1 : 5 + + Labs.Button { + Layout.fillWidth: root.narrowLayout + text: qsTr("Button") + } + + Labs.CheckBox { + Layout.fillWidth: root.narrowLayout + checked: true + text: qsTr("Check box") + } + + Labs.Switch { + Layout.fillWidth: root.narrowLayout + checked: true + text: qsTr("Switch") + } + + Labs.TextField { + Layout.fillWidth: root.narrowLayout + placeholderText: qsTr("Text field") + } + + Labs.ComboBox { + Layout.fillWidth: root.narrowLayout + model: [qsTr("Automatic"), qsTr("Manual")] + } + } + + GridLayout { + objectName: "previewProgressLayout" + Layout.fillWidth: true + columns: root.narrowLayout ? 1 : 2 + + Labs.ProgressBar { + Layout.fillWidth: true + value: 0.65 + } + + Labs.Slider { + Layout.fillWidth: true + value: 0.65 + } + } + + GridLayout { + objectName: "previewVariationLayout" + Layout.fillWidth: true + columns: root.narrowLayout ? 1 : 3 + + Labs.Label { + text: qsTr("Variations") + } + + Labs.Button { + Layout.fillWidth: root.narrowLayout + Labs.StyleVariation.variations: ["primary"] + text: qsTr("Primary") + } + + Labs.Button { + Layout.fillWidth: root.narrowLayout + Labs.StyleVariation.variations: ["compact"] + text: qsTr("Compact") + } + } + } +} diff --git a/test/QmlControls/QGCStyleTest.cc b/test/QmlControls/QGCStyleTest.cc new file mode 100644 index 000000000000..3038d471283e --- /dev/null +++ b/test/QmlControls/QGCStyleTest.cc @@ -0,0 +1,954 @@ +#include "QGCStyleTest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AppSettings.h" +#include "Fact.h" +#include "SettingsManager.h" + +void QGCStyleTest::init() +{ + _originalTheme = QGCPalette::globalTheme(); +} + +void QGCStyleTest::cleanup() +{ + QGCPalette::setGlobalTheme(_originalTheme); +} + +void QGCStyleTest::_galleryLoads_data() +{ + QTest::addColumn("theme"); + + QTest::newRow("light") << static_cast(QGCPalette::Light); + QTest::newRow("dark") << static_cast(QGCPalette::Dark); +} + +void QGCStyleTest::_galleryLoads() +{ + QFETCH(int, theme); + + QGCPalette::setGlobalTheme(static_cast(theme)); + + static constexpr char qml[] = R"( +import QGCStyle.Testing as QGCStyleTesting + +QGCStyleTesting.QGCStyleGallery { +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCStyleGalleryLoadTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer gallery(component.create()); + QVERIFY2(gallery, qPrintable(component.errorString())); +} + +void QGCStyleTest::_galleryRenders_data() +{ + QTest::addColumn("theme"); + QTest::addColumn("scaleFactor"); + QTest::addColumn("touchMode"); + QTest::addColumn("mirrored"); + QTest::addColumn("viewportSize"); + + static constexpr std::array scaleFactors = {0.5, 1., 1.25, 1.5, 1.75, 2.}; + for (const auto theme : {QGCPalette::Light, QGCPalette::Dark}) { + for (const double scaleFactor : scaleFactors) { + const QString name = QStringLiteral("desktop-%1-%2-percent") + .arg(theme == QGCPalette::Light ? QStringLiteral("light") : QStringLiteral("dark")) + .arg(qRound(scaleFactor * 100)); + QTest::newRow(qPrintable(name)) + << static_cast(theme) << scaleFactor << false << false << QSize(1200, 900); + } + + for (const bool touchMode : {false, true}) { + for (const double scaleFactor : {1., 2.}) { + const QString name = + QStringLiteral("narrow-%1-%2-%3-percent") + .arg(theme == QGCPalette::Light ? QStringLiteral("light") : QStringLiteral("dark")) + .arg(touchMode ? QStringLiteral("touch") : QStringLiteral("pointer")) + .arg(qRound(scaleFactor * 100)); + QTest::newRow(qPrintable(name)) + << static_cast(theme) << scaleFactor << touchMode << false << QSize(480, 900); + } + } + + const QString themeName = theme == QGCPalette::Light ? QStringLiteral("light") : QStringLiteral("dark"); + QTest::newRow(qPrintable(QStringLiteral("rtl-wide-%1-125-percent").arg(themeName))) + << static_cast(theme) << 1.25 << false << true << QSize(1200, 900); + QTest::newRow(qPrintable(QStringLiteral("rtl-narrow-%1-touch-175-percent").arg(themeName))) + << static_cast(theme) << 1.75 << true << true << QSize(480, 900); + } +} + +void QGCStyleTest::_galleryRenders() +{ + QFETCH(int, theme); + QFETCH(double, scaleFactor); + QFETCH(bool, touchMode); + QFETCH(bool, mirrored); + QFETCH(QSize, viewportSize); + + QGCPalette::setGlobalTheme(static_cast(theme)); + + static constexpr char qml[] = R"( +import QtQuick +import Qt.labs.StyleKit as Labs +import QGCStyle as QGCStyle +import QGCStyle.Testing as QGCStyleTesting + +Item { + id: root + + required property real requestedScaleFactor + required property bool requestedMirrored + required property bool requestedTouchMode + property real originalScaleFactor: 1 + property int originalTouchModeOverride: QGCStyle.StylePreferences.Automatic + + readonly property int contentMargin: QGCStyle.StyleMetrics.contentMargin + readonly property int controlHeight: QGCStyle.StyleMetrics.controlHeight + readonly property int controlWidth: QGCStyle.StyleMetrics.controlWidth + readonly property int galleryScalePercent: gallery.uiScalePercent + readonly property bool galleryMirroredLayout: gallery.mirroredLayout + readonly property bool galleryNarrowLayout: gallery.narrowLayout + readonly property int indicatorSize: QGCStyle.StyleMetrics.indicatorSize + readonly property int nominalTouchTarget: QGCStyle.StyleMetrics.nominalTouchTarget + readonly property int toolbarIconSize: QGCStyle.StyleMetrics.toolbarIconSize + readonly property bool touchMode: QGCStyle.StylePreferences.touchMode + + Labs.StyleKit.style: qgcStyle + LayoutMirroring.childrenInherit: true + LayoutMirroring.enabled: requestedMirrored + + function isStyleLoaded(): bool { + return Labs.StyleKit.styleLoaded() + } + + Component.onCompleted: { + originalScaleFactor = QGCStyle.StyleTypography.scaleFactor + originalTouchModeOverride = QGCStyle.StylePreferences.touchModeOverride + QGCStyle.StylePreferences.setTouchModePreference(requestedTouchMode ? QGCStyle.StylePreferences.Touch + : QGCStyle.StylePreferences.Pointer) + QGCStyle.StyleTypography.scaleFactor = requestedScaleFactor + } + Component.onDestruction: { + QGCStyle.StylePreferences.setTouchModePreference(originalTouchModeOverride) + QGCStyle.StyleTypography.scaleFactor = originalScaleFactor + } + + QGCStyleTesting.QGCStyleGallery { + id: gallery + + anchors.fill: parent + } + + QGCStyle.QGCStyleKit { + id: qgcStyle + } +} +)"; + + QQuickView view; + view.engine()->addImportPath(QStringLiteral("qrc:/qml")); + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.resize(viewportSize); + + const QUrl testUrl(QStringLiteral("qrc:/tests/QGCStyleGalleryScaleTest.qml")); + QQmlComponent component(view.engine()); + component.setData(qml, testUrl); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QObject* const root = component.createWithInitialProperties({ + {QStringLiteral("requestedScaleFactor"), scaleFactor}, + {QStringLiteral("requestedMirrored"), mirrored}, + {QStringLiteral("requestedTouchMode"), touchMode}, + }); + QVERIFY2(root, qPrintable(component.errorString())); + view.setContent(testUrl, &component, root); + + QStringList errors; + for (const QQmlError& error : view.errors()) { + errors.append(error.toString()); + } + QVERIFY2(view.status() == QQuickView::Ready, qPrintable(errors.join(QLatin1Char('\n')))); + + view.show(); + QTRY_VERIFY_WITH_TIMEOUT(view.isExposed(), 5000); + const auto styleLoaded = [root]() { + bool result = false; + return QMetaObject::invokeMethod(root, "isStyleLoaded", Q_RETURN_ARG(bool, result)) && result; + }; + QTRY_VERIFY_WITH_TIMEOUT(styleLoaded(), 5000); + const int densityControlHeight = touchMode ? 48 : 40; + const int densityControlWidth = touchMode ? 120 : 100; + const int densityContentMargin = touchMode ? 16 : 12; + const int densityIndicatorSize = touchMode ? 32 : 28; + QTRY_COMPARE_WITH_TIMEOUT(root->property("touchMode").toBool(), touchMode, 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("controlHeight").toInt(), qRound(densityControlHeight * scaleFactor), + 5000); + QCOMPARE(root->property("controlWidth").toInt(), qRound(densityControlWidth * scaleFactor)); + QCOMPARE(root->property("contentMargin").toInt(), qRound(densityContentMargin * scaleFactor)); + QCOMPARE(root->property("galleryScalePercent").toInt(), qRound(scaleFactor * 100)); + QCOMPARE(root->property("galleryMirroredLayout").toBool(), mirrored); + QCOMPARE(root->property("galleryNarrowLayout").toBool(), + viewportSize.width() < qRound(densityControlWidth * scaleFactor) * 6); + QCOMPARE(root->property("indicatorSize").toInt(), qRound(densityIndicatorSize * scaleFactor)); + QCOMPARE(root->property("nominalTouchTarget").toInt(), qRound(48 * scaleFactor)); + QCOMPARE(root->property("toolbarIconSize").toInt(), qRound(24 * scaleFactor)); + + QQuickItem* const contentLayout = root->findChild(QStringLiteral("contentLayout")); + QQuickItem* const preferenceLayout = root->findChild(QStringLiteral("preferenceLayout")); + QQuickItem* const controlLayout = root->findChild(QStringLiteral("controlLayout")); + QQuickItem* const densityComboBox = root->findChild(QStringLiteral("densityComboBox")); + QQuickItem* const reducedMotionSwitch = root->findChild(QStringLiteral("reducedMotionSwitch")); + QVERIFY(contentLayout); + QVERIFY(preferenceLayout); + QVERIFY(controlLayout); + QVERIFY(densityComboBox); + QVERIFY(reducedMotionSwitch); + QCOMPARE(preferenceLayout->property("columns").toInt(), root->property("galleryNarrowLayout").toBool() ? 1 : 4); + QCOMPARE(controlLayout->property("columns").toInt(), root->property("galleryNarrowLayout").toBool() ? 2 : 4); + if (!root->property("galleryNarrowLayout").toBool()) { + QCOMPARE(densityComboBox->x() > reducedMotionSwitch->x(), mirrored); + } + + const auto childrenFitWidth = [](const QQuickItem* layout) { + constexpr qreal tolerance = 0.5; + for (const QQuickItem* const child : layout->childItems()) { + if (!child->isVisible()) { + continue; + } + const QRectF bounds = child->mapRectToItem(layout, child->boundingRect()); + if (bounds.left() < -tolerance || bounds.right() > layout->width() + tolerance) { + return false; + } + } + return true; + }; + QTRY_VERIFY_WITH_TIMEOUT(childrenFitWidth(contentLayout), 5000); + QTRY_VERIFY_WITH_TIMEOUT(childrenFitWidth(preferenceLayout), 5000); + QTRY_VERIFY_WITH_TIMEOUT(childrenFitWidth(controlLayout), 5000); + + const QImage image = view.grabWindow().convertToFormat(QImage::Format_ARGB32); + QVERIFY(!image.isNull()); + QCOMPARE(image.size(), view.size()); + + QSet sampledColors; + for (int y = 0; y < image.height(); y += 16) { + for (int x = 0; x < image.width(); x += 16) { + sampledColors.insert(image.pixel(x, y)); + } + } + QVERIFY2(sampledColors.size() >= 8, "The style gallery rendered as an unexpectedly uniform frame"); +} + +void QGCStyleTest::_previewRenders_data() +{ + QTest::addColumn("scaleFactor"); + QTest::addColumn("viewportSize"); + QTest::addColumn("narrowLayout"); + + QTest::newRow("wide") << 1. << QSize(1200, 500) << false; + QTest::newRow("narrow-scaled") << 2. << QSize(480, 900) << true; +} + +void QGCStyleTest::_previewRenders() +{ + QFETCH(double, scaleFactor); + QFETCH(QSize, viewportSize); + QFETCH(bool, narrowLayout); + + static constexpr char qml[] = R"( +import QtQuick +import Qt.labs.StyleKit as Labs +import QGCStyle as QGCStyle +import QGCStyle.Testing as QGCStyleTesting + +Item { + id: root + + required property real requestedScaleFactor + property real originalScaleFactor: 1 + readonly property bool narrowLayout: preview.narrowLayout + + Labs.StyleKit.style: qgcStyle + + Component.onCompleted: { + originalScaleFactor = QGCStyle.StyleTypography.scaleFactor + QGCStyle.StyleTypography.scaleFactor = requestedScaleFactor + } + Component.onDestruction: QGCStyle.StyleTypography.scaleFactor = originalScaleFactor + + QGCStyleTesting.QGCStyleKitPreview { + id: preview + + anchors.fill: parent + } + + QGCStyle.QGCStyleKit { + id: qgcStyle + } +} +)"; + + QQuickView view; + view.engine()->addImportPath(QStringLiteral("qrc:/qml")); + view.setResizeMode(QQuickView::SizeRootObjectToView); + view.resize(viewportSize); + + const QUrl testUrl(QStringLiteral("qrc:/tests/QGCStylePreviewScaleTest.qml")); + QQmlComponent component(view.engine()); + component.setData(qml, testUrl); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QObject* const root = component.createWithInitialProperties({{QStringLiteral("requestedScaleFactor"), scaleFactor}}); + QVERIFY2(root, qPrintable(component.errorString())); + view.setContent(testUrl, &component, root); + view.show(); + QTRY_VERIFY_WITH_TIMEOUT(view.isExposed(), 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("narrowLayout").toBool(), narrowLayout, 5000); + + QObject* const controlLayout = root->findChild(QStringLiteral("previewControlLayout")); + QObject* const progressLayout = root->findChild(QStringLiteral("previewProgressLayout")); + QObject* const variationLayout = root->findChild(QStringLiteral("previewVariationLayout")); + QVERIFY(controlLayout); + QVERIFY(progressLayout); + QVERIFY(variationLayout); + QCOMPARE(controlLayout->property("columns").toInt(), narrowLayout ? 1 : 5); + QCOMPARE(progressLayout->property("columns").toInt(), narrowLayout ? 1 : 2); + QCOMPARE(variationLayout->property("columns").toInt(), narrowLayout ? 1 : 3); +} + +void QGCStyleTest::_layoutProfile() +{ + static constexpr char qml[] = R"( +import QtQml +import QGCStyle as QGCStyle + +QtObject { + id: root + + property real availableHeight: 500 + property real availableWidth: 500 + property bool mobileLayout: false + property real pixelDensity: 2 + + readonly property real fallbackTouchTarget: QGCStyle.StyleMetrics.fallbackTouchTargetHeight + readonly property bool isShort: layoutProfile.isShort + readonly property bool isTiny: layoutProfile.isTiny + readonly property real minimumTouchTarget: layoutProfile.minimumTouchTarget + + property QGCStyle.LayoutProfile layoutProfile: QGCStyle.LayoutProfile { + availableHeight: root.availableHeight + availableWidth: root.availableWidth + mobileLayout: root.mobileLayout + pixelDensity: root.pixelDensity + } +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCStyleLayoutProfileTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + QVERIFY(!root->property("isTiny").toBool()); + QVERIFY(!root->property("isShort").toBool()); + QCOMPARE(root->property("minimumTouchTarget").toReal(), 10.); + + QVERIFY(root->setProperty("availableWidth", 200.)); + QTRY_VERIFY_WITH_TIMEOUT(root->property("isTiny").toBool(), 5000); + + QVERIFY(root->setProperty("availableWidth", 500.)); + QVERIFY(root->setProperty("availableHeight", 250.)); + QVERIFY(root->setProperty("pixelDensity", 1.)); + QVERIFY(root->setProperty("mobileLayout", true)); + QTRY_VERIFY_WITH_TIMEOUT(root->property("isShort").toBool(), 5000); + + QVERIFY(root->setProperty("availableHeight", 50.)); + QVERIFY(root->setProperty("pixelDensity", 2.)); + QTRY_COMPARE_WITH_TIMEOUT(root->property("minimumTouchTarget"), root->property("fallbackTouchTarget"), 5000); +} + +void QGCStyleTest::_liveUiScaleReflow() +{ + Fact* const uiScalePercentFact = SettingsManager::instance()->appSettings()->uiScalePercent(); + QVERIFY(uiScalePercentFact); + const QVariant originalUiScalePercent = uiScalePercentFact->rawValue(); + const auto restoreUiScalePercent = qScopeGuard( + [uiScalePercentFact, originalUiScalePercent]() { uiScalePercentFact->setRawValue(originalUiScalePercent); }); + uiScalePercentFact->setRawValue(25); + + static constexpr char qml[] = R"( +import QtQuick +import QGCStyle as QGCStyle +import QGroundControl +import QGroundControl.Controls + +Item { + readonly property var styleEnvironment: QGCStyleEnvironment + + readonly property real bodyPointSize: QGCStyle.StyleTypography.bodyPointSize + readonly property real buttonImplicitWidth: button.background.implicitWidth + readonly property int controlHeight: QGCStyle.StyleMetrics.controlHeight + readonly property real styleScaleFactor: QGCStyle.StyleTypography.scaleFactor + readonly property int uiScalePercent: QGroundControl.settingsManager.appSettings.uiScalePercent.value + + function setUiScalePercent(percent: int) { + QGroundControl.settingsManager.appSettings.uiScalePercent.value = percent + } + + QGCButton { + id: button + + text: qsTr("Scale test") + } +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCLiveUiScaleTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + QTRY_COMPARE_WITH_TIMEOUT(root->property("uiScalePercent").toInt(), 100, 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("styleScaleFactor").toReal(), 1., 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("controlHeight").toInt(), 40, 5000); + const double baseBodyPointSize = root->property("bodyPointSize").toReal(); + const double baseButtonImplicitWidth = root->property("buttonImplicitWidth").toReal(); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setUiScalePercent", Q_ARG(int, 200))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("styleScaleFactor").toReal(), 2., 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("bodyPointSize").toReal(), baseBodyPointSize * 2., 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("controlHeight").toInt(), 80, 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("buttonImplicitWidth").toReal() > baseButtonImplicitWidth, 5000); +} + +void QGCStyleTest::_stylePreferencesTouchMode() +{ + static constexpr char qml[] = R"( +import QtQuick +import QGCStyle as QGCStyle +import QGroundControl + +QtObject { + readonly property int automaticTouchMode: QGCStyle.StylePreferences.Automatic + readonly property bool forceHighContrast: QGCStyle.StylePreferences.forceHighContrast + readonly property bool inputTouchDeviceAvailable: QGCStyle.InputCapabilities.touchDeviceAvailable + readonly property bool platformTouchMode: QGCStyle.StylePreferences.platformTouchMode + readonly property int pointerTouchMode: QGCStyle.StylePreferences.Pointer + readonly property bool reducedMotion: QGCStyle.StylePreferences.reducedMotion + readonly property bool settingsForceHighContrast: QGroundControl.settingsManager.appSettings.forceHighContrast.rawValue + readonly property bool settingsReducedMotion: QGroundControl.settingsManager.appSettings.reducedMotion.rawValue + readonly property int settingsTouchModeOverride: QGroundControl.settingsManager.appSettings.touchModeOverride.rawValue + readonly property int touchDensityMode: QGCStyle.StylePreferences.Touch + readonly property bool touchMode: QGCStyle.StylePreferences.touchMode + readonly property int touchModeOverride: QGCStyle.StylePreferences.touchModeOverride + + function setForceHighContrast(enabled: bool) { + QGCStyle.StylePreferences.setForceHighContrast(enabled) + } + + function setReducedMotion(enabled: bool) { + QGCStyle.StylePreferences.setReducedMotion(enabled) + } + + function setTouchInputObserved(observed: bool) { + QGCStyle.StylePreferences.touchInputObserved = observed + } + + function setTouchMode(enabled: bool) { + QGCStyle.StylePreferences.setTouchMode(enabled) + } + + function setTouchModePreference(preference: int) { + QGCStyle.StylePreferences.setTouchModePreference(preference) + } + + function useAutomaticTouchMode() { + QGCStyle.StylePreferences.useAutomaticTouchMode() + } +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCStylePreferencesTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + const bool originalForceHighContrast = root->property("forceHighContrast").toBool(); + const bool originalReducedMotion = root->property("reducedMotion").toBool(); + const int originalTouchModeOverride = root->property("touchModeOverride").toInt(); + const int automaticTouchMode = root->property("automaticTouchMode").toInt(); + const int pointerTouchMode = root->property("pointerTouchMode").toInt(); + const int touchDensityMode = root->property("touchDensityMode").toInt(); + const auto restoreStylePreferences = qScopeGuard([&root, originalForceHighContrast, originalReducedMotion, + originalTouchModeOverride]() { + if (root) { + (void) QMetaObject::invokeMethod(root.data(), "setForceHighContrast", + Q_ARG(bool, originalForceHighContrast)); + (void) QMetaObject::invokeMethod(root.data(), "setReducedMotion", Q_ARG(bool, originalReducedMotion)); + (void) QMetaObject::invokeMethod(root.data(), "setTouchModePreference", + Q_ARG(int, originalTouchModeOverride)); + } + }); + + QCOMPARE(root->property("settingsForceHighContrast").toBool(), originalForceHighContrast); + QCOMPARE(root->property("settingsReducedMotion").toBool(), originalReducedMotion); + QCOMPARE(root->property("settingsTouchModeOverride").toInt(), originalTouchModeOverride); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setForceHighContrast", Q_ARG(bool, !originalForceHighContrast))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("forceHighContrast").toBool(), !originalForceHighContrast, 5000); + QCOMPARE(root->property("settingsForceHighContrast").toBool(), !originalForceHighContrast); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setReducedMotion", Q_ARG(bool, !originalReducedMotion))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("reducedMotion").toBool(), !originalReducedMotion, 5000); + QCOMPARE(root->property("settingsReducedMotion").toBool(), !originalReducedMotion); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "useAutomaticTouchMode")); + QCOMPARE(root->property("touchModeOverride").toInt(), automaticTouchMode); + QCOMPARE(root->property("settingsTouchModeOverride").toInt(), automaticTouchMode); + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchInputObserved", Q_ARG(bool, true))); + QTRY_VERIFY_WITH_TIMEOUT(root->property("touchMode").toBool(), 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchInputObserved", Q_ARG(bool, false))); + QTRY_COMPARE_WITH_TIMEOUT( + root->property("touchMode").toBool(), + root->property("platformTouchMode").toBool() || root->property("inputTouchDeviceAvailable").toBool(), 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchInputObserved", Q_ARG(bool, true))); + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchMode", Q_ARG(bool, false))); + QCOMPARE(root->property("touchModeOverride").toInt(), pointerTouchMode); + QTRY_VERIFY_WITH_TIMEOUT(!root->property("touchMode").toBool(), 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchMode", Q_ARG(bool, true))); + QCOMPARE(root->property("touchModeOverride").toInt(), touchDensityMode); + QTRY_VERIFY_WITH_TIMEOUT(root->property("touchMode").toBool(), 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTouchModePreference", Q_ARG(int, 42))); + QCOMPARE(root->property("touchModeOverride").toInt(), automaticTouchMode); + QCOMPARE(root->property("settingsTouchModeOverride").toInt(), automaticTouchMode); +} + +void QGCStyleTest::_styleTypographyMetrics() +{ + static constexpr char qml[] = R"( +import QtQuick +import QGCStyle as QGCStyle + +QtObject { + readonly property FontInfo _bodyFontInfo: FontInfo { + font: QGCStyle.StyleTypography.bodyFont + } + readonly property FontMetrics _captionFontMetrics: FontMetrics { + font: QGCStyle.StyleTypography.captionFont + } + readonly property TextMetrics _captionTextMetrics: TextMetrics { + font: QGCStyle.StyleTypography.captionFont + text: "X" + } + readonly property FontInfo _fixedFontInfo: FontInfo { + font: QGCStyle.StyleTypography.fixedFont + } + readonly property FontInfo _monospaceFontInfo: FontInfo { + font.family: "monospace" + } + readonly property FontInfo _systemFixedFontInfo: FontInfo { + font: QGCStyle.SystemFonts.fixedFont + } + readonly property FontMetrics _headingFontMetrics: FontMetrics { + font: QGCStyle.StyleTypography.headingFont + } + readonly property TextMetrics _headingTextMetrics: TextMetrics { + font: QGCStyle.StyleTypography.headingFont + text: "X" + } + readonly property TextMetrics _numericNarrowMetrics: TextMetrics { + font: QGCStyle.StyleTypography.numericFont + text: "1111" + } + readonly property TextMetrics _numericWideMetrics: TextMetrics { + font: QGCStyle.StyleTypography.numericFont + text: "8888" + } + readonly property FontMetrics _titleFontMetrics: FontMetrics { + font: QGCStyle.StyleTypography.titleFont + } + readonly property TextMetrics _titleTextMetrics: TextMetrics { + font: QGCStyle.StyleTypography.titleFont + text: "X" + } + readonly property real bodyAscent: QGCStyle.StyleTypography.bodyAscent + readonly property real bodyAverageCharacterWidth: QGCStyle.StyleTypography.bodyAverageCharacterWidth + readonly property real bodyBaselineOffset: QGCStyle.StyleTypography.bodyBaselineOffset + readonly property real bodyCapitalHeight: QGCStyle.StyleTypography.bodyCapitalHeight + readonly property real bodyDescent: QGCStyle.StyleTypography.bodyDescent + readonly property string bodyFontFamily: QGCStyle.StyleTypography.bodyFont.family + readonly property real bodyFontHeight: QGCStyle.StyleTypography.bodyFontHeight + readonly property real bodyFontPointSize: QGCStyle.StyleTypography.bodyFont.pointSize + readonly property real bodyLineSpacing: QGCStyle.StyleTypography.bodyLineSpacing + readonly property real bodyPixelHeight: QGCStyle.StyleTypography.bodyPixelHeight + readonly property real bodyPixelWidth: QGCStyle.StyleTypography.bodyPixelWidth + readonly property real bodyPointSize: QGCStyle.StyleTypography.bodyPointSize + readonly property real captionAscent: QGCStyle.StyleTypography.captionAscent + readonly property real captionBaselineOffset: QGCStyle.StyleTypography.captionBaselineOffset + readonly property real captionDescent: QGCStyle.StyleTypography.captionDescent + readonly property real captionFontHeight: QGCStyle.StyleTypography.captionFontHeight + readonly property real captionLineSpacing: QGCStyle.StyleTypography.captionLineSpacing + readonly property real captionPixelHeight: QGCStyle.StyleTypography.captionPixelHeight + readonly property real captionPixelWidth: QGCStyle.StyleTypography.captionPixelWidth + readonly property real captionPointSize: QGCStyle.StyleTypography.captionPointSize + readonly property int controlContentHeight: QGCStyle.StyleMetrics.controlContentHeight + readonly property int controlHeight: QGCStyle.StyleMetrics.controlHeight + readonly property int controlVerticalPadding: QGCStyle.StyleMetrics.controlVerticalPadding + readonly property real defaultDialogControlSpacing: QGCStyle.StyleMetrics.defaultDialogControlSpacing + readonly property string fixedFontFamily: QGCStyle.StyleTypography.fixedFontFamily + readonly property bool fixedFontIsFixedPitch: _fixedFontInfo.fixedPitch + readonly property string monospaceFontFamily: _monospaceFontInfo.family + readonly property bool monospaceFontIsFixedPitch: _monospaceFontInfo.fixedPitch + readonly property real headingAscent: QGCStyle.StyleTypography.headingAscent + readonly property real headingBaselineOffset: QGCStyle.StyleTypography.headingBaselineOffset + readonly property real headingDescent: QGCStyle.StyleTypography.headingDescent + readonly property real headingFontHeight: QGCStyle.StyleTypography.headingFontHeight + readonly property real headingLineSpacing: QGCStyle.StyleTypography.headingLineSpacing + readonly property real headingPixelHeight: QGCStyle.StyleTypography.headingPixelHeight + readonly property real headingPixelWidth: QGCStyle.StyleTypography.headingPixelWidth + readonly property real headingPointSize: QGCStyle.StyleTypography.headingPointSize + readonly property int inlineIconSize: QGCStyle.StyleMetrics.inlineIconSize + readonly property int minimumInteractiveHeight: QGCStyle.StyleMetrics.minimumInteractiveHeight + readonly property int numericTabularFeature: QGCStyle.StyleTypography.numericFont.features["tnum"] + readonly property real numericWideWidth: _numericWideMetrics.advanceWidth + readonly property real numericNarrowWidth: _numericNarrowMetrics.advanceWidth + readonly property string resolvedBodyFontFamily: _bodyFontInfo.family + readonly property font systemFixedFont: QGCStyle.SystemFonts.fixedFont + readonly property string systemFixedFontFamily: _systemFixedFontInfo.family + readonly property bool systemFixedFontIsFixedPitch: _systemFixedFontInfo.fixedPitch + readonly property real titleAscent: QGCStyle.StyleTypography.titleAscent + readonly property real titleBaselineOffset: QGCStyle.StyleTypography.titleBaselineOffset + readonly property real titleDescent: QGCStyle.StyleTypography.titleDescent + readonly property real titleFontHeight: QGCStyle.StyleTypography.titleFontHeight + readonly property real titleLineSpacing: QGCStyle.StyleTypography.titleLineSpacing + readonly property real titlePixelHeight: QGCStyle.StyleTypography.titlePixelHeight + readonly property real titlePixelWidth: QGCStyle.StyleTypography.titlePixelWidth + readonly property real titlePointSize: QGCStyle.StyleTypography.titlePointSize + + readonly property real expectedCaptionPixelHeight: Math.round(_captionFontMetrics.height / 2) * 2 + readonly property real expectedCaptionPixelWidth: Math.round(_captionTextMetrics.advanceWidth / 2) * 2 + readonly property real expectedHeadingPixelHeight: Math.round(_headingFontMetrics.height / 2) * 2 + readonly property real expectedHeadingPixelWidth: Math.round(_headingTextMetrics.advanceWidth / 2) * 2 + readonly property real expectedTitlePixelHeight: Math.round(_titleFontMetrics.height / 2) * 2 + readonly property real expectedTitlePixelWidth: Math.round(_titleTextMetrics.advanceWidth / 2) * 2 + + function setTypographyScale(platformPointSize: real, scaleFactor: real) { + QGCStyle.StyleTypography.platformPointSize = platformPointSize + QGCStyle.StyleTypography.scaleFactor = scaleFactor + } + + function setTypographyFontFamily(fontFamily: string) { + QGCStyle.StyleTypography.normalFontFamily = fontFamily + } +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCStyleTypographyTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTypographyScale", Q_ARG(double, 8.), Q_ARG(double, 1.))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("bodyPointSize").toReal(), 8., 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("bodyFontPointSize").toReal(), 8., 5000); + QVERIFY( + QMetaObject::invokeMethod(root.data(), "setTypographyFontFamily", Q_ARG(QString, QStringLiteral("Open Sans")))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("bodyFontFamily").toString(), QStringLiteral("Open Sans"), 5000); + QCOMPARE(root->property("bodyFontFamily"), root->property("resolvedBodyFontFamily")); + QVERIFY(root->property("bodyAscent").toReal() > 0.); + QCOMPARE(root->property("bodyBaselineOffset"), root->property("bodyAscent")); + QVERIFY(root->property("bodyAverageCharacterWidth").toReal() > 0.); + QVERIFY(root->property("bodyCapitalHeight").toReal() > 0.); + QVERIFY(root->property("bodyFontHeight").toReal() > 0.); + QVERIFY(root->property("bodyLineSpacing").toReal() >= root->property("bodyFontHeight").toReal()); + QCOMPARE(root->property("captionPointSize").toReal(), 6.); + QCOMPARE(root->property("headingPointSize").toReal(), 10.); + QCOMPARE(root->property("titlePointSize").toReal(), 12.); + QCOMPARE(root->property("captionBaselineOffset"), root->property("captionAscent")); + QCOMPARE(root->property("headingBaselineOffset"), root->property("headingAscent")); + QCOMPARE(root->property("titleBaselineOffset"), root->property("titleAscent")); + QVERIFY(root->property("captionDescent").toReal() >= 0.); + QVERIFY(root->property("headingDescent").toReal() >= 0.); + QVERIFY(root->property("titleDescent").toReal() >= 0.); + QVERIFY(root->property("captionLineSpacing").toReal() >= root->property("captionFontHeight").toReal()); + QVERIFY(root->property("headingLineSpacing").toReal() >= root->property("headingFontHeight").toReal()); + QVERIFY(root->property("titleLineSpacing").toReal() >= root->property("titleFontHeight").toReal()); + QCOMPARE(root->property("captionPixelHeight"), root->property("expectedCaptionPixelHeight")); + QCOMPARE(root->property("captionPixelWidth"), root->property("expectedCaptionPixelWidth")); + QCOMPARE(root->property("headingPixelHeight"), root->property("expectedHeadingPixelHeight")); + QCOMPARE(root->property("headingPixelWidth"), root->property("expectedHeadingPixelWidth")); + QCOMPARE(root->property("titlePixelHeight"), root->property("expectedTitlePixelHeight")); + QCOMPARE(root->property("titlePixelWidth"), root->property("expectedTitlePixelWidth")); + QCOMPARE(root->property("defaultDialogControlSpacing").toInt(), + qMax(1, qRound(root->property("bodyLineSpacing").toReal() / 2.))); + QTRY_VERIFY_WITH_TIMEOUT(root->property("bodyPixelWidth").toReal() > 0., 5000); + QVERIFY(root->property("fixedFontIsFixedPitch").toBool()); + QCOMPARE(root->property("fixedFontFamily"), root->property("systemFixedFontFamily")); + QVERIFY(root->property("monospaceFontIsFixedPitch").toBool()); + QCOMPARE(root->property("systemFixedFont").value(), QFontDatabase::systemFont(QFontDatabase::FixedFont)); + QVERIFY(root->property("systemFixedFontIsFixedPitch").toBool()); + QCOMPARE(root->property("numericTabularFeature").toInt(), 1); + QVERIFY(qAbs(root->property("numericNarrowWidth").toReal() - root->property("numericWideWidth").toReal()) < 0.01); + QCOMPARE(root->property("controlContentHeight").toInt(), qCeil(root->property("bodyFontHeight").toReal())); + QCOMPARE(root->property("controlHeight").toInt(), qMax(root->property("minimumInteractiveHeight").toInt(), + root->property("controlContentHeight").toInt() + + root->property("controlVerticalPadding").toInt() * 2)); + QCOMPARE(root->property("inlineIconSize").toInt(), qMax(1, qCeil(root->property("bodyCapitalHeight").toReal()))); + const double compactBodyPixelWidth = root->property("bodyPixelWidth").toReal(); + const double compactCaptionPixelHeight = root->property("captionPixelHeight").toReal(); + const double compactHeadingPixelHeight = root->property("headingPixelHeight").toReal(); + const double compactTitlePixelHeight = root->property("titlePixelHeight").toReal(); + QVERIFY(QMetaObject::invokeMethod(root.data(), "setTypographyScale", Q_ARG(double, 12.), Q_ARG(double, 1.25))); + QTRY_COMPARE_WITH_TIMEOUT(root->property("bodyPointSize").toReal(), 15., 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("bodyPixelHeight").toReal() > 0., 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("bodyPixelWidth").toReal() > compactBodyPixelWidth, 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("captionPixelHeight").toReal() > compactCaptionPixelHeight, 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("headingPixelHeight").toReal() > compactHeadingPixelHeight, 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("titlePixelHeight").toReal() > compactTitlePixelHeight, 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("bodyDescent").toReal() >= 0., 5000); +} + +void QGCStyleTest::_styleKitApplies() +{ + QGCPalette::setGlobalTheme(QGCPalette::Light); + + static constexpr char qml[] = R"( +import QtQuick +import Qt.labs.StyleKit as Labs +import QGCStyle as QGCStyle +import QGroundControl.Controls + +QGCStyle.ApplicationWindow { + id: root + + width: 640 + height: 480 + visible: true + + readonly property int animationDuration: QGCStyle.StyleMetrics.animationDuration + readonly property var activeStyle: Labs.StyleKit.style + readonly property string activeThemeName: activeStyle.themeName + readonly property bool controlsLoaded: controlsLoader.status === Loader.Ready + readonly property string expectedFontFamily: QGCStyle.StyleTypography.normalFontFamily + readonly property real expectedFontPointSize: QGCStyle.StyleTypography.bodyPointSize + readonly property int fastAnimationDuration: QGCStyle.StyleMetrics.fastAnimationDuration + readonly property int normalAnimationDuration: QGCStyle.StyleMetrics.normalAnimationDuration + readonly property bool reducedMotion: QGCStyle.StylePreferences.reducedMotion + readonly property int slowAnimationDuration: QGCStyle.StyleMetrics.slowAnimationDuration + readonly property color styleHighlightColor: activeStyle.palette.highlight + readonly property bool touchDeviceAvailable: QGCStyle.InputCapabilities.touchDeviceAvailable + readonly property bool touchInputObserved: QGCStyle.StylePreferences.touchInputObserved + readonly property bool transitionsEnabled: Labs.StyleKit.transitionsEnabled + readonly property string windowFontFamily: root.font.family + readonly property real windowFontPointSize: root.font.pointSize + readonly property color windowColor: root.palette.window + + function isStyleLoaded(): bool { + return Labs.StyleKit.styleLoaded() + } + + function loadControls() { + controlsLoader.active = true + } + + function setReducedMotion(enabled: bool) { + QGCStyle.StylePreferences.setReducedMotion(enabled) + } + + Loader { + id: controlsLoader + + active: false + sourceComponent: Component { + Item { + objectName: "styleKitControls" + + readonly property real compactBackgroundHeight: compactButton.background.delegateStyle.implicitHeight + readonly property real expectedScrollBarExtent: QGCStyle.StyleMetrics.scrollBarExtent + readonly property real expectedCompactBackgroundHeight: QGCStyle.StyleMetrics.iconSize + QGCStyle.StyleMetrics.spacing + readonly property string expectedFontFamily: QGCStyle.StyleTypography.normalFontFamily + readonly property color expectedLabelColor: root.palette.windowText + readonly property real expectedFontPointSize: QGCStyle.StyleTypography.bodyPointSize + readonly property color labelColor: qgcLabel.color + readonly property string labelFontFamily: qgcLabel.font.family + readonly property real labelFontPointSize: qgcLabel.font.pointSize + readonly property real expectedNormalBackgroundHeight: QGCStyle.StyleMetrics.controlHeight + readonly property real normalBackgroundHeight: normalButton.background.delegateStyle.implicitHeight + readonly property string normalFontFamily: normalButton.font.family + readonly property real normalFontPointSize: normalButton.font.pointSize + readonly property int numericFieldTabularFeature: numericField.font.features["tnum"] + readonly property color primaryBackgroundColor: primaryButton.background.delegateStyle.color + readonly property bool primaryFontBold: primaryButton.font.bold + readonly property real scrollBarBackgroundHeight: horizontalScrollBar.background.delegateStyle.implicitHeight + readonly property real scrollIndicatorBackgroundHeight: horizontalScrollIndicator.background.delegateStyle.implicitHeight + + Column { + QGCLabel { + id: qgcLabel + text: qsTr("QGC label") + } + QGCTextField { + id: numericField + numericValuesOnly: true + text: "1234" + } + Labs.Button { + id: normalButton + text: qsTr("Button") + } + Labs.Button { + id: primaryButton + text: qsTr("Primary") + Labs.StyleVariation.variations: ["primary"] + } + Labs.Button { + id: compactButton + text: qsTr("Compact") + Labs.StyleVariation.variations: ["compact"] + } + Labs.CheckBox { text: qsTr("Check box"); checked: true } + Labs.ComboBox { model: [qsTr("One"), qsTr("Two")] } + Labs.ProgressBar { value: 0.5 } + Labs.RadioButton { text: qsTr("Radio"); checked: true } + Labs.Slider { value: 0.5 } + Labs.Switch { text: qsTr("Switch"); checked: true } + Labs.TextArea { text: qsTr("Text area") } + Labs.TextField { text: qsTr("Text field") } + Labs.ScrollBar { + id: horizontalScrollBar + orientation: Qt.Horizontal + } + Labs.ScrollIndicator { + id: horizontalScrollIndicator + orientation: Qt.Horizontal + } + } + } + } + } +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/QGCStyleKitTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + const bool originalReducedMotion = root->property("reducedMotion").toBool(); + const auto restoreReducedMotion = qScopeGuard([&root, originalReducedMotion]() { + if (root) { + (void) QMetaObject::invokeMethod(root.data(), "setReducedMotion", Q_ARG(bool, originalReducedMotion)); + } + }); + + const auto styleLoaded = [&root]() { + bool result = false; + return QMetaObject::invokeMethod(root.data(), "isStyleLoaded", Q_RETURN_ARG(bool, result)) && result; + }; + QVERIFY(root->property("activeStyle").value()); + QTRY_VERIFY_WITH_TIMEOUT(styleLoaded(), 5000); + QCOMPARE(root->property("activeThemeName").toString(), QStringLiteral("QGCPalette")); + QCOMPARE(root->property("windowFontFamily"), root->property("expectedFontFamily")); + QCOMPARE(root->property("windowFontPointSize"), root->property("expectedFontPointSize")); + const QColor lightWindowColor = root->property("windowColor").value(); + const bool originalTouchDeviceAvailable = root->property("touchDeviceAvailable").toBool(); + + QWindow* const window = qobject_cast(root.data()); + QVERIFY(window); + QVERIFY(QTest::qWaitForWindowExposed(window)); + QScopedPointer touchDevice(QTest::createTouchDevice()); + QVERIFY(touchDevice); + QTest::touchEvent(window, touchDevice.data()).press(0, QPoint(10, 10)); + QTRY_VERIFY_WITH_TIMEOUT(root->property("touchDeviceAvailable").toBool(), 5000); + QTRY_VERIFY_WITH_TIMEOUT(root->property("touchInputObserved").toBool(), 5000); + QTest::touchEvent(window, touchDevice.data()).release(0, QPoint(10, 10)); + touchDevice.reset(); + QTRY_COMPARE_WITH_TIMEOUT(root->property("touchDeviceAvailable").toBool(), originalTouchDeviceAvailable, 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "loadControls")); + QTRY_VERIFY_WITH_TIMEOUT(root->property("controlsLoaded").toBool(), 5000); + + QObject* const controls = root->findChild(QStringLiteral("styleKitControls")); + QVERIFY(controls); + QCOMPARE(controls->property("normalBackgroundHeight"), controls->property("expectedNormalBackgroundHeight")); + QCOMPARE(controls->property("compactBackgroundHeight"), controls->property("expectedCompactBackgroundHeight")); + QCOMPARE(controls->property("scrollBarBackgroundHeight"), controls->property("expectedScrollBarExtent")); + QCOMPARE(controls->property("scrollIndicatorBackgroundHeight"), controls->property("expectedScrollBarExtent")); + QVERIFY(controls->property("compactBackgroundHeight").toReal() < + controls->property("normalBackgroundHeight").toReal()); + QVERIFY(controls->property("primaryFontBold").toBool()); + QCOMPARE(controls->property("primaryBackgroundColor"), root->property("styleHighlightColor")); + QCOMPARE(controls->property("normalFontFamily"), controls->property("expectedFontFamily")); + QCOMPARE(controls->property("normalFontPointSize"), controls->property("expectedFontPointSize")); + QCOMPARE(controls->property("numericFieldTabularFeature").toInt(), 1); + QCOMPARE(controls->property("labelColor"), controls->property("expectedLabelColor")); + QCOMPARE(controls->property("labelFontFamily"), controls->property("expectedFontFamily")); + QCOMPARE(controls->property("labelFontPointSize"), controls->property("expectedFontPointSize")); + + QGCPalette::setGlobalTheme(QGCPalette::Dark); + QTRY_VERIFY_WITH_TIMEOUT(root->property("windowColor").value() != lightWindowColor, 5000); + + QGCPalette::setGlobalTheme(QGCPalette::Light); + QTRY_COMPARE_WITH_TIMEOUT(root->property("windowColor").value(), lightWindowColor, 5000); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setReducedMotion", Q_ARG(bool, false))); + QTRY_VERIFY_WITH_TIMEOUT(root->property("transitionsEnabled").toBool(), 5000); + QVERIFY(root->property("animationDuration").toInt() > 0); + QVERIFY(root->property("fastAnimationDuration").toInt() > 0); + QVERIFY(root->property("normalAnimationDuration").toInt() > 0); + QVERIFY(root->property("slowAnimationDuration").toInt() > 0); + + QVERIFY(QMetaObject::invokeMethod(root.data(), "setReducedMotion", Q_ARG(bool, true))); + QTRY_VERIFY_WITH_TIMEOUT(!root->property("transitionsEnabled").toBool(), 5000); + QTRY_COMPARE_WITH_TIMEOUT(root->property("animationDuration").toInt(), 0, 5000); + QCOMPARE(root->property("fastAnimationDuration").toInt(), 0); + QCOMPARE(root->property("normalAnimationDuration").toInt(), 0); + QCOMPARE(root->property("slowAnimationDuration").toInt(), 0); +} + +UT_REGISTER_TEST(QGCStyleTest, TestLabel::Unit) diff --git a/test/QmlControls/QGCStyleTest.h b/test/QmlControls/QGCStyleTest.h new file mode 100644 index 000000000000..d0a9ab5971fd --- /dev/null +++ b/test/QmlControls/QGCStyleTest.h @@ -0,0 +1,27 @@ +#pragma once + +#include "QGCPalette.h" +#include "UnitTest.h" + +class QGCStyleTest : public UnitTest +{ + Q_OBJECT + +private slots: + void init(); + void cleanup(); + void _galleryLoads_data(); + void _galleryLoads(); + void _galleryRenders_data(); + void _galleryRenders(); + void _previewRenders_data(); + void _previewRenders(); + void _layoutProfile(); + void _liveUiScaleReflow(); + void _stylePreferencesTouchMode(); + void _styleTypographyMetrics(); + void _styleKitApplies(); + +private: + QGCPalette::Theme _originalTheme = QGCPalette::Dark; +}; diff --git a/test/QmlControls/ScreenToolsTest.cc b/test/QmlControls/ScreenToolsTest.cc new file mode 100644 index 000000000000..19ac9e661e24 --- /dev/null +++ b/test/QmlControls/ScreenToolsTest.cc @@ -0,0 +1,178 @@ +#include "ScreenToolsTest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AppSettings.h" +#include "Fact.h" +#include "QGCApplication.h" +#include "SettingsManager.h" + +void ScreenToolsTest::_compatibilityFacade() +{ + static constexpr char qml[] = R"( +import QtQuick +import QGCStyle as QGCStyle +import QGroundControl +import QGroundControl.Controls + +QtObject { + readonly property bool isDebugBuild: QGroundControl.isDebugBuild + readonly property QtObject mockScreen: QtObject { + property real height: 678 + property real pixelDensity: 3.25 + property real width: 1234 + } + readonly property string platformOs: Qt.platform.os + readonly property var screenTools: ScreenTools + readonly property var styleEnvironment: QGCStyleEnvironment + readonly property var styleMetrics: QGCStyle.StyleMetrics + readonly property var styleTypography: QGCStyle.StyleTypography +} +)"; + + QQmlEngine engine; + engine.addImportPath(QStringLiteral("qrc:/qml")); + QQmlComponent component(&engine); + component.setData(qml, QUrl(QStringLiteral("qrc:/tests/ScreenToolsCompatibilityTest.qml"))); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + + QScopedPointer root(component.create()); + QVERIFY2(root, qPrintable(component.errorString())); + + QObject* const screenTools = root->property("screenTools").value(); + QObject* const mockScreen = root->property("mockScreen").value(); + QObject* const styleMetrics = root->property("styleMetrics").value(); + QObject* const styleTypography = root->property("styleTypography").value(); + QVERIFY(screenTools); + QVERIFY(mockScreen); + QVERIFY(styleMetrics); + QVERIFY(styleTypography); + + static constexpr std::array metricMappings = { + std::pair{"implicitButtonWidth", "implicitButtonWidth"}, + std::pair{"implicitButtonHeight", "implicitButtonHeight"}, + std::pair{"implicitCheckBoxHeight", "implicitCheckBoxHeight"}, + std::pair{"implicitTextFieldWidth", "implicitTextFieldWidth"}, + std::pair{"implicitTextFieldHeight", "implicitTextFieldHeight"}, + std::pair{"implicitComboBoxHeight", "implicitComboBoxHeight"}, + std::pair{"implicitComboBoxWidth", "implicitComboBoxWidth"}, + std::pair{"comboBoxPadding", "comboBoxPadding"}, + std::pair{"implicitSliderHeight", "implicitSliderHeight"}, + std::pair{"defaultBorderRadius", "defaultBorderRadius"}, + std::pair{"defaultDialogControlSpacing", "defaultDialogControlSpacing"}, + std::pair{"radioButtonIndicatorSize", "radioButtonIndicatorSize"}, + std::pair{"minTouchMillimeters", "minimumTouchMillimeters"}, + std::pair{"toolbarHeight", "toolbarHeight"}, + }; + for (const auto& [screenToolsProperty, styleProperty] : metricMappings) { + QCOMPARE(screenTools->property(screenToolsProperty), styleMetrics->property(styleProperty)); + } + + static constexpr std::array typographyMappings = { + std::pair{"defaultFontDescent", "bodyDescent"}, std::pair{"defaultFontPixelHeight", "bodyPixelHeight"}, + std::pair{"defaultFontPixelWidth", "bodyPixelWidth"}, std::pair{"defaultFontPointSize", "bodyPointSize"}, + std::pair{"largeFontPixelHeight", "titlePixelHeight"}, std::pair{"largeFontPixelWidth", "titlePixelWidth"}, + std::pair{"largeFontPointRatio", "titleScale"}, std::pair{"largeFontPointSize", "titlePointSize"}, + std::pair{"mediumFontPointRatio", "headingScale"}, std::pair{"mediumFontPointSize", "headingPointSize"}, + std::pair{"platformFontPointSize", "platformPointSize"}, std::pair{"smallFontPointRatio", "captionScale"}, + std::pair{"smallFontPointSize", "captionPointSize"}, std::pair{"fixedFontFamily", "fixedFontFamily"}, + std::pair{"normalFontFamily", "normalFontFamily"}, + }; + for (const auto& [screenToolsProperty, styleProperty] : typographyMappings) { + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property(screenToolsProperty), styleTypography->property(styleProperty), + 5000); + } + + QScreen* const expectedScreen = QGuiApplication::primaryScreen(); + QVERIFY(expectedScreen); + const bool fakeMobile = screenTools->property("isFakeMobile").toBool(); +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + const bool expectedFakeMobile = false; +#else + const bool expectedFakeMobile = QCoreApplication::arguments().contains(QStringLiteral("--fake-mobile")) || + QCoreApplication::arguments().contains(QStringLiteral("-fake-mobile")); +#endif + QCOMPARE(fakeMobile, expectedFakeMobile); + QCOMPARE(screenTools->property("screenWidth").toInt(), fakeMobile ? 731 : expectedScreen->size().width()); + QCOMPARE(screenTools->property("screenHeight").toInt(), fakeMobile ? 411 : expectedScreen->size().height()); + const qreal expectedPixelDensity = expectedScreen->physicalDotsPerInch() / 25.4; + QCOMPARE(screenTools->property("_screenPixelDensity").toReal(), + expectedPixelDensity > 0 ? expectedPixelDensity : 1); + + QObject* const layoutProfile = screenTools->property("_layoutProfile").value(); + QVERIFY(layoutProfile); + QCOMPARE(screenTools->property("isShortScreen"), layoutProfile->property("isShort")); + QCOMPARE(screenTools->property("isTinyScreen"), layoutProfile->property("isTiny")); + QCOMPARE(screenTools->property("minTouchPixels"), layoutProfile->property("minimumTouchTarget")); + QCOMPARE(screenTools->property("realPixelDensity"), layoutProfile->property("pixelDensity")); + + if (!fakeMobile) { + const QVariant originalWindowScreen = screenTools->property("_windowScreen"); + const auto restoreWindowScreen = qScopeGuard( + [screenTools, originalWindowScreen]() { screenTools->setProperty("_windowScreen", originalWindowScreen); }); + + QVERIFY(screenTools->setProperty("_windowScreen", QVariant::fromValue(mockScreen))); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("screenWidth").toReal(), 1234., 5000); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("screenHeight").toReal(), 678., 5000); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("_screenPixelDensity").toReal(), 3.25, 5000); + + QVERIFY(screenTools->setProperty("_windowScreen", QVariant::fromValue(static_cast(nullptr)))); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("screenWidth").toInt(), expectedScreen->size().width(), 5000); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("screenHeight").toInt(), expectedScreen->size().height(), 5000); + QTRY_COMPARE_WITH_TIMEOUT(screenTools->property("_screenPixelDensity").toReal(), + expectedPixelDensity > 0 ? expectedPixelDensity : 1, 5000); + } + + const QString platformOs = root->property("platformOs").toString(); + QCOMPARE(screenTools->property("isAndroid").toBool(), platformOs == QStringLiteral("android")); + QCOMPARE(screenTools->property("isiOS").toBool(), platformOs == QStringLiteral("ios")); + QCOMPARE(screenTools->property("isMobile").toBool(), + screenTools->property("isAndroid").toBool() || screenTools->property("isiOS").toBool() || fakeMobile); + QCOMPARE(screenTools->metaObject()->indexOfProperty("hasTouch"), -1); + QCOMPARE(screenTools->metaObject()->indexOfProperty("isDebug"), -1); + QCOMPARE(screenTools->metaObject()->indexOfProperty("isSerialAvailable"), -1); + QCOMPARE(screenTools->metaObject()->indexOfProperty("_systemFixedFontFamily"), -1); +#ifdef QT_DEBUG + QVERIFY(root->property("isDebugBuild").toBool()); +#else + QVERIFY(!root->property("isDebugBuild").toBool()); +#endif + QCOMPARE(styleTypography->property("normalFontFamily").toString(), QGuiApplication::font().family()); +} + +void ScreenToolsTest::_runtimeLanguageFontSwitch() +{ + Fact* const languageFact = SettingsManager::instance()->appSettings()->qLocaleLanguage(); + QVERIFY(languageFact); + const QVariant originalLanguage = languageFact->rawValue(); + ignoreLogMessage("API.QGCApplication", QtWarningMsg, + QRegularExpression(R"(Qt lib localization for ".+" is not present)")); + ignoreLogMessage("API.QGCApplication", QtWarningMsg, + QRegularExpression(R"(Error loading source localization for ".+")")); + ignoreLogMessage("API.QGCApplication", QtWarningMsg, + QRegularExpression(R"(Error loading json localization for ".+")")); + const auto restoreLanguage = + qScopeGuard([languageFact, originalLanguage]() { languageFact->setRawValue(originalLanguage); }); + + languageFact->setRawValue(QLocale::English); + QTRY_COMPARE_WITH_TIMEOUT(qgcApp()->getCurrentLanguage().language(), QLocale::English, 5000); + QTRY_COMPARE_WITH_TIMEOUT(QGuiApplication::font().family(), QStringLiteral("Open Sans"), 5000); + + languageFact->setRawValue(QLocale::Korean); + QTRY_COMPARE_WITH_TIMEOUT(qgcApp()->getCurrentLanguage().language(), QLocale::Korean, 5000); + QTRY_COMPARE_WITH_TIMEOUT(QGuiApplication::font().family(), QStringLiteral("NanumGothic"), 5000); + + languageFact->setRawValue(QLocale::English); + QTRY_COMPARE_WITH_TIMEOUT(QGuiApplication::font().family(), QStringLiteral("Open Sans"), 5000); +} + +UT_REGISTER_TEST(ScreenToolsTest, TestLabel::Unit) diff --git a/test/QmlControls/ScreenToolsTest.h b/test/QmlControls/ScreenToolsTest.h new file mode 100644 index 000000000000..1137443014e1 --- /dev/null +++ b/test/QmlControls/ScreenToolsTest.h @@ -0,0 +1,12 @@ +#pragma once + +#include "UnitTest.h" + +class ScreenToolsTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _compatibilityFacade(); + void _runtimeLanguageFontSwitch(); +}; diff --git a/test/QmlUITests/QmlUITestBase.cc b/test/QmlUITests/QmlUITestBase.cc index 8a9d6f3ba4c2..dafad0adb5b3 100644 --- a/test/QmlUITests/QmlUITestBase.cc +++ b/test/QmlUITests/QmlUITestBase.cc @@ -75,7 +75,7 @@ void QmlUITestBase::startUI() // warning that would trip the strict-mode log check. static bool s_styleSet = false; if (!s_styleSet) { - QQuickStyle::setStyle("Basic"); + QQuickStyle::setStyle("QGCStyle"); s_styleSet = true; } QGCCorePlugin::instance()->init(); diff --git a/tools/tests/test_install_dependencies.py b/tools/tests/test_install_dependencies.py index b96fcd48e79c..1c7108c606d8 100644 --- a/tools/tests/test_install_dependencies.py +++ b/tools/tests/test_install_dependencies.py @@ -99,6 +99,16 @@ def test_sysroot_script_single_sources_cross_arm64() -> None: ) +def test_sysroot_script_retries_partial_apt_updates() -> None: + script = (REPO_ROOT / "deploy" / "docker" / "install-sysroot-aarch64.sh").read_text() + assert "APT::Update::Error-Mode=any" in script + + +def test_sysroot_script_uses_https_arm64_mirror() -> None: + script = (REPO_ROOT / "deploy" / "docker" / "install-sysroot-aarch64.sh").read_text() + assert 'MIRROR="${MIRROR:-https://ports.ubuntu.com/ubuntu-ports}"' in script + + def test_get_debian_packages_no_duplicates() -> None: pkgs = get_debian_packages() assert len(pkgs) == len(set(pkgs))