The sidebar was a flat scan of thirty-one rows; now it reads like a settings app. Multi-subject categories (Input, Network & Sharing, Applications, Users & Accounts, Privacy & Security, System) carry an Appearance-style tab strip above the page, drawn by the shell so the leaf pages themselves are untouched. The taxonomy lives in one new file, services/SettingsRoutes.qml; the sidebar, the strip, route validation, search breadcrumbs, and both generators derive from it. ShellState.settingsPage still holds leaf ids, so every deep link, IPC call, and search result keeps working — and now lands on the exact tab. Dictation moves out of Sound onto its own page under Input, with a handoff back to Sound for the microphone. The strip scrolls when System's nine tabs outgrow a tiled window. All 161 contracts pass. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
284 lines
9.6 KiB
QML
284 lines
9.6 KiB
QML
// The first thing a new machine shows.
|
|
//
|
|
// Panama has fifteen settings categories and thirty-odd pages inside them,
|
|
// which is the opposite of the usual problem: somebody arriving from GNOME,
|
|
// macOS or Windows cannot tell which four things matter. This is those four
|
|
// things, once, on the first start.
|
|
//
|
|
// Deliberately not a tour. Nobody reads a tour, and a multi-step wizard on a
|
|
// desktop somebody just installed is another thing standing between them and
|
|
// using it. One screen, the handful of keys that unlock everything else, and a
|
|
// way out.
|
|
//
|
|
// The chords are read from the live keymap rather than written here, so a
|
|
// machine whose owner has already rebound something shows what they actually
|
|
// have -- and so this cannot drift the way a hand-written list would.
|
|
//
|
|
// Shown once, tracked by the `welcomeSeen` preference. It stays reachable
|
|
// afterwards from the launcher and from Settings > System > About, because
|
|
// the moment somebody wants it again is exactly the moment a one-shot has
|
|
// thrown it away.
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
import qs.modules.settings
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
readonly property bool open: ShellState.welcomeOpen
|
|
|
|
// What to teach, in the order it becomes useful. Each names a bind by the
|
|
// description hypr/keybinds.lua gives it, so the chord shown is whatever
|
|
// that bind currently answers to.
|
|
readonly property var lessons: [
|
|
{
|
|
find: "Launcher",
|
|
fallback: "Super + Space",
|
|
title: "Find anything",
|
|
detail: "Applications, files, clipboard history, emoji, a calculator. Start typing."
|
|
},
|
|
{
|
|
find: "Terminal",
|
|
fallback: "Super + T",
|
|
title: "Open a terminal",
|
|
detail: "Windows tile themselves as they open. Nothing needs arranging."
|
|
},
|
|
{
|
|
find: "Overview",
|
|
fallback: "Super + `",
|
|
title: "See every window",
|
|
detail: "All your workspaces at once, and search across them."
|
|
},
|
|
{
|
|
find: "Settings",
|
|
fallback: "Super + I",
|
|
title: "Change anything",
|
|
detail: "Displays, sound, network, appearance, and everything else."
|
|
},
|
|
{
|
|
find: "Keyboard shortcuts",
|
|
fallback: "Super + /",
|
|
title: "The rest of the keys",
|
|
detail: "Every shortcut this desktop has, whenever you want it."
|
|
}
|
|
]
|
|
|
|
function chordFor(description: string, fallback: string): string {
|
|
for (const bind of Keybinds.binds) {
|
|
if (bind.description === description)
|
|
return bind.chord;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
exclusiveZone: 0
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
WlrLayershell.namespace: "qs-popover-welcome"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: root.open
|
|
? WlrKeyboardFocus.Exclusive
|
|
: WlrKeyboardFocus.None
|
|
|
|
property bool mapped: false
|
|
visible: root.mapped
|
|
|
|
onOpenChanged: {
|
|
if (root.open) {
|
|
unmapTimer.stop();
|
|
root.mapped = true;
|
|
Keybinds.refresh();
|
|
} else {
|
|
unmapTimer.restart();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: unmapTimer
|
|
interval: Theme.durNormal
|
|
onTriggered: root.mapped = false
|
|
}
|
|
|
|
// Dismissing is what marks it seen. Closing by any route counts: somebody
|
|
// who pressed Escape has seen it, and showing it again next login would be
|
|
// the desktop failing to take no for an answer.
|
|
function dismiss(): void {
|
|
DesktopPreferences.set("welcomeSeen", true);
|
|
ShellState.close();
|
|
}
|
|
|
|
// First start on a fresh install. Waits for the keymap so the chords are
|
|
// real rather than fallbacks, and gives the rest of the shell a moment to
|
|
// finish coming up -- appearing over a half-drawn desktop reads as a crash.
|
|
Timer {
|
|
id: firstStart
|
|
interval: 2500
|
|
running: !DesktopPreferences.get("welcomeSeen")
|
|
onTriggered: {
|
|
if (!DesktopPreferences.get("welcomeSeen") && !ShellState.anyOverlayOpen)
|
|
ShellState.open("welcome");
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
|
|
opacity: root.open ? 1 : 0
|
|
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
|
|
// No click-outside-to-close. Everything else on this desktop dismisses
|
|
// that way, but this is the one surface where a stray click during the
|
|
// first thirty seconds would throw away the only explanation on offer.
|
|
MouseArea { anchors.fill: parent }
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
|
|
anchors.centerIn: parent
|
|
width: Math.min(root.width - 120, 640)
|
|
height: Math.min(root.height - 120, content.implicitHeight + 64)
|
|
radius: Theme.popoverRadius
|
|
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.fg, 0.1)
|
|
|
|
opacity: root.open ? 1 : 0
|
|
scale: root.open ? 1 : 0.98
|
|
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
Behavior on scale { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
|
|
PrismEdge {
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
inset: Theme.popoverRadius
|
|
}
|
|
|
|
Column {
|
|
id: content
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.margins: 32
|
|
spacing: 22
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 6
|
|
|
|
Text {
|
|
text: "Welcome to Panama"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeTitle
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "Five keys, and you have the whole desktop."
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: 14
|
|
|
|
Repeater {
|
|
model: root.lessons
|
|
|
|
Item {
|
|
required property var modelData
|
|
|
|
width: parent.width
|
|
height: Math.max(chordPill.height, lessonText.implicitHeight)
|
|
|
|
Rectangle {
|
|
id: chordPill
|
|
width: 150
|
|
height: chordLabel.implicitHeight + 12
|
|
radius: Theme.pillRadius
|
|
color: Theme.alpha(Theme.fg, 0.07)
|
|
|
|
Text {
|
|
id: chordLabel
|
|
anchors.centerIn: parent
|
|
text: root.chordFor(modelData.find, modelData.fallback)
|
|
color: Theme.accent
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.features: Theme.tabularFigures
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: lessonText
|
|
anchors.left: chordPill.right
|
|
anchors.leftMargin: 16
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: chordPill.verticalCenter
|
|
spacing: 2
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: modelData.title
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: modelData.detail
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
spacing: 8
|
|
|
|
SettingsButton {
|
|
text: "Show every shortcut"
|
|
onClicked: {
|
|
DesktopPreferences.set("welcomeSeen", true);
|
|
ShellState.open("cheatsheet");
|
|
}
|
|
}
|
|
|
|
SettingsButton {
|
|
text: "Start using it"
|
|
tone: "accent"
|
|
onClicked: root.dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
focus: true
|
|
Keys.onEscapePressed: root.dismiss()
|
|
}
|
|
}
|