Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c37ca3baee | ||
|
|
bd5d030d91 |
@@ -871,6 +871,26 @@ Singleton {
|
||||
{ value: "light", label: "Light" }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "accentName", type: "enum", def: "blue", group: "appearance",
|
||||
label: "Accent colour",
|
||||
detail: "Drives the focused window border, the bar hairline, and every active state",
|
||||
// NAMED accents, not a free colour. Each name carries a curated
|
||||
// pair per scheme, because one hex cannot serve both: a colour
|
||||
// legible on the dark ground is usually illegible on the light one.
|
||||
// The palette and its measured contrast live in config/Theme.qml,
|
||||
// which is also what stops this list drifting from what is drawn.
|
||||
options: [
|
||||
{ value: "blue", label: "Prism blue" },
|
||||
{ value: "orchid", label: "Orchid" },
|
||||
{ value: "teal", label: "Teal" },
|
||||
{ value: "green", label: "Green" },
|
||||
{ value: "amber", label: "Amber" },
|
||||
{ value: "orange", label: "Orange" },
|
||||
{ value: "rose", label: "Rose" },
|
||||
{ value: "slate", label: "Slate" }
|
||||
]
|
||||
},
|
||||
|
||||
// ── Application themes ─────────────────────────────────────────────
|
||||
// ColorScheme owns GTK's light/dark theme. These are the two theme
|
||||
|
||||
@@ -42,13 +42,41 @@ Singleton {
|
||||
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||
|
||||
// The pair. `accent` is the primary and carries every state meaning
|
||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
||||
// theme — it never appears alone, only as the far end of a gradient. That
|
||||
// restraint is the whole point: the two colours meeting is the signature,
|
||||
// so the pink stops being special the moment it's used as a flat fill.
|
||||
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
||||
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
||||
// ── The accent ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// `accent` is the primary and carries every state meaning (focused, active,
|
||||
// on). `accentSecondary` never appears alone, only as the far end of a
|
||||
// gradient. That restraint is the whole point: the two colours meeting is
|
||||
// the signature, so the second colour stops being special the moment it is
|
||||
// used as a flat fill.
|
||||
//
|
||||
// NAMED accents rather than a free colour. Each name carries a curated
|
||||
// triple per scheme, because an arbitrary hex cannot work in both: a colour
|
||||
// legible on the Moon background is usually illegible on the Day one, and a
|
||||
// picker that lets someone choose an unreadable desktop is not a feature.
|
||||
// Every pair below measures at least 3:1 against the ground it sits on.
|
||||
// This is also GNOME's model, which is the parity being chased.
|
||||
//
|
||||
// Blue is the shipped Prism -- blue leading, orchid following -- and stays
|
||||
// the default.
|
||||
readonly property var accents: ({
|
||||
"blue": { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue" },
|
||||
"orchid": { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid" },
|
||||
"teal": { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal" },
|
||||
"green": { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green" },
|
||||
"amber": { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber" },
|
||||
"orange": { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange" },
|
||||
"rose": { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose" },
|
||||
"slate": { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate" }
|
||||
})
|
||||
|
||||
// Falls back to blue for an unknown name, so a settings file written by a
|
||||
// newer Panama -- or edited by hand -- degrades to the shipped identity
|
||||
// rather than to an undefined colour.
|
||||
readonly property var accentPair: root.accents[DesktopPreferences.get("accentName")] ?? root.accents["blue"]
|
||||
|
||||
readonly property color accent: root.dark ? root.accentPair.dark : root.accentPair.light
|
||||
readonly property color accentSecondary: root.dark ? root.accentPair.darkSecondary : root.accentPair.lightSecondary
|
||||
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// The three system power profiles, as an expandable list.
|
||||
//
|
||||
// A row per profile rather than a cycling button: there are three, and cycling
|
||||
// through them means passing through one you did not want on a machine where
|
||||
// the change is immediate and audible. The same shape the audio device lists
|
||||
// use, so the panel reads consistently.
|
||||
//
|
||||
// The daemon owns the profile -- it survives a shell restart and anything else
|
||||
// on the system can change it -- so this reads back rather than assuming, the
|
||||
// same as monitor brightness.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: PowerProfiles.profiles
|
||||
|
||||
RowButton {
|
||||
required property var modelData
|
||||
|
||||
width: root.width
|
||||
icon: {
|
||||
switch (modelData) {
|
||||
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||
case "performance": return "power-profile-performance-symbolic";
|
||||
default: return "power-profile-balanced-symbolic";
|
||||
}
|
||||
}
|
||||
iconFallback: "preferences-system-power-symbolic"
|
||||
label: PowerProfiles.label(modelData)
|
||||
sublabel: PowerProfiles.detail(modelData)
|
||||
selected: modelData === PowerProfiles.active
|
||||
dimmed: PowerProfiles.busy
|
||||
onClicked: PowerProfiles.set(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Only when it is true. A machine that cannot deliver the profile it is set
|
||||
// to is the one case where the label alone is misleading.
|
||||
Text {
|
||||
visible: PowerProfiles.degraded !== ""
|
||||
width: root.width
|
||||
text: "Limited right now: " + PowerProfiles.degraded
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
leftPadding: 10
|
||||
topPadding: 4
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,9 @@ PanelWindow {
|
||||
if (root.visible) {
|
||||
KdeConnect.refresh();
|
||||
HomeAssistant.refresh();
|
||||
// The daemon owns the power profile and anything on the system can
|
||||
// change it, so the panel asks rather than trusting what it last saw.
|
||||
PowerProfiles.refresh();
|
||||
openAnim.restart();
|
||||
} else {
|
||||
panel.reset();
|
||||
|
||||
@@ -130,6 +130,21 @@ Item {
|
||||
onToggled: Caffeine.toggle()
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
icon: ColorScheme.dark ? "weather-clear-night-symbolic" : "weather-clear-symbolic"
|
||||
label: "Appearance"
|
||||
sublabel: ColorScheme.dark ? "Dark" : "Light"
|
||||
// "active" reads as the non-default state across this grid, and
|
||||
// dark is what Panama ships, so light is the lit one.
|
||||
active: !ColorScheme.dark
|
||||
// Writes the preference and stops. ColorScheme propagates it to
|
||||
// GTK, the portal, the terminals, the launcher, btop, tmux and
|
||||
// the lock screen; nothing here needs to know that list.
|
||||
onToggled: SystemSettings.commitPreference("colorScheme",
|
||||
ColorScheme.dark ? "light" : "dark")
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
icon: "night-light-symbolic"
|
||||
@@ -190,6 +205,38 @@ Item {
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
}
|
||||
|
||||
// ── Power ───────────────────────────────────────────────────────────
|
||||
// Only where a power-profiles daemon is actually running. A desktop
|
||||
// without one should not show a control that cannot do anything.
|
||||
RowButton {
|
||||
visible: PowerProfiles.available
|
||||
width: content.width
|
||||
icon: {
|
||||
switch (PowerProfiles.active) {
|
||||
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||
case "performance": return "power-profile-performance-symbolic";
|
||||
default: return "power-profile-balanced-symbolic";
|
||||
}
|
||||
}
|
||||
iconFallback: "preferences-system-power-symbolic"
|
||||
label: "Power profile"
|
||||
sublabel: PowerProfiles.degraded !== ""
|
||||
? PowerProfiles.label(PowerProfiles.active) + " · limited"
|
||||
: PowerProfiles.label(PowerProfiles.active)
|
||||
selected: root.expandedSection === "power"
|
||||
onClicked: root.expand("power")
|
||||
}
|
||||
|
||||
Section {
|
||||
width: content.width
|
||||
expanded: root.expandedSection === "power"
|
||||
|
||||
PowerProfileList {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sliders ─────────────────────────────────────────────────────────
|
||||
AudioSlider {
|
||||
width: content.width
|
||||
|
||||
@@ -17,3 +17,4 @@ RowButton 1.0 RowButton.qml
|
||||
ScrollColumn 1.0 ScrollColumn.qml
|
||||
Section 1.0 Section.qml
|
||||
WifiList 1.0 WifiList.qml
|
||||
PowerProfileList 1.0 PowerProfileList.qml
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Choosing the desktop accent.
|
||||
//
|
||||
// Each swatch is drawn as the GRADIENT it will actually produce, not a flat
|
||||
// dot, because the gradient is the thing being chosen -- the focused window
|
||||
// border, the bar hairline and every active state are the two colours meeting.
|
||||
// A row of flat circles would misrepresent all of them.
|
||||
//
|
||||
// Named accents rather than a colour wheel: each name carries a curated pair
|
||||
// per scheme, so every choice stays legible in both light and dark. See
|
||||
// config/Theme.qml for the palette and the reasoning.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
spacing: 10
|
||||
|
||||
readonly property string current: DesktopPreferences.get("accentName") || "blue"
|
||||
|
||||
Repeater {
|
||||
// Object key order is insertion order here, so the palette's own order
|
||||
// is what the user sees; blue first because it is what Panama ships.
|
||||
model: Object.keys(Theme.accents)
|
||||
|
||||
Column {
|
||||
id: entry
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property var pair: Theme.accents[entry.modelData]
|
||||
readonly property bool selected: entry.modelData === root.current
|
||||
readonly property color start: Theme.dark ? entry.pair.dark : entry.pair.light
|
||||
readonly property color end: Theme.dark ? entry.pair.darkSecondary : entry.pair.lightSecondary
|
||||
|
||||
spacing: 5
|
||||
|
||||
Rectangle {
|
||||
width: 46
|
||||
height: 46
|
||||
radius: 23
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
// The ring sits outside the gradient rather than over it, so a
|
||||
// selected swatch still shows its true colours.
|
||||
border.width: entry.selected ? 2 : 1
|
||||
border.color: entry.selected ? Theme.fg : Theme.alpha(Theme.fg, 0.14)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: entry.selected ? 4 : 3
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: entry.start }
|
||||
GradientStop { position: 1.0; color: entry.end }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: SystemSettings.commitPreference("accentName", entry.modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Always shown, not a tooltip. Telling swatches apart by colour is
|
||||
// exactly what someone with a colour vision deficiency cannot do,
|
||||
// and it is the reason the palette is named rather than freeform --
|
||||
// hiding the names behind a hover would waste that.
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: entry.pair.label
|
||||
color: entry.selected ? Theme.fg : Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: entry.selected ? Font.DemiBold : Font.Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,13 @@ SettingsPage {
|
||||
? ColorScheme.lastError
|
||||
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
|
||||
|
||||
ChoiceRow { setting: "colorScheme"; divider: false }
|
||||
ChoiceRow { setting: "colorScheme" }
|
||||
|
||||
// Drawn as the gradient each accent produces rather than a flat dot,
|
||||
// because the gradient is what is being chosen.
|
||||
AccentPicker {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -61,3 +61,4 @@ PrivacyPage 1.0 PrivacyPage.qml
|
||||
RegionPage 1.0 RegionPage.qml
|
||||
SearchPicker 1.0 SearchPicker.qml
|
||||
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
|
||||
AccentPicker 1.0 AccentPicker.qml
|
||||
|
||||
@@ -29,6 +29,16 @@ Singleton {
|
||||
readonly property string inactiveBorderDark: "rgba(3b426199)"
|
||||
readonly property string inactiveBorderLight: "rgba(a8aecb99)"
|
||||
readonly property string inactiveBorder: root.dark ? root.inactiveBorderDark : root.inactiveBorderLight
|
||||
|
||||
// The focused border follows the chosen accent. `ee` is the shipped alpha
|
||||
// for the Prism gradient; Theme owns which colours, this owns the form the
|
||||
// compositor wants them in.
|
||||
function hyprColor(value: var): string {
|
||||
// Qt gives "#rrggbb"; Hyprland wants rgba(rrggbbaa).
|
||||
return "rgba(" + String(value).replace("#", "").slice(0, 6) + "ee)";
|
||||
}
|
||||
readonly property string accentBorderStart: root.hyprColor(Theme.accent)
|
||||
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
|
||||
property string lastError: ""
|
||||
|
||||
// Applied one command at a time: Process runs a single command, and several
|
||||
@@ -104,11 +114,24 @@ Singleton {
|
||||
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
|
||||
];
|
||||
|
||||
// Unfocused window borders need scheme-relative contrast. The focused
|
||||
// Prism border is deliberately owned by the accent/theme layer.
|
||||
// Unfocused window borders need scheme-relative contrast, and stay this
|
||||
// service's to own. The FOCUSED border is the accent role and belongs to
|
||||
// the theme -- see modules/settings/README.md -- so it is written from
|
||||
// the chosen accent rather than from the scheme.
|
||||
//
|
||||
// Both are pushed together because both change when the scheme flips:
|
||||
// each accent carries a separate pair for light and dark, so switching
|
||||
// schemes must restate the focused border too, not only the neutral one.
|
||||
commands.push(["hyprctl", "eval",
|
||||
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
||||
|
||||
// A two-stop gradient at the shipped angle. Written as a Lua TABLE: the
|
||||
// string form of a gradient carries only one stop, and passing
|
||||
// "rgba(a) rgba(b) 115deg" as a string is accepted and silently keeps
|
||||
// the previous value.
|
||||
commands.push(["hyprctl", "eval",
|
||||
`hl.config({ general = { col = { active_border = { colors = { "${root.accentBorderStart}", "${root.accentBorderEnd}" }, angle = 115 } } } })`]);
|
||||
|
||||
// Applications that predate org.freedesktop.appearance and carry their
|
||||
// own palettes -- terminals, chiefly. Everything that reads the portal
|
||||
// (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings
|
||||
|
||||
Reference in New Issue
Block a user