Add profile-aware initial setup

Detect the device class, stage the selected experience, and write the resulting setup choices through SetupState.

Load the new device and experience modules before the existing setup pages, and use the Shift icon on the finished page.
This commit is contained in:
Marco Allegretti 2026-05-11 10:03:07 +02:00
parent 1b8efc3be3
commit 42d41351e2
19 changed files with 1000 additions and 151 deletions

View file

@ -7,6 +7,10 @@ add_subdirectory(modules)
add_executable(plasma-mobile-initial-start add_executable(plasma-mobile-initial-start
main.cpp main.cpp
devicecontext.cpp
devicecontext.h
setupstate.cpp
setupstate.h
wizard.cpp wizard.cpp
wizard.h wizard.h
settings.cpp settings.cpp

View file

@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "devicecontext.h"
#include <KRuntimePlatform>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QInputDevice>
#include <QJSEngine>
#include <QQmlEngine>
#include <QScreen>
#include <algorithm>
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<const QInputDevice *> 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<QScreen *> screens = QGuiApplication::screens();
m_displayCount = std::max(1, static_cast<int>(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;
}

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
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;
};

View file

@ -3,6 +3,7 @@
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QDebug>
#include <QIcon> #include <QIcon>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
@ -12,6 +13,7 @@
#include <KLocalizedString> #include <KLocalizedString>
#include "settings.h" #include "settings.h"
#include "setupstate.h"
#include "version.h" #include "version.h"
#include "wizard.h" #include "wizard.h"
@ -19,6 +21,11 @@ std::unique_ptr<QCommandLineParser> createParser()
{ {
auto parser = std::make_unique<QCommandLineParser>(); auto parser = std::make_unique<QCommandLineParser>();
parser->addOption(QCommandLineOption(QStringLiteral("test-wizard"), i18n("Opens the initial start wizard without modifying configuration"))); 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; return parser;
} }
@ -44,14 +51,32 @@ int main(int argc, char *argv[])
aboutData.processCommandLine(parser.get()); aboutData.processCommandLine(parser.get());
const bool testWizard = parser->isSet(QStringLiteral("test-wizard")); 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 (!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()) { 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; return 0;
} }
} }
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("start-here-shift")));
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext{&engine}); engine.rootContext()->setContextObject(new KLocalizedContext{&engine});
@ -63,9 +88,7 @@ int main(int argc, char *argv[])
return wizard; return wizard;
}); });
engine.load(QUrl(QStringLiteral("qrc:org/kde/plasma/mobileinitialstart/initialstart/qml/Main.qml"))); engine.load(QUrl(QStringLiteral("qrc:/org/kde/plasma/mobileinitialstart/initialstart/qml/Main.qml")));
app.setWindowIcon(QIcon::fromTheme(QStringLiteral("start-here-symbolic")));
return app.exec(); return app.exec();
} }

View file

@ -1,6 +1,8 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org> # SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later # 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(finished org.kde.plasma.mobileinitialstart.finished mobileinitialstart)
plasma_install_package(systemnavigation org.kde.plasma.mobileinitialstart.systemnavigation mobileinitialstart) plasma_install_package(systemnavigation org.kde.plasma.mobileinitialstart.systemnavigation mobileinitialstart)
add_subdirectory(cellular) add_subdirectory(cellular)

View file

@ -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
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -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
}
}
}
}
}

View file

@ -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"
}
}

View file

@ -14,27 +14,29 @@ InitialStartModule {
contentItem: Item { contentItem: Item {
id: root id: root
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Kirigami.Units.gridUnit 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 { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.alignment: Qt.AlignHCenter
text: i18n("Your device is now ready. <br /><br />Enjoy <b>%1</b>!", InitialStartUtil.distroName) text: i18n("Your device is ready. <br /><br />SHIFT will start with the <b>%1</b> experience.", SetupState.experienceProfile)
wrapMode: Text.Wrap wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
Item { Layout.fillHeight: true } Item { Layout.fillHeight: true }
Image {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
fillMode: Image.PreserveAspectFit
source: "konqi-calling.png"
}
} }
} }
} }

View file

@ -9,7 +9,6 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.plasma.mobileinitialstart.initialstart import org.kde.plasma.mobileinitialstart.initialstart
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
InitialStartModule { InitialStartModule {
name: i18n("System Navigation") name: i18n("System Navigation")
@ -49,14 +48,14 @@ InitialStartModule {
text: i18n("Gesture navigation") text: i18n("Gesture navigation")
description: i18n("Swipe up from the bottom to see running applications. Flick to go to the home screen.") description: i18n("Swipe up from the bottom to see running applications. Flick to go to the home screen.")
onClicked: { onClicked: {
if (checked && ShellSettings.Settings.navigationPanelEnabled) { if (checked && SetupState.navigationPanelEnabled) {
ShellSettings.Settings.navigationPanelEnabled = false; SetupState.navigationPanelEnabled = false;
} }
checked = Qt.binding(function () { return !ShellSettings.Settings.navigationPanelEnabled; }); checked = Qt.binding(function () { return !SetupState.navigationPanelEnabled; });
} }
Binding on checked { Binding on checked {
value: !ShellSettings.Settings.navigationPanelEnabled value: !SetupState.navigationPanelEnabled
} }
} }
} }
@ -70,14 +69,14 @@ InitialStartModule {
text: i18n("Button navigation") text: i18n("Button navigation")
description: i18n("Use buttons on a navigation bar to navigate the system.") description: i18n("Use buttons on a navigation bar to navigate the system.")
onClicked: { onClicked: {
if (checked && !ShellSettings.Settings.navigationPanelEnabled) { if (checked && !SetupState.navigationPanelEnabled) {
ShellSettings.Settings.navigationPanelEnabled = true; SetupState.navigationPanelEnabled = true;
} }
checked = Qt.binding(function () { return ShellSettings.Settings.navigationPanelEnabled; }); checked = Qt.binding(function () { return SetupState.navigationPanelEnabled; });
} }
Binding on checked { Binding on checked {
value: ShellSettings.Settings.navigationPanelEnabled value: SetupState.navigationPanelEnabled
} }
} }
} }

View file

@ -102,21 +102,20 @@ Item {
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
text: i18n("Welcome to<br/><b>Plasma Mobile</b>") text: i18n("Welcome to<br/><b>SHIFT</b>")
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
font.pointSize: 18 font.pointSize: 18
color: "white" color: "white"
} }
} }
ColumnLayout { ColumnLayout {
opacity: root.contentOpacity opacity: root.contentOpacity
spacing: Kirigami.Units.largeSpacing spacing: Kirigami.Units.largeSpacing
anchors { anchors {
left: parent.left left: parent.left
right: parent.right right: parent.right
bottom: parent.bottom bottom: parent.bottom
@ -125,10 +124,9 @@ Item {
bottomMargin: Kirigami.Units.gridUnit * 2 bottomMargin: Kirigami.Units.gridUnit * 2
} }
Kirigami.Heading { Kirigami.Heading {
Layout.fillWidth: true Layout.fillWidth: true
text: i18n("Powered by<br/><b>%1</b>", InitialStartUtil.distroName) text: i18n("Built on<br/><b>%1</b>", InitialStartUtil.distroName)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap

View file

@ -4,6 +4,7 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Window 2.15
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@ -12,11 +13,13 @@ import initialstart 1.0 as InitialStart
Kirigami.ApplicationWindow { Kirigami.ApplicationWindow {
id: root id: root
width: 360 minimumWidth: Kirigami.Units.gridUnit * 20
height: 720 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" visibility: "Windowed"
title: i18n("Initial Start") title: i18n("SHIFT Initial Setup")
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn

View file

@ -29,42 +29,25 @@ Kirigami.Page {
readonly property bool onFinalPage: currentIndex === (stepCount - 1) 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 // step animation
// manually doing the animation is more performant and less glitchy with window resize than a SwipeView // 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 currentStepItemX: 0
property real nextStepItemX: 0 property real nextStepItemX: root.width
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;
}
}
}
onStepCountChanged: { onStepCountChanged: {
// reset position // reset position
updateStepItems();
requestPreviousPage(); requestPreviousPage();
} }
@ -74,54 +57,43 @@ Kirigami.Page {
} }
function requestNextPage() { function requestNextPage() {
if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) { if (currentIndex >= stepCount - 1) {
return; return;
} }
previousStepItemX = 0;
currentIndex++; currentIndex++;
updateStepItems();
stepHeading.changeText(currentStepItem.name); stepHeading.changeText(currentStepItem.name);
currentStepItemX = root.width; previousStepItemX = -root.width;
currentStepItem.visible = true; currentStepItemX = 0;
nextStepItemX = root.width;
previousStepAnim.to = -root.width;
previousStepAnim.restart();
currentStepAnim.to = 0;
currentStepAnim.restart();
} }
function requestPreviousPage() { function requestPreviousPage() {
if (previousStepAnim.running || currentStepAnim.running || nextStepAnim.running) {
return;
}
if (currentIndex === 0) { if (currentIndex === 0) {
root.showingLanding = true; root.showingLanding = true;
landingComponent.returnToLanding(); landingComponent.returnToLanding();
} else { } else {
nextStepItemX = 0;
currentIndex--; currentIndex--;
updateStepItems();
stepHeading.changeText(currentStepItem.name); stepHeading.changeText(currentStepItem.name);
currentStepItemX = -root.width; previousStepItemX = -root.width;
currentStepItem.visible = true; currentStepItemX = 0;
nextStepItemX = root.width;
nextStepAnim.to = root.width;
nextStepAnim.restart();
currentStepAnim.to = 0;
currentStepAnim.restart();
} }
} }
LandingComponent { LandingComponent {
id: landingComponent id: landingComponent
anchors.fill: parent anchors.fill: parent
enabled: root.showingLanding
visible: root.showingLanding
onRequestNextPage: { onRequestNextPage: {
root.showingLanding = false; root.showingLanding = false;
root.updateStepItems();
stepHeading.changeText(root.currentStepItem.name); stepHeading.changeText(root.currentStepItem.name);
} }
} }
@ -136,20 +108,6 @@ Kirigami.Page {
x: 0 x: 0
y: root.showingLanding ? overlaySteps.height : 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 // heading for all the wizard steps
Label { Label {
id: stepHeading id: stepHeading
@ -168,24 +126,8 @@ Kirigami.Page {
property string toText property string toText
function changeText(text) { function changeText(text) {
toText = text; stepHeading.text = text;
toHidden.restart(); stepHeading.opacity = 1;
}
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
} }
} }
@ -213,11 +155,12 @@ Kirigami.Page {
// setup steps // setup steps
Repeater { Repeater {
id: stepRepeater
model: InitialStart.Wizard.steps model: InitialStart.Wizard.steps
delegate: MobileShell.BaseItem { delegate: MobileShell.BaseItem {
id: item id: item
visible: model.index === 0 // the binding is broken later visible: !root.showingLanding && Math.abs(item.currentIndex - root.currentIndex) <= 1
contentItem: modelData.contentItem contentItem: modelData.contentItem
transform: Translate { transform: Translate {
x: { x: {
@ -238,27 +181,8 @@ Kirigami.Page {
property string name: modelData.name property string name: modelData.name
property int currentIndex: model.index 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: { Component.onCompleted: {
updateRootItems(); root.updateStepItems();
}
// keep root properties updated
Connections {
target: root
function onCurrentIndexChanged() {
item.updateRootItems();
}
} }
} }
} }

View file

@ -4,7 +4,6 @@
#include "settings.h" #include "settings.h"
#include <KConfigGroup> #include <KConfigGroup>
#include <KRuntimePlatform>
const QString CONFIG_FILE = QStringLiteral("plasmamobilerc"); const QString CONFIG_FILE = QStringLiteral("plasmamobilerc");
const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart"); const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart");
@ -12,18 +11,14 @@ const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart");
Settings::Settings(QObject *parent) Settings::Settings(QObject *parent)
: QObject{parent} : QObject{parent}
, m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE)} , m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE)}
, m_isMobilePlatform{KRuntimePlatform::runtimePlatform().contains(QStringLiteral("phone"))}
{ {
} }
bool Settings::shouldStartWizard() bool Settings::shouldStartWizard()
{ {
if (!m_isMobilePlatform) { const KConfigGroup initialStartGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP};
return false; const KConfigGroup generalGroup{m_mobileConfig, QStringLiteral("General")};
} return !initialStartGroup.readEntry("wizardRun", false) || generalGroup.readEntry("setupExperienceProfile", QString()).isEmpty();
auto group = KConfigGroup{m_mobileConfig, INITIAL_START_CONFIG_GROUP};
return !group.readEntry("wizardRun", false);
} }
void Settings::setWizardFinished() void Settings::setWizardFinished()

View file

@ -18,5 +18,4 @@ public:
private: private:
KSharedConfig::Ptr m_mobileConfig; KSharedConfig::Ptr m_mobileConfig;
bool m_isMobilePlatform;
}; };

257
initialstart/setupstate.cpp Normal file
View file

@ -0,0 +1,257 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "setupstate.h"
#include "devicecontext.h"
#include <KConfigGroup>
#include <KSharedConfig>
#include <QCoreApplication>
#include <QJSEngine>
#include <QProcess>
#include <QQmlEngine>
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();
}

85
initialstart/setupstate.h Normal file
View file

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
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;
};

View file

@ -3,6 +3,7 @@
#include "wizard.h" #include "wizard.h"
#include "settings.h" #include "settings.h"
#include "setupstate.h"
#include "utils.h" #include "utils.h"
#include <KPackage/PackageLoader> #include <KPackage/PackageLoader>
@ -11,15 +12,24 @@
#include <QQmlComponent> #include <QQmlComponent>
// TODO read distro provided config file // TODO read distro provided config file
const QList<QString> WIZARD_MODULE_ORDER = {QStringLiteral("org.kde.plasma.mobileinitialstart.prepare"), const QList<QString> 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.time"),
QStringLiteral("org.kde.plasma.mobileinitialstart.wifi"), QStringLiteral("org.kde.plasma.mobileinitialstart.wifi"),
QStringLiteral("org.kde.plasma.mobileinitialstart.cellular"), QStringLiteral("org.kde.plasma.mobileinitialstart.cellular"),
QStringLiteral("org.kde.plasma.mobileinitialstart.systemnavigation"), QStringLiteral("org.kde.plasma.mobileinitialstart.systemnavigation"),
QStringLiteral("org.kde.plasma.mobileinitialstart.finished")}; 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) Wizard::Wizard(QObject *parent, QQmlEngine *engine)
: QObject{parent} : QObject{parent}
, m_testingMode{false}
, m_engine{engine} , m_engine{engine}
{ {
} }
@ -45,7 +55,7 @@ void Wizard::load()
// sort modules by order // sort modules by order
std::sort(m_modulePackages.begin(), m_modulePackages.end(), [](const auto &lhs, const auto &rhs) { 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); QQmlComponent *c = new QQmlComponent(m_engine, this);
@ -105,7 +115,9 @@ int Wizard::stepsCount()
void Wizard::wizardFinished() void Wizard::wizardFinished()
{ {
Settings::self()->setWizardFinished(); if (!m_testingMode) {
SetupState::self()->apply();
}
QCoreApplication::quit(); QCoreApplication::quit();
} }