diff --git a/initialstart/CMakeLists.txt b/initialstart/CMakeLists.txt index 07cead8e..00a6ab44 100644 --- a/initialstart/CMakeLists.txt +++ b/initialstart/CMakeLists.txt @@ -7,6 +7,10 @@ add_subdirectory(modules) add_executable(plasma-mobile-initial-start main.cpp + devicecontext.cpp + devicecontext.h + setupstate.cpp + setupstate.h wizard.cpp wizard.h settings.cpp diff --git a/initialstart/devicecontext.cpp b/initialstart/devicecontext.cpp new file mode 100644 index 00000000..074b9d64 --- /dev/null +++ b/initialstart/devicecontext.cpp @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "devicecontext.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ +bool pathExists(const QString &path) +{ + return QFileInfo::exists(path); +} + +bool systemHasBattery() +{ + const QDir powerSupply(QStringLiteral("/sys/class/power_supply")); + const QStringList entries = powerSupply.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &entry : entries) { + if (entry.startsWith(QStringLiteral("BAT"), Qt::CaseInsensitive)) { + return true; + } + if (pathExists(powerSupply.filePath(entry + QStringLiteral("/type")))) { + QFile typeFile(powerSupply.filePath(entry + QStringLiteral("/type"))); + if (typeFile.open(QIODevice::ReadOnly) && QString::fromUtf8(typeFile.readAll()).trimmed() == QLatin1String("Battery")) { + return true; + } + } + } + return false; +} +} + +DeviceContext::DeviceContext(QObject *parent) + : QObject(parent) + , m_runtimePlatform(KRuntimePlatform::runtimePlatform().join(QLatin1Char(','))) + , m_hasBattery(systemHasBattery()) +{ + const QList devices = QInputDevice::devices(); + for (const QInputDevice *device : devices) { + if (!device) { + continue; + } + const auto type = device->type(); + m_hasTouch = m_hasTouch || type == QInputDevice::DeviceType::TouchScreen; + m_hasKeyboard = m_hasKeyboard || type == QInputDevice::DeviceType::Keyboard; + m_hasMouse = m_hasMouse || type == QInputDevice::DeviceType::Mouse; + } + + const QList screens = QGuiApplication::screens(); + m_displayCount = std::max(1, static_cast(screens.size())); + if (QScreen *screen = QGuiApplication::primaryScreen()) { + const QSize size = screen->geometry().size(); + m_primaryDisplayLandscape = size.width() >= size.height(); + } + + const QString platform = m_runtimePlatform; + if (m_displayCount > 1) { + m_recommendedDeviceClass = QStringLiteral("dual-screen"); + m_recommendedExperienceProfile = QStringLiteral("hybrid"); + } else if (platform.contains(QStringLiteral("phone"))) { + m_recommendedDeviceClass = QStringLiteral("phone"); + m_recommendedExperienceProfile = QStringLiteral("mobile"); + } else if (platform.contains(QStringLiteral("handheld"))) { + m_recommendedDeviceClass = QStringLiteral("handheld"); + m_recommendedExperienceProfile = QStringLiteral("gaming"); + } else if (platform.contains(QStringLiteral("tablet"))) { + m_recommendedDeviceClass = QStringLiteral("tablet"); + m_recommendedExperienceProfile = QStringLiteral("mobile"); + } else if (m_hasBattery && m_hasTouch && (m_hasKeyboard || m_hasMouse)) { + m_recommendedDeviceClass = QStringLiteral("tablet"); + m_recommendedExperienceProfile = QStringLiteral("hybrid"); + } else if (m_hasBattery && (m_hasKeyboard || m_hasMouse)) { + m_recommendedDeviceClass = QStringLiteral("laptop"); + m_recommendedExperienceProfile = QStringLiteral("desktop"); + } else if (!m_hasBattery && !m_hasTouch) { + m_recommendedDeviceClass = QStringLiteral("desktop"); + m_recommendedExperienceProfile = QStringLiteral("desktop"); + } else { + m_recommendedDeviceClass = QStringLiteral("tablet"); + m_recommendedExperienceProfile = QStringLiteral("mobile"); + } +} + +DeviceContext *DeviceContext::self() +{ + static auto *instance = new DeviceContext(qApp); + return instance; +} + +DeviceContext *DeviceContext::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) +{ + Q_UNUSED(qmlEngine) + Q_UNUSED(jsEngine) + return self(); +} + +QString DeviceContext::runtimePlatform() const +{ + return m_runtimePlatform; +} + +QString DeviceContext::recommendedDeviceClass() const +{ + return m_recommendedDeviceClass; +} + +QString DeviceContext::recommendedExperienceProfile() const +{ + return m_recommendedExperienceProfile; +} + +bool DeviceContext::hasTouch() const +{ + return m_hasTouch; +} + +bool DeviceContext::hasKeyboard() const +{ + return m_hasKeyboard; +} + +bool DeviceContext::hasMouse() const +{ + return m_hasMouse; +} + +bool DeviceContext::hasBattery() const +{ + return m_hasBattery; +} + +int DeviceContext::displayCount() const +{ + return m_displayCount; +} + +bool DeviceContext::hasExternalDisplay() const +{ + return m_displayCount > 1; +} + +bool DeviceContext::primaryDisplayLandscape() const +{ + return m_primaryDisplayLandscape; +} diff --git a/initialstart/devicecontext.h b/initialstart/devicecontext.h new file mode 100644 index 00000000..3ed528dd --- /dev/null +++ b/initialstart/devicecontext.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include + +class QQmlEngine; +class QJSEngine; + +class DeviceContext : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString runtimePlatform READ runtimePlatform CONSTANT) + Q_PROPERTY(QString recommendedDeviceClass READ recommendedDeviceClass CONSTANT) + Q_PROPERTY(QString recommendedExperienceProfile READ recommendedExperienceProfile CONSTANT) + Q_PROPERTY(bool hasTouch READ hasTouch CONSTANT) + Q_PROPERTY(bool hasKeyboard READ hasKeyboard CONSTANT) + Q_PROPERTY(bool hasMouse READ hasMouse CONSTANT) + Q_PROPERTY(bool hasBattery READ hasBattery CONSTANT) + Q_PROPERTY(int displayCount READ displayCount CONSTANT) + Q_PROPERTY(bool hasExternalDisplay READ hasExternalDisplay CONSTANT) + Q_PROPERTY(bool primaryDisplayLandscape READ primaryDisplayLandscape CONSTANT) + +public: + explicit DeviceContext(QObject *parent = nullptr); + + static DeviceContext *self(); + static DeviceContext *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + + QString runtimePlatform() const; + QString recommendedDeviceClass() const; + QString recommendedExperienceProfile() const; + bool hasTouch() const; + bool hasKeyboard() const; + bool hasMouse() const; + bool hasBattery() const; + int displayCount() const; + bool hasExternalDisplay() const; + bool primaryDisplayLandscape() const; + +private: + QString m_runtimePlatform; + QString m_recommendedDeviceClass; + QString m_recommendedExperienceProfile; + bool m_hasTouch = false; + bool m_hasKeyboard = false; + bool m_hasMouse = false; + bool m_hasBattery = false; + int m_displayCount = 1; + bool m_primaryDisplayLandscape = false; +}; diff --git a/initialstart/main.cpp b/initialstart/main.cpp index 80d49b4d..1f66f844 100644 --- a/initialstart/main.cpp +++ b/initialstart/main.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include "settings.h" +#include "setupstate.h" #include "version.h" #include "wizard.h" @@ -19,6 +21,11 @@ std::unique_ptr createParser() { auto parser = std::make_unique(); parser->addOption(QCommandLineOption(QStringLiteral("test-wizard"), i18n("Opens the initial start wizard without modifying configuration"))); + parser->addOption(QCommandLineOption(QStringLiteral("test-apply-defaults"), + i18n("Writes recommended setup defaults and exits without applying them to the running session"))); + parser->addOption(QCommandLineOption(QStringLiteral("test-apply-profile"), + i18n("Writes setup defaults for a profile and exits without applying them to the running session"), + i18n("profile"))); return parser; } @@ -44,14 +51,32 @@ int main(int argc, char *argv[]) aboutData.processCommandLine(parser.get()); const bool testWizard = parser->isSet(QStringLiteral("test-wizard")); + const bool testApplyDefaults = parser->isSet(QStringLiteral("test-apply-defaults")); + const QString testApplyProfile = parser->value(QStringLiteral("test-apply-profile")); + if (testApplyDefaults || !testApplyProfile.isEmpty()) { + if (!testApplyProfile.isEmpty()) { + const QStringList supportedProfiles{QStringLiteral("mobile"), QStringLiteral("desktop"), QStringLiteral("gaming"), QStringLiteral("hybrid")}; + if (!supportedProfiles.contains(testApplyProfile)) { + qCritical() << "Unsupported setup profile" << testApplyProfile; + return 2; + } + SetupState::self()->applyExperienceDefaults(testApplyProfile); + } + + SetupState::self()->apply(false); + return 0; + } + if (!testWizard) { - // if the wizard has already been run, or we aren't in plasma mobile + // if the wizard has already been run with a setup profile if (!Settings::self()->shouldStartWizard()) { - qDebug() << "Wizard will not be started since either it has already been run, or the current session is not Plasma Mobile."; + qDebug() << "Wizard will not be started since it has already been run."; return 0; } } + app.setWindowIcon(QIcon::fromTheme(QStringLiteral("start-here-shift"))); + QQmlApplicationEngine engine; engine.rootContext()->setContextObject(new KLocalizedContext{&engine}); @@ -63,9 +88,7 @@ int main(int argc, char *argv[]) return wizard; }); - engine.load(QUrl(QStringLiteral("qrc:org/kde/plasma/mobileinitialstart/initialstart/qml/Main.qml"))); - - app.setWindowIcon(QIcon::fromTheme(QStringLiteral("start-here-symbolic"))); + engine.load(QUrl(QStringLiteral("qrc:/org/kde/plasma/mobileinitialstart/initialstart/qml/Main.qml"))); return app.exec(); } diff --git a/initialstart/modules/CMakeLists.txt b/initialstart/modules/CMakeLists.txt index e8b335de..f09bb932 100644 --- a/initialstart/modules/CMakeLists.txt +++ b/initialstart/modules/CMakeLists.txt @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2023 Devin Lin # SPDX-License-Identifier: GPL-2.0-or-later +plasma_install_package(deviceprofile org.kde.plasma.mobileinitialstart.deviceprofile mobileinitialstart) +plasma_install_package(experienceprofile org.kde.plasma.mobileinitialstart.experienceprofile mobileinitialstart) plasma_install_package(finished org.kde.plasma.mobileinitialstart.finished mobileinitialstart) plasma_install_package(systemnavigation org.kde.plasma.mobileinitialstart.systemnavigation mobileinitialstart) add_subdirectory(cellular) diff --git a/initialstart/modules/deviceprofile/contents/ui/main.qml b/initialstart/modules/deviceprofile/contents/ui/main.qml new file mode 100644 index 00000000..ef365da7 --- /dev/null +++ b/initialstart/modules/deviceprofile/contents/ui/main.qml @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.plasma.mobileinitialstart.initialstart + +InitialStartModule { + name: i18n("Device") + + contentItem: Item { + id: root + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + function chooseDevice(deviceClass, primaryInput) { + SetupState.applyDeviceDefaults(deviceClass, primaryInput) + } + + function selectedDeviceLabel() { + switch (SetupState.deviceClass) { + case "phone": + return i18n("Phone") + case "tablet": + return i18n("Tablet or 2-in-1") + case "laptop": + return i18n("Laptop") + case "desktop": + return i18n("Desktop PC") + case "handheld": + return i18n("Gaming handheld") + case "mini-pc": + return i18n("Gaming PC") + case "dual-screen": + return i18n("Dual-screen device") + case "foldable": + return i18n("Foldable device") + default: + return SetupState.deviceClass + } + } + + function selectedInputLabel() { + switch (SetupState.primaryInput) { + case "touch": + return i18n("touch") + case "keyboardMouse": + return i18n("keyboard and pointer") + case "gamepad": + return i18n("gamepad") + default: + return SetupState.primaryInput + } + } + + ScrollView { + anchors { + fill: parent + topMargin: Kirigami.Units.gridUnit + } + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: -1 + + ColumnLayout { + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: i18n("What are you setting up SHIFT on?") + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + FormCard.FormTextDelegate { + text: i18n("Current selection") + description: i18n("%1, controlled with %2", root.selectedDeviceLabel(), root.selectedInputLabel()) + } + + FormCard.FormButtonDelegate { + text: i18n("Use Hardware Recommendation") + icon.name: "emblem-ok-symbolic" + onClicked: SetupState.useRecommendedSettings() + } + + FormCard.FormDelegateSeparator {} + + FormCard.FormRadioDelegate { + text: i18n("Phone") + description: i18n("Pocket touch device. Starts with the simple mobile home screen and full-screen apps.") + onClicked: root.chooseDevice("phone", "touch") + + Binding on checked { + value: SetupState.deviceClass === "phone" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Tablet or 2-in-1") + description: i18n("Touch device that may rotate, dock, or use a keyboard. Starts with adaptive defaults.") + onClicked: root.chooseDevice("tablet", "touch") + + Binding on checked { + value: SetupState.deviceClass === "tablet" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Laptop or Desktop") + description: i18n("Keyboard and pointer computer. Starts with windows, Overview, dock, tiling, and snap layouts.") + onClicked: root.chooseDevice(DeviceContext.hasBattery ? "laptop" : "desktop", "keyboardMouse") + + Binding on checked { + value: SetupState.deviceClass === "laptop" || SetupState.deviceClass === "desktop" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Gaming PC or Handheld") + description: i18n("Gamepad-first setup for a handheld, console-style mini PC, or living-room gaming device.") + onClicked: root.chooseDevice(DeviceContext.hasBattery ? "handheld" : "mini-pc", "gamepad") + + Binding on checked { + value: SetupState.deviceClass === "handheld" || SetupState.deviceClass === "mini-pc" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Dual-screen or Foldable") + description: i18n("Hardware whose screen layout changes by posture, hinge, external display, or dock.") + onClicked: root.chooseDevice(DeviceContext.displayCount > 1 ? "dual-screen" : "foldable", SetupState.primaryInput) + + Binding on checked { + value: SetupState.deviceClass === "dual-screen" || SetupState.deviceClass === "foldable" + } + } + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + FormCard.FormTextDelegate { + text: i18n("Detected context") + description: i18n("Recommended: %1 with %2 layout. Displays: %3, Touch: %4, Battery: %5", + DeviceContext.recommendedDeviceClass, + DeviceContext.recommendedExperienceProfile, + DeviceContext.displayCount, + DeviceContext.hasTouch ? i18n("yes") : i18n("no"), + DeviceContext.hasBattery ? i18n("yes") : i18n("no")) + } + } + + Item { + Layout.fillHeight: true + } + } + } + } +} diff --git a/initialstart/modules/deviceprofile/metadata.json b/initialstart/modules/deviceprofile/metadata.json new file mode 100644 index 00000000..0995f2f7 --- /dev/null +++ b/initialstart/modules/deviceprofile/metadata.json @@ -0,0 +1,16 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "Marco Allegretti" + } + ], + "Description": "Device profile initial setup module for SHIFT", + "Id": "org.kde.plasma.mobileinitialstart.deviceprofile", + "License": "EUPL-1.2", + "Name": "Device Profile", + "Website": "https://kde.org" + } +} diff --git a/initialstart/modules/experienceprofile/contents/ui/main.qml b/initialstart/modules/experienceprofile/contents/ui/main.qml new file mode 100644 index 00000000..0d738fad --- /dev/null +++ b/initialstart/modules/experienceprofile/contents/ui/main.qml @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.plasma.mobileinitialstart.initialstart + +InitialStartModule { + name: i18n("Experience") + + contentItem: Item { + id: root + + readonly property real cardWidth: Math.min(Kirigami.Units.gridUnit * 30, root.width - Kirigami.Units.gridUnit * 2) + + function profileLabel(profile) { + switch (profile) { + case "mobile": + return i18n("Touch home") + case "desktop": + return i18n("Desktop windows") + case "gaming": + return i18n("Gamepad gaming") + case "hybrid": + return i18n("Adaptive docked") + default: + return profile + } + } + + ScrollView { + anchors { + fill: parent + topMargin: Kirigami.Units.gridUnit + } + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: -1 + + ColumnLayout { + width: root.width + spacing: Kirigami.Units.gridUnit + + Label { + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + text: i18n("Choose how SHIFT should start after setup.") + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + FormCard.FormRadioDelegate { + text: i18n("Touch home") + description: i18n("Maximized apps for phones and small tablets.") + onClicked: SetupState.applyExperienceDefaults("mobile") + + Binding on checked { + value: SetupState.experienceProfile === "mobile" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Desktop windows") + description: i18n("Windows, tiling, dock, and Overview.") + onClicked: SetupState.applyExperienceDefaults("desktop") + + Binding on checked { + value: SetupState.experienceProfile === "desktop" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Gamepad gaming") + description: i18n("Game Center layout for handheld PCs.") + onClicked: SetupState.applyExperienceDefaults("gaming") + + Binding on checked { + value: SetupState.experienceProfile === "gaming" + } + } + + FormCard.FormRadioDelegate { + text: i18n("Adaptive docked") + description: i18n("Auto-hide panels for tablets and foldables.") + onClicked: SetupState.applyExperienceDefaults("hybrid") + + Binding on checked { + value: SetupState.experienceProfile === "hybrid" + } + } + } + + FormCard.FormCard { + maximumWidth: root.cardWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + FormCard.FormTextDelegate { + text: i18n("Current selection") + description: i18n("%1. Windows: %2, Gaming: %3, Tiling: %4", + root.profileLabel(SetupState.experienceProfile), + SetupState.convergenceModeEnabled ? i18n("on") : i18n("off"), + SetupState.gamingModeEnabled ? i18n("on") : i18n("off"), + SetupState.dynamicTilingEnabled ? i18n("on") : i18n("off")) + } + } + + Item { + Layout.fillHeight: true + } + } + } + } +} diff --git a/initialstart/modules/experienceprofile/metadata.json b/initialstart/modules/experienceprofile/metadata.json new file mode 100644 index 00000000..81e4021d --- /dev/null +++ b/initialstart/modules/experienceprofile/metadata.json @@ -0,0 +1,16 @@ +{ + "KPackageStructure": "KPackage/GenericQML", + "KPlugin": { + "Authors": [ + { + "Email": "", + "Name": "Marco Allegretti" + } + ], + "Description": "Experience profile initial setup module for SHIFT", + "Id": "org.kde.plasma.mobileinitialstart.experienceprofile", + "License": "EUPL-1.2", + "Name": "Experience Profile", + "Website": "https://kde.org" + } +} diff --git a/initialstart/modules/finished/contents/ui/main.qml b/initialstart/modules/finished/contents/ui/main.qml index 28b88669..0645a814 100644 --- a/initialstart/modules/finished/contents/ui/main.qml +++ b/initialstart/modules/finished/contents/ui/main.qml @@ -14,27 +14,29 @@ InitialStartModule { contentItem: Item { id: root - ColumnLayout { anchors.fill: parent anchors.margins: Kirigami.Units.gridUnit + spacing: Kirigami.Units.gridUnit + + Item { Layout.fillHeight: true } + + Kirigami.Icon { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Kirigami.Units.iconSizes.huge + Layout.preferredHeight: Kirigami.Units.iconSizes.huge + source: "start-here-shift" + } Label { Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - text: i18n("Your device is now ready.

Enjoy %1!", InitialStartUtil.distroName) + Layout.alignment: Qt.AlignHCenter + text: i18n("Your device is ready.

SHIFT will start with the %1 experience.", SetupState.experienceProfile) wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter } Item { Layout.fillHeight: true } - - Image { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - fillMode: Image.PreserveAspectFit - source: "konqi-calling.png" - } } } } diff --git a/initialstart/modules/systemnavigation/contents/ui/main.qml b/initialstart/modules/systemnavigation/contents/ui/main.qml index 6715f877..91ad353d 100644 --- a/initialstart/modules/systemnavigation/contents/ui/main.qml +++ b/initialstart/modules/systemnavigation/contents/ui/main.qml @@ -9,7 +9,6 @@ import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.formcard as FormCard import org.kde.plasma.mobileinitialstart.initialstart -import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings InitialStartModule { name: i18n("System Navigation") @@ -49,14 +48,14 @@ InitialStartModule { text: i18n("Gesture navigation") description: i18n("Swipe up from the bottom to see running applications. Flick to go to the home screen.") onClicked: { - if (checked && ShellSettings.Settings.navigationPanelEnabled) { - ShellSettings.Settings.navigationPanelEnabled = false; + if (checked && SetupState.navigationPanelEnabled) { + SetupState.navigationPanelEnabled = false; } - checked = Qt.binding(function () { return !ShellSettings.Settings.navigationPanelEnabled; }); + checked = Qt.binding(function () { return !SetupState.navigationPanelEnabled; }); } Binding on checked { - value: !ShellSettings.Settings.navigationPanelEnabled + value: !SetupState.navigationPanelEnabled } } } @@ -70,14 +69,14 @@ InitialStartModule { text: i18n("Button navigation") description: i18n("Use buttons on a navigation bar to navigate the system.") onClicked: { - if (checked && !ShellSettings.Settings.navigationPanelEnabled) { - ShellSettings.Settings.navigationPanelEnabled = true; + if (checked && !SetupState.navigationPanelEnabled) { + SetupState.navigationPanelEnabled = true; } - checked = Qt.binding(function () { return ShellSettings.Settings.navigationPanelEnabled; }); + checked = Qt.binding(function () { return SetupState.navigationPanelEnabled; }); } Binding on checked { - value: ShellSettings.Settings.navigationPanelEnabled + value: SetupState.navigationPanelEnabled } } } diff --git a/initialstart/qml/LandingComponent.qml b/initialstart/qml/LandingComponent.qml index 8e978e62..39ca3988 100644 --- a/initialstart/qml/LandingComponent.qml +++ b/initialstart/qml/LandingComponent.qml @@ -102,21 +102,20 @@ Item { Label { Layout.fillWidth: true - text: i18n("Welcome to
Plasma Mobile") + text: i18n("Welcome to
SHIFT") horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap font.pointSize: 18 color: "white" - } - + } } ColumnLayout { opacity: root.contentOpacity spacing: Kirigami.Units.largeSpacing - anchors { + anchors { left: parent.left right: parent.right bottom: parent.bottom @@ -125,10 +124,9 @@ Item { bottomMargin: Kirigami.Units.gridUnit * 2 } - Kirigami.Heading { Layout.fillWidth: true - text: i18n("Powered by
%1", InitialStartUtil.distroName) + text: i18n("Built on
%1", InitialStartUtil.distroName) horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap diff --git a/initialstart/qml/Main.qml b/initialstart/qml/Main.qml index 4386d97a..28fcc923 100644 --- a/initialstart/qml/Main.qml +++ b/initialstart/qml/Main.qml @@ -4,6 +4,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 import org.kde.kirigami as Kirigami @@ -12,11 +13,13 @@ import initialstart 1.0 as InitialStart Kirigami.ApplicationWindow { id: root - width: 360 - height: 720 + minimumWidth: Kirigami.Units.gridUnit * 20 + minimumHeight: Kirigami.Units.gridUnit * 28 + width: Math.min(Screen.width, Math.max(Kirigami.Units.gridUnit * 24, Screen.width * 0.42)) + height: Math.min(Screen.height, Math.max(Kirigami.Units.gridUnit * 36, Screen.height * 0.78)) visibility: "Windowed" - title: i18n("Initial Start") + title: i18n("SHIFT Initial Setup") pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn diff --git a/initialstart/qml/Wizard.qml b/initialstart/qml/Wizard.qml index 97b3c27c..ed42e060 100644 --- a/initialstart/qml/Wizard.qml +++ b/initialstart/qml/Wizard.qml @@ -29,42 +29,25 @@ Kirigami.Page { readonly property bool onFinalPage: currentIndex === (stepCount - 1) + function updateStepItems() { + if (stepRepeater.count === 0) { + return; + } + + root.previousStepItem = currentIndex > 0 ? stepRepeater.itemAt(currentIndex - 1) : null; + root.currentStepItem = stepRepeater.itemAt(currentIndex); + root.nextStepItem = currentIndex < stepRepeater.count - 1 ? stepRepeater.itemAt(currentIndex + 1) : null; + } + // step animation // manually doing the animation is more performant and less glitchy with window resize than a SwipeView - property real previousStepItemX: 0 + property real previousStepItemX: -root.width property real currentStepItemX: 0 - property real nextStepItemX: 0 - - NumberAnimation on previousStepItemX { - id: previousStepAnim - duration: 400 - easing.type: Easing.OutExpo - onFinished: { - if (root.previousStepItemX != 0) { - root.previousStepItem.visible = false; - } - } - } - - NumberAnimation on currentStepItemX { - id: currentStepAnim - duration: 400 - easing.type: Easing.OutExpo - } - - NumberAnimation on nextStepItemX { - id: nextStepAnim - duration: 400 - easing.type: Easing.OutExpo - onFinished: { - if (root.nextStepItemX != 0) { - root.nextStepItem.visible = false; - } - } - } + property real nextStepItemX: root.width onStepCountChanged: { // reset position + updateStepItems(); requestPreviousPage(); } @@ -74,54 +57,43 @@ Kirigami.Page { } function requestNextPage() { - if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) { + if (currentIndex >= stepCount - 1) { return; } - previousStepItemX = 0; - currentIndex++; + updateStepItems(); stepHeading.changeText(currentStepItem.name); - currentStepItemX = root.width; - currentStepItem.visible = true; - - previousStepAnim.to = -root.width; - previousStepAnim.restart(); - currentStepAnim.to = 0; - currentStepAnim.restart(); + previousStepItemX = -root.width; + currentStepItemX = 0; + nextStepItemX = root.width; } function requestPreviousPage() { - if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) { - return; - } - if (currentIndex === 0) { root.showingLanding = true; landingComponent.returnToLanding(); } else { - nextStepItemX = 0; - currentIndex--; + updateStepItems(); stepHeading.changeText(currentStepItem.name); - currentStepItemX = -root.width; - currentStepItem.visible = true; - - nextStepAnim.to = root.width; - nextStepAnim.restart(); - currentStepAnim.to = 0; - currentStepAnim.restart(); + previousStepItemX = -root.width; + currentStepItemX = 0; + nextStepItemX = root.width; } } LandingComponent { id: landingComponent anchors.fill: parent + enabled: root.showingLanding + visible: root.showingLanding onRequestNextPage: { root.showingLanding = false; + root.updateStepItems(); stepHeading.changeText(root.currentStepItem.name); } } @@ -136,20 +108,6 @@ Kirigami.Page { x: 0 y: root.showingLanding ? overlaySteps.height : 0 - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutExpo - } - } - - Behavior on y { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutExpo - } - } - // heading for all the wizard steps Label { id: stepHeading @@ -168,24 +126,8 @@ Kirigami.Page { property string toText function changeText(text) { - toText = text; - toHidden.restart(); - } - - NumberAnimation on opacity { - id: toHidden - duration: 200 - to: 0 - onFinished: { - stepHeading.text = stepHeading.toText; - toShown.restart(); - } - } - - NumberAnimation on opacity { - id: toShown - duration: 200 - to: 1 + stepHeading.text = text; + stepHeading.opacity = 1; } } @@ -213,11 +155,12 @@ Kirigami.Page { // setup steps Repeater { + id: stepRepeater model: InitialStart.Wizard.steps delegate: MobileShell.BaseItem { id: item - visible: model.index === 0 // the binding is broken later + visible: !root.showingLanding && Math.abs(item.currentIndex - root.currentIndex) <= 1 contentItem: modelData.contentItem transform: Translate { x: { @@ -238,27 +181,8 @@ Kirigami.Page { property string name: modelData.name property int currentIndex: model.index - function updateRootItems() { - if (model.index === root.currentIndex) { - root.currentStepItem = item; - } else if (model.index === root.currentIndex - 1) { - root.previousStepItem = item; - } else if (model.index === root.currentIndex + 1) { - root.nextStepItem = item; - } - } - Component.onCompleted: { - updateRootItems(); - } - - // keep root properties updated - Connections { - target: root - - function onCurrentIndexChanged() { - item.updateRootItems(); - } + root.updateStepItems(); } } } diff --git a/initialstart/settings.cpp b/initialstart/settings.cpp index e415cd3f..122e1c84 100644 --- a/initialstart/settings.cpp +++ b/initialstart/settings.cpp @@ -4,7 +4,6 @@ #include "settings.h" #include -#include const QString CONFIG_FILE = QStringLiteral("plasmamobilerc"); const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); @@ -12,18 +11,14 @@ const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); Settings::Settings(QObject *parent) : QObject{parent} , m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE)} - , m_isMobilePlatform{KRuntimePlatform::runtimePlatform().contains(QStringLiteral("phone"))} { } bool Settings::shouldStartWizard() { - if (!m_isMobilePlatform) { - return false; - } - - auto group = KConfigGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP}; - return !group.readEntry("wizardRun", false); + const KConfigGroup initialStartGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP}; + const KConfigGroup generalGroup{m_mobileConfig, QStringLiteral("General")}; + return !initialStartGroup.readEntry("wizardRun", false) || generalGroup.readEntry("setupExperienceProfile", QString()).isEmpty(); } void Settings::setWizardFinished() diff --git a/initialstart/settings.h b/initialstart/settings.h index f415f124..6788c7dd 100644 --- a/initialstart/settings.h +++ b/initialstart/settings.h @@ -18,5 +18,4 @@ public: private: KSharedConfig::Ptr m_mobileConfig; - bool m_isMobilePlatform; }; diff --git a/initialstart/setupstate.cpp b/initialstart/setupstate.cpp new file mode 100644 index 00000000..ac0a08da --- /dev/null +++ b/initialstart/setupstate.cpp @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "setupstate.h" +#include "devicecontext.h" + +#include +#include + +#include +#include +#include +#include + +const QString CONFIG_FILE = QStringLiteral("plasmamobilerc"); +const QString GENERAL_CONFIG_GROUP = QStringLiteral("General"); +const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); + +SetupState::SetupState(QObject *parent) + : QObject(parent) +{ + useRecommendedSettings(); +} + +SetupState *SetupState::self() +{ + static auto *instance = new SetupState(qApp); + return instance; +} + +SetupState *SetupState::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) +{ + Q_UNUSED(qmlEngine) + Q_UNUSED(jsEngine) + return self(); +} + +QString SetupState::deviceClass() const +{ + return m_deviceClass; +} + +void SetupState::setDeviceClass(const QString &deviceClass) +{ + if (m_deviceClass == deviceClass) { + return; + } + m_deviceClass = deviceClass; + Q_EMIT deviceClassChanged(); +} + +QString SetupState::experienceProfile() const +{ + return m_experienceProfile; +} + +void SetupState::setExperienceProfile(const QString &experienceProfile) +{ + if (m_experienceProfile == experienceProfile) { + return; + } + m_experienceProfile = experienceProfile; + Q_EMIT experienceProfileChanged(); +} + +QString SetupState::primaryInput() const +{ + return m_primaryInput; +} + +void SetupState::setPrimaryInput(const QString &primaryInput) +{ + if (m_primaryInput == primaryInput) { + return; + } + m_primaryInput = primaryInput; + Q_EMIT primaryInputChanged(); +} + +bool SetupState::convergenceModeEnabled() const +{ + return m_convergenceModeEnabled; +} + +void SetupState::setConvergenceModeEnabled(bool enabled) +{ + if (m_convergenceModeEnabled == enabled) { + return; + } + m_convergenceModeEnabled = enabled; + emitShellSettingsChanged(); +} + +bool SetupState::gamingModeEnabled() const +{ + return m_gamingModeEnabled; +} + +void SetupState::setGamingModeEnabled(bool enabled) +{ + if (m_gamingModeEnabled == enabled) { + return; + } + m_gamingModeEnabled = enabled; + emitShellSettingsChanged(); +} + +bool SetupState::navigationPanelEnabled() const +{ + return m_navigationPanelEnabled; +} + +void SetupState::setNavigationPanelEnabled(bool enabled) +{ + if (m_navigationPanelEnabled == enabled) { + return; + } + m_navigationPanelEnabled = enabled; + emitShellSettingsChanged(); +} + +bool SetupState::dynamicTilingEnabled() const +{ + return m_dynamicTilingEnabled; +} + +void SetupState::setDynamicTilingEnabled(bool enabled) +{ + if (m_dynamicTilingEnabled == enabled) { + return; + } + m_dynamicTilingEnabled = enabled; + emitShellSettingsChanged(); +} + +bool SetupState::snapLayoutsEnabled() const +{ + return m_snapLayoutsEnabled; +} + +void SetupState::setSnapLayoutsEnabled(bool enabled) +{ + if (m_snapLayoutsEnabled == enabled) { + return; + } + m_snapLayoutsEnabled = enabled; + emitShellSettingsChanged(); +} + +bool SetupState::autoHidePanelsEnabled() const +{ + return m_autoHidePanelsEnabled; +} + +void SetupState::setAutoHidePanelsEnabled(bool enabled) +{ + if (m_autoHidePanelsEnabled == enabled) { + return; + } + m_autoHidePanelsEnabled = enabled; + emitShellSettingsChanged(); +} + +void SetupState::useRecommendedSettings() +{ + auto *context = DeviceContext::self(); + setDeviceClass(context->recommendedDeviceClass()); + + if (context->recommendedExperienceProfile() == QLatin1String("gaming")) { + setPrimaryInput(QStringLiteral("gamepad")); + } else if (context->hasTouch() && !context->hasKeyboard() && !context->hasMouse()) { + setPrimaryInput(QStringLiteral("touch")); + } else { + setPrimaryInput(QStringLiteral("keyboardMouse")); + } + + applyExperienceDefaults(context->recommendedExperienceProfile()); +} + +void SetupState::applyDeviceDefaults(const QString &deviceClass, const QString &primaryInput) +{ + setDeviceClass(deviceClass); + setPrimaryInput(primaryInput); + + if (deviceClass == QLatin1String("handheld") || deviceClass == QLatin1String("mini-pc")) { + applyExperienceDefaults(QStringLiteral("gaming")); + } else if (deviceClass == QLatin1String("laptop") || deviceClass == QLatin1String("desktop")) { + applyExperienceDefaults(QStringLiteral("desktop")); + } else if (deviceClass == QLatin1String("dual-screen") || deviceClass == QLatin1String("foldable")) { + applyExperienceDefaults(QStringLiteral("hybrid")); + } else { + applyExperienceDefaults(QStringLiteral("mobile")); + } +} + +void SetupState::applyExperienceDefaults(const QString &experienceProfile) +{ + setExperienceProfile(experienceProfile); + + if (experienceProfile == QLatin1String("gaming")) { + setConvergenceModeEnabled(false); + setGamingModeEnabled(true); + setNavigationPanelEnabled(false); + setDynamicTilingEnabled(false); + setSnapLayoutsEnabled(false); + setAutoHidePanelsEnabled(true); + } else if (experienceProfile == QLatin1String("desktop")) { + setConvergenceModeEnabled(true); + setGamingModeEnabled(false); + setNavigationPanelEnabled(false); + setDynamicTilingEnabled(true); + setSnapLayoutsEnabled(true); + setAutoHidePanelsEnabled(false); + } else if (experienceProfile == QLatin1String("hybrid")) { + setConvergenceModeEnabled(true); + setGamingModeEnabled(false); + setNavigationPanelEnabled(false); + setDynamicTilingEnabled(true); + setSnapLayoutsEnabled(true); + setAutoHidePanelsEnabled(true); + } else { + setConvergenceModeEnabled(false); + setGamingModeEnabled(false); + setNavigationPanelEnabled(false); + setDynamicTilingEnabled(false); + setSnapLayoutsEnabled(false); + setAutoHidePanelsEnabled(false); + } +} + +void SetupState::apply(bool applyToRunningSession) +{ + auto config = KSharedConfig::openConfig(CONFIG_FILE); + KConfigGroup general(config, GENERAL_CONFIG_GROUP); + general.writeEntry("setupDeviceClass", m_deviceClass, KConfigGroup::Notify); + general.writeEntry("setupExperienceProfile", m_experienceProfile, KConfigGroup::Notify); + general.writeEntry("setupPrimaryInput", m_primaryInput, KConfigGroup::Notify); + general.writeEntry("convergenceModeEnabled", m_convergenceModeEnabled, KConfigGroup::Notify); + general.writeEntry("gamingModeEnabled", m_gamingModeEnabled, KConfigGroup::Notify); + general.writeEntry("navigationPanelEnabled", m_navigationPanelEnabled, KConfigGroup::Notify); + general.writeEntry("dynamicTilingEnabled", m_dynamicTilingEnabled, KConfigGroup::Notify); + general.writeEntry("snapLayoutsEnabled", m_snapLayoutsEnabled, KConfigGroup::Notify); + general.writeEntry("autoHidePanelsEnabled", m_autoHidePanelsEnabled, KConfigGroup::Notify); + + KConfigGroup initialStart(config, INITIAL_START_CONFIG_GROUP); + initialStart.writeEntry("wizardRun", true, KConfigGroup::Notify); + config->sync(); + + if (applyToRunningSession) { + QProcess::startDetached(QStringLiteral("plasma-mobile-envmanager"), {QStringLiteral("--apply-settings")}); + } +} + +void SetupState::emitShellSettingsChanged() +{ + Q_EMIT shellSettingsChanged(); +} diff --git a/initialstart/setupstate.h b/initialstart/setupstate.h new file mode 100644 index 00000000..96468171 --- /dev/null +++ b/initialstart/setupstate.h @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include + +class QQmlEngine; +class QJSEngine; + +class SetupState : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString deviceClass READ deviceClass WRITE setDeviceClass NOTIFY deviceClassChanged) + Q_PROPERTY(QString experienceProfile READ experienceProfile WRITE setExperienceProfile NOTIFY experienceProfileChanged) + Q_PROPERTY(QString primaryInput READ primaryInput WRITE setPrimaryInput NOTIFY primaryInputChanged) + Q_PROPERTY(bool convergenceModeEnabled READ convergenceModeEnabled WRITE setConvergenceModeEnabled NOTIFY shellSettingsChanged) + Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY shellSettingsChanged) + Q_PROPERTY(bool navigationPanelEnabled READ navigationPanelEnabled WRITE setNavigationPanelEnabled NOTIFY shellSettingsChanged) + Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY shellSettingsChanged) + Q_PROPERTY(bool snapLayoutsEnabled READ snapLayoutsEnabled WRITE setSnapLayoutsEnabled NOTIFY shellSettingsChanged) + Q_PROPERTY(bool autoHidePanelsEnabled READ autoHidePanelsEnabled WRITE setAutoHidePanelsEnabled NOTIFY shellSettingsChanged) + +public: + explicit SetupState(QObject *parent = nullptr); + + static SetupState *self(); + static SetupState *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + + QString deviceClass() const; + void setDeviceClass(const QString &deviceClass); + + QString experienceProfile() const; + void setExperienceProfile(const QString &experienceProfile); + + QString primaryInput() const; + void setPrimaryInput(const QString &primaryInput); + + bool convergenceModeEnabled() const; + void setConvergenceModeEnabled(bool enabled); + + bool gamingModeEnabled() const; + void setGamingModeEnabled(bool enabled); + + bool navigationPanelEnabled() const; + void setNavigationPanelEnabled(bool enabled); + + bool dynamicTilingEnabled() const; + void setDynamicTilingEnabled(bool enabled); + + bool snapLayoutsEnabled() const; + void setSnapLayoutsEnabled(bool enabled); + + bool autoHidePanelsEnabled() const; + void setAutoHidePanelsEnabled(bool enabled); + + Q_INVOKABLE void useRecommendedSettings(); + Q_INVOKABLE void applyDeviceDefaults(const QString &deviceClass, const QString &primaryInput); + Q_INVOKABLE void applyExperienceDefaults(const QString &experienceProfile); + void apply(bool applyToRunningSession = true); + +Q_SIGNALS: + void deviceClassChanged(); + void experienceProfileChanged(); + void primaryInputChanged(); + void shellSettingsChanged(); + +private: + void emitShellSettingsChanged(); + + QString m_deviceClass; + QString m_experienceProfile; + QString m_primaryInput; + bool m_convergenceModeEnabled = false; + bool m_gamingModeEnabled = false; + bool m_navigationPanelEnabled = false; + bool m_dynamicTilingEnabled = true; + bool m_snapLayoutsEnabled = true; + bool m_autoHidePanelsEnabled = false; +}; diff --git a/initialstart/wizard.cpp b/initialstart/wizard.cpp index 9fb4a984..3cad3152 100644 --- a/initialstart/wizard.cpp +++ b/initialstart/wizard.cpp @@ -3,6 +3,7 @@ #include "wizard.h" #include "settings.h" +#include "setupstate.h" #include "utils.h" #include @@ -11,15 +12,24 @@ #include // TODO read distro provided config file -const QList WIZARD_MODULE_ORDER = {QStringLiteral("org.kde.plasma.mobileinitialstart.prepare"), +const QList WIZARD_MODULE_ORDER = {QStringLiteral("org.kde.plasma.mobileinitialstart.deviceprofile"), + QStringLiteral("org.kde.plasma.mobileinitialstart.experienceprofile"), + QStringLiteral("org.kde.plasma.mobileinitialstart.prepare"), QStringLiteral("org.kde.plasma.mobileinitialstart.time"), QStringLiteral("org.kde.plasma.mobileinitialstart.wifi"), QStringLiteral("org.kde.plasma.mobileinitialstart.cellular"), QStringLiteral("org.kde.plasma.mobileinitialstart.systemnavigation"), QStringLiteral("org.kde.plasma.mobileinitialstart.finished")}; +int moduleOrderIndex(const QString &pluginId) +{ + const int index = WIZARD_MODULE_ORDER.indexOf(pluginId); + return index == -1 ? WIZARD_MODULE_ORDER.size() : index; +} + Wizard::Wizard(QObject *parent, QQmlEngine *engine) : QObject{parent} + , m_testingMode{false} , m_engine{engine} { } @@ -45,7 +55,7 @@ void Wizard::load() // sort modules by order std::sort(m_modulePackages.begin(), m_modulePackages.end(), [](const auto &lhs, const auto &rhs) { - return WIZARD_MODULE_ORDER.indexOf(lhs.first->pluginId()) < WIZARD_MODULE_ORDER.indexOf(rhs.first->pluginId()); + return moduleOrderIndex(lhs.first->pluginId()) < moduleOrderIndex(rhs.first->pluginId()); }); QQmlComponent *c = new QQmlComponent(m_engine, this); @@ -105,7 +115,9 @@ int Wizard::stepsCount() void Wizard::wizardFinished() { - Settings::self()->setWizardFinished(); + if (!m_testingMode) { + SetupState::self()->apply(); + } QCoreApplication::quit(); }