Watch
1
0
Fork
You've already forked souveraine
0
souveraine/surfaces/quickshell/settings.qml
Fimeg 19fe6b1d0e shell: dock reorder, fullscreen detection fix, idle-power, sessiond, misc shell work
- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell)
- Fullscreen detection: scan all windows via HyprlandData.windowList
- IdleCoordinator, GlobalStates, Session.qml updates
- Deploy script, qmldir, settings, wallpaper, visualizer fixes
- sessiond server, memory module updates

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 22:18:20 -04:00

371 lines
14 KiB
QML

//@ pragma UseQApplication
//@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
// Souveraine settings — one app, two form factors, gated on
// Config.options.souveraine.phone (the Device tab's "Phone mode" switch):
//
// phone === true → full-screen portrait stack navigation
// (list → push page → back), finger-sized rows.
// phone === false → a normal desktop window: a side rail of pages beside
// a content pane, both selecting from the SAME pages[].
//
// Both modes load identical page components (modules/settings/*Config.qml) via
// one Loader, so a page added once appears in both. Only the chrome differs.
// This replaces the old split of settings.qml (upstream desktop) +
// settings-phone.qml (our mobile) — one file is the source of truth now.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Io
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions as CF
ApplicationWindow {
id: root
readonly property bool phoneMode: Config.options.souveraine.phone
// -1 = list view (phone idle state). In desktop mode a page is always
// selected, so it starts at 0.
property int currentPage: Config.options.souveraine.phone ? -1 : 0
readonly property real rowHeight: 68 // finger-sized tap target
readonly property real edgeMargin: 16
// The page set — shared by both chromes. {name, icon, component}. One page
// per domain we own; About reuses ii's stock page verbatim.
property var pages: [
{
name: Translation.tr("Device"),
icon: "smartphone",
component: "modules/settings/DeviceConfig.qml"
},
{
name: Translation.tr("Network & internet"),
icon: "wifi",
component: "modules/settings/NetworkConfig.qml"
},
{
name: Translation.tr("Display"),
icon: "display_settings",
component: "modules/settings/DisplayConfig.qml"
},
{
name: Translation.tr("Sound & microphone"),
icon: "volume_up",
component: "modules/settings/SoundConfig.qml"
},
{
name: Translation.tr("Lock screen"),
icon: "lock",
component: "modules/settings/LockConfig.qml"
},
{
name: Translation.tr("Wallpaper"),
icon: "wallpaper",
component: "modules/settings/WallpaperConfig.qml"
},
{
name: Translation.tr("Dock"),
icon: "dock_to_bottom",
component: "modules/settings/DockConfig.qml"
},
{
name: Translation.tr("Navigation"),
icon: "gesture",
component: "modules/settings/NavigationConfig.qml"
},
{
name: Translation.tr("Idle & sleep"),
icon: "bedtime",
component: "modules/settings/IdleConfig.qml"
},
{
name: Translation.tr("Keyboard"),
icon: "keyboard",
component: "modules/settings/KeyboardConfig.qml"
},
{
name: Translation.tr("Speech"),
icon: "mic",
component: "modules/settings/SpeechConfig.qml"
},
{
name: Translation.tr("About"),
icon: "info",
component: "modules/settings/About.qml"
}
// Next: app permissions/health, storage, accounts, updates.
]
visible: true
onClosing: Qt.quit()
title: "Settings"
color: Appearance.m3colors.m3background
// Phone windows deliberately start windowed and then take the exact same
// Hyprland fullscreen path as the navigation pill. Qt's Window.FullScreen
// sends a client fullscreen request instead; Hyprland treats that state
// differently from the pill's internal fullscreen dispatcher and the
// navigation rail can disappear. Keeping one compositor-owned path also
// means double-tapping the pill reliably returns Settings to windowed.
visibility: Window.Windowed
width: root.phoneMode ? Screen.width : 900
height: root.phoneMode ? Screen.height : 640
minimumWidth: root.phoneMode ? 0 : 640
minimumHeight: root.phoneMode ? 0 : 480
Component.onCompleted: {
MaterialThemeLoader.reapplyTheme()
Config.readWriteDelay = 0
if (root.phoneMode) phoneFullscreenRetry.start()
}
Process {
id: phoneFullscreen
command: [
"hyprctl", "dispatch",
`hl.dsp.window.fullscreen({ window = "pid:${Quickshell.processId}", mode = "fullscreen", action = "set" })`
]
}
// The QWindow maps just after Component.onCompleted. Retry briefly so a
// cold-started settings process cannot race its own Wayland toplevel.
Timer {
id: phoneFullscreenRetry
property int attempts: 0
interval: 180
repeat: true
onTriggered: {
attempts += 1
if (!phoneFullscreen.running) phoneFullscreen.running = true
if (attempts >= 3) stop()
}
}
// ====================================================================
// PHONE MODE — full-screen stack navigation
// ====================================================================
// --- List view (idle state) ------------------------------------------
ColumnLayout {
id: listView
anchors.fill: parent
spacing: 0
visible: root.phoneMode && root.currentPage < 0
Item {
Layout.fillWidth: true
Layout.leftMargin: root.edgeMargin
Layout.rightMargin: root.edgeMargin
Layout.topMargin: root.edgeMargin + 16
Layout.bottomMargin: 8
implicitHeight: titleText.implicitHeight
StyledText {
id: titleText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: Appearance.colors.colOnLayer0
text: Translation.tr("Settings")
font {
family: Appearance.font.family.title
pixelSize: Appearance.font.pixelSize.title
variableAxes: Appearance.font.variableAxes.title
}
}
}
StyledListView {
id: pageList
Layout.fillWidth: true
Layout.fillHeight: true
model: root.pages
delegate: RippleButton {
required property var index
required property var modelData
width: pageList.width
height: root.rowHeight
buttonRadius: 0
onClicked: root.currentPage = index
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: root.edgeMargin
anchors.rightMargin: root.edgeMargin
spacing: 16
MaterialSymbol {
text: modelData.icon
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer0
}
StyledText {
Layout.fillWidth: true
text: modelData.name
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.larger
}
MaterialSymbol {
text: "chevron_right"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colSubtext
}
}
}
}
}
// --- Page view (drilled-in state) ------------------------------------
Item {
id: pageView
anchors.fill: parent
visible: root.phoneMode && root.currentPage >= 0
Item {
id: pageHeader
anchors { top: parent.top; left: parent.left; right: parent.right }
height: 56
RippleButton {
id: backButton
anchors {
left: parent.left
leftMargin: 4
verticalCenter: parent.verticalCenter
}
buttonRadius: Appearance.rounding.full
implicitWidth: 48
implicitHeight: 48
onClicked: root.currentPage = -1
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "arrow_back"
iconSize: 24
}
}
StyledText {
anchors {
left: backButton.right
leftMargin: 8
right: parent.right
rightMargin: root.edgeMargin
verticalCenter: parent.verticalCenter
}
color: Appearance.colors.colOnLayer0
text: (root.currentPage >= 0 && root.currentPage < root.pages.length)
? root.pages[root.currentPage].name : ""
elide: Text.ElideRight
font {
family: Appearance.font.family.title
pixelSize: Appearance.font.pixelSize.larger
}
}
}
Loader {
id: phonePageLoader
anchors {
top: pageHeader.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
}
active: root.phoneMode && root.currentPage >= 0 && Config.ready
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
? root.pages[root.currentPage].component : ""
}
}
// ====================================================================
// DESKTOP MODE — side rail + content pane
// ====================================================================
RowLayout {
id: desktopView
anchors.fill: parent
spacing: 0
visible: !root.phoneMode
// Left rail
Rectangle {
Layout.fillHeight: true
Layout.preferredWidth: 220
color: Appearance.colors.colLayer1
ColumnLayout {
anchors.fill: parent
spacing: 0
Item {
Layout.fillWidth: true
Layout.topMargin: root.edgeMargin + 8
Layout.leftMargin: root.edgeMargin
Layout.bottomMargin: 8
implicitHeight: railTitle.implicitHeight
StyledText {
id: railTitle
anchors.left: parent.left
text: Translation.tr("Settings")
color: Appearance.colors.colOnLayer1
font {
family: Appearance.font.family.title
pixelSize: Appearance.font.pixelSize.larger
variableAxes: Appearance.font.variableAxes.title
}
}
}
StyledListView {
id: railList
Layout.fillWidth: true
Layout.fillHeight: true
model: root.pages
delegate: RippleButton {
required property var index
required property var modelData
width: railList.width
height: 44
buttonRadius: 0
toggled: root.currentPage === index
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
onClicked: root.currentPage = index
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: root.edgeMargin
anchors.rightMargin: 8
spacing: 12
MaterialSymbol {
text: modelData.icon
iconSize: Appearance.font.pixelSize.large
color: root.currentPage === index
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
}
StyledText {
Layout.fillWidth: true
text: modelData.name
elide: Text.ElideRight
color: root.currentPage === index
? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.normal
}
}
}
}
}
}
// Content pane
Loader {
id: desktopPageLoader
Layout.fillWidth: true
Layout.fillHeight: true
active: !root.phoneMode && root.currentPage >= 0 && Config.ready
source: (root.currentPage >= 0 && root.currentPage < root.pages.length)
? root.pages[root.currentPage].component : ""
}
}
}