579 lines
20 KiB
QML
579 lines
20 KiB
QML
// Appearance — the page the rest of the settings vocabulary was built for.
|
|
//
|
|
// Before Stage 3 this page adjusted a clock format and three vitals toggles,
|
|
// and its "Theme" card was two rows of text pretending to be controls. It now
|
|
// drives the compositor directly: every row here is applied through
|
|
// `hyprctl eval` and confirmed by reading the value back before it is stored.
|
|
//
|
|
// The preview is pinned above the controls rather than sitting inline, because
|
|
// the numbers are meaningless on their own -- "outer gaps 24" only means
|
|
// something once you have watched the windows move apart by that much, at the
|
|
// scale of the display you actually use.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
property string expandedPicker: ""
|
|
|
|
// Whether the accent's HSV fine-tune is open. Collapsed by default: it is
|
|
// six sliders that answer a question the four color wells above have
|
|
// usually already answered.
|
|
property bool fineTuneOpen: false
|
|
|
|
// Which group of cards is on screen. Themes leads deliberately: light and
|
|
// dark is the control reached most often, and it used to be the third
|
|
// section down, below a wallpaper grid and the whole lock screen.
|
|
property string tab: "themes"
|
|
|
|
// Arriving from another page that named a section opens on it. Taken once
|
|
// rather than bound, so the tabs still work normally afterwards. The old
|
|
// `theme` id still resolves: deep links, IPC calls and muscle memory all
|
|
// hold it, and it means the same thing this tab does.
|
|
Component.onCompleted: {
|
|
const section = ShellState.takeSettingsSection();
|
|
if (section === "theme")
|
|
root.tab = "themes";
|
|
else if (section !== "")
|
|
root.tab = section;
|
|
}
|
|
|
|
title: "Appearance"
|
|
lede: "Tune the Prism shell and the applications that live inside it. The preview above is your real geometry, to scale."
|
|
|
|
header: Component {
|
|
Column {
|
|
spacing: 9
|
|
|
|
DesktopPreview {
|
|
width: parent.width
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: SystemSettings.lastError !== ""
|
|
? SystemSettings.lastError
|
|
: "Live preview — the focused window wears the prism border"
|
|
color: SystemSettings.lastError !== "" ? Theme.warn : Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsTabs {
|
|
tabs: [
|
|
{ value: "themes", label: "Themes" },
|
|
{ value: "editor", label: "Theme editor" },
|
|
{ value: "background", label: "Background" },
|
|
{ value: "type", label: "Typography" },
|
|
{ value: "windows", label: "Windows" },
|
|
]
|
|
current: root.tab
|
|
onSelected: value => root.tab = value
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "background"
|
|
title: "Background"
|
|
subtitle: Wallpaper.lastError !== ""
|
|
? Wallpaper.lastError
|
|
: "Applied to every display, from the folder below."
|
|
|
|
TextEntryRow { setting: "wallpaperDir"; placeholder: "Pictures/Wallpapers" }
|
|
|
|
WallpaperControls {
|
|
id: wallpaperControls
|
|
width: parent.width
|
|
}
|
|
|
|
WallpaperPicker {
|
|
id: wallpapers
|
|
width: parent.width
|
|
mode: wallpaperControls.mode
|
|
selectedOutput: wallpaperControls.selectedOutput
|
|
}
|
|
|
|
ActionRow {
|
|
visible: wallpapers.hidden > 0 || wallpapers.expanded
|
|
label: wallpapers.expanded ? "Showing every image" : wallpapers.hidden + " more available"
|
|
detail: "The grid is kept short so the rest of this page stays reachable"
|
|
action: wallpapers.expanded ? "Show fewer" : "Show all"
|
|
onTriggered: wallpapers.expanded = !wallpapers.expanded
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Look for new images"
|
|
detail: Wallpaper.scanning
|
|
? "Scanning…"
|
|
: Wallpaper.available.length + " image" + (Wallpaper.available.length === 1 ? "" : "s") + " found"
|
|
action: "Rescan"
|
|
divider: false
|
|
enabled: !Wallpaper.scanning
|
|
onTriggered: Wallpaper.rescan()
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "background"
|
|
title: "Video playback"
|
|
subtitle: VideoWallpaper.lastError !== ""
|
|
? VideoWallpaper.lastError
|
|
: "Videos loop muted and decode on the GPU. Considerate by default — a video wallpaper should never cost you a frame you care about."
|
|
|
|
TextEntryRow { setting: "videoWallpaperDir"; placeholder: "Videos/Wallpapers" }
|
|
|
|
ToggleRow { setting: "videoWallpaperPauseOnBattery" }
|
|
|
|
TextRow {
|
|
label: "During games and fullscreen"
|
|
detail: "Pauses automatically — not optional, and free"
|
|
value: "Automatic"
|
|
}
|
|
|
|
TextRow {
|
|
label: "Pause from the bar"
|
|
detail: "While a video plays, a pill sits in the bar — one click pauses it, for remote desktop or just quiet"
|
|
value: ""
|
|
}
|
|
|
|
TextRow {
|
|
label: "Lock screen"
|
|
detail: "Uses a still frame of the video — hyprlock cannot play motion"
|
|
value: "Still frame"
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Look for new videos"
|
|
detail: VideoWallpaper.available
|
|
? VideoWallpaper.candidates.length + " video" + (VideoWallpaper.candidates.length === 1 ? "" : "s") + " found"
|
|
: "mpvpaper is not installed — run panama update to pick it up"
|
|
action: "Rescan"
|
|
divider: false
|
|
enabled: VideoWallpaper.available
|
|
onTriggered: VideoWallpaper.rescan()
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "background"
|
|
title: "Lock screen"
|
|
subtitle: LockScreen.lastError !== ""
|
|
? LockScreen.lastError
|
|
: "A representative preview of the screen shown before authentication."
|
|
|
|
LockScreenPreview {
|
|
width: parent.width
|
|
}
|
|
|
|
ChoiceRow { setting: "lockBackgroundMode" }
|
|
SliderRow { setting: "lockBlurLevel"; zeroLabel: "Off" }
|
|
ToggleRow { setting: "lockShowClock" }
|
|
ToggleRow { setting: "lockShowDate" }
|
|
ToggleRow { setting: "lockShowUser" }
|
|
ToggleRow { setting: "lockFadeOnEmpty"; divider: false }
|
|
}
|
|
|
|
// ── Themes ──────────────────────────────────────────────────────────────
|
|
//
|
|
// The gallery is the page, not a card on it. Cards would put a heading and
|
|
// a border around each of two lists that are already visually separate, and
|
|
// the theme cards themselves are the only surfaces here that need edges.
|
|
|
|
ThemeModeHero {
|
|
visible: root.tab === "themes"
|
|
width: parent.width
|
|
}
|
|
|
|
Text {
|
|
visible: root.tab === "themes" && ThemeCatalog.lastError !== ""
|
|
width: parent.width
|
|
text: ThemeCatalog.lastError
|
|
color: Theme.warn
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
ThemeGallery {
|
|
visible: root.tab === "themes"
|
|
width: parent.width
|
|
scheme: "dark"
|
|
heading: "Dark themes"
|
|
}
|
|
|
|
ThemeGallery {
|
|
visible: root.tab === "themes"
|
|
width: parent.width
|
|
scheme: "light"
|
|
heading: "Light themes"
|
|
}
|
|
|
|
// Dashed rather than solid, and the width of the page: this is a door out
|
|
// of the gallery, not an eleventh theme in it.
|
|
Rectangle {
|
|
id: buildLink
|
|
|
|
visible: root.tab === "themes"
|
|
width: parent.width
|
|
implicitHeight: buildCopy.implicitHeight + 30
|
|
radius: Theme.cardRadius + 2
|
|
color: Theme.alpha(Theme.accent, buildHover.hovered ? 0.12 : 0.07)
|
|
border.width: buildLink.activeFocus ? 2 : 1
|
|
border.color: buildLink.activeFocus
|
|
? Theme.accentSecondary : Theme.alpha(Theme.accent, 0.35)
|
|
activeFocusOnTab: true
|
|
|
|
Accessible.role: Accessible.Button
|
|
Accessible.name: "Build your own theme"
|
|
Accessible.description: "Opens the theme editor"
|
|
Accessible.focusable: true
|
|
Accessible.focused: buildLink.activeFocus
|
|
|
|
Keys.onReturnPressed: root.tab = "editor"
|
|
Keys.onSpacePressed: root.tab = "editor"
|
|
|
|
Text {
|
|
id: buildGlyph
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 17
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "◐"
|
|
color: Theme.accent
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: 20
|
|
}
|
|
|
|
Column {
|
|
id: buildCopy
|
|
|
|
anchors.left: buildGlyph.right
|
|
anchors.leftMargin: 13
|
|
anchors.right: buildArrow.left
|
|
anchors.rightMargin: 13
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 3
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "Build your own theme"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSize
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: "Primary, secondary, background, foreground, effects — every color yours, saved as a theme that lives in the galleries above"
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: buildArrow
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 17
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "→"
|
|
color: Theme.accent
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: 17
|
|
}
|
|
|
|
HoverHandler {
|
|
id: buildHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: root.tab = "editor"
|
|
}
|
|
}
|
|
|
|
// ── Theme editor ────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
id: editorCard
|
|
|
|
visible: root.tab === "editor"
|
|
title: "Your theme"
|
|
// The identity of what is being edited, always. Failures get their own
|
|
// line below rather than displacing it -- a subtitle that sometimes
|
|
// says which theme you are editing and sometimes says something went
|
|
// wrong answers neither question reliably.
|
|
subtitle: ThemeProfiles.activeProfile.shipped === true
|
|
? ThemeProfiles.activeProfile.name
|
|
+ " — editing it forks a copy you can name below"
|
|
: "Based on " + ThemeProfiles.activeProfile.name
|
|
+ " · every change previews live on the real desktop"
|
|
|
|
ErrorRow {
|
|
label: "The theme needs attention"
|
|
message: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError
|
|
divider: true
|
|
}
|
|
|
|
ThemeStartChips {
|
|
width: parent.width
|
|
bottomPadding: 13
|
|
}
|
|
|
|
SegmentRow {
|
|
label: "Scheme"
|
|
detail: "Flipping sides returns to the theme you last chose there"
|
|
options: [{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }]
|
|
value: ThemeProfiles.scheme
|
|
onSelected: value => ThemeProfiles.setScheme(value)
|
|
}
|
|
|
|
Item { width: 1; height: 13 }
|
|
|
|
ThemeEditorWells {
|
|
id: wells
|
|
width: parent.width
|
|
}
|
|
|
|
Item { width: 1; height: 13 }
|
|
|
|
ThemeSaturationRow {
|
|
width: parent.width
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Fine-tune"
|
|
detail: "Hue, saturation and value for each end of the accent"
|
|
action: root.fineTuneOpen ? "Close" : "Open"
|
|
onTriggered: root.fineTuneOpen = !root.fineTuneOpen
|
|
}
|
|
|
|
AccentEditor {
|
|
visible: root.fineTuneOpen
|
|
width: parent.width
|
|
}
|
|
|
|
ThemeSaveRow {
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
// One selector for every font. The two always-open lists and the
|
|
// open-one-at-a-time disclosure both retire: a closed dropdown per row
|
|
// reads as five choices instead of a wall of samples, and each row names
|
|
// what it actually controls — the old tab never said which font was the
|
|
// shell's and which was the applications'.
|
|
SettingsCard {
|
|
visible: root.tab === "type"
|
|
title: "Fonts"
|
|
subtitle: {
|
|
if (Fonts.lastError !== "")
|
|
return Fonts.lastError;
|
|
if (DesktopStyle.lastError !== "")
|
|
return DesktopStyle.lastError;
|
|
return "Each row names what it controls. The menu shows every family in its own face — type to filter.";
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Interface"
|
|
detail: Fonts.interfaceMissing
|
|
? "Not installed — fontconfig is substituting something else"
|
|
: "The shell — bar, dock, notifications, this window"
|
|
divider: false
|
|
controlWidth: 8
|
|
}
|
|
|
|
FontPicker {
|
|
width: parent.width
|
|
families: Fonts.interfaceFonts
|
|
current: Fonts.interfaceFont
|
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No interface fonts found"
|
|
onPicked: family => Fonts.setInterface(family)
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Icons"
|
|
detail: Fonts.iconMissing
|
|
? "Not installed — the shell's glyphs will not draw correctly"
|
|
: "Glyphs only, never text — needs a Nerd Font"
|
|
divider: false
|
|
controlWidth: 8
|
|
}
|
|
|
|
FontPicker {
|
|
width: parent.width
|
|
families: Fonts.iconFonts
|
|
current: Fonts.iconFont
|
|
emptyText: "No Nerd Fonts installed"
|
|
onPicked: family => Fonts.setIcon(family)
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Application"
|
|
detail: "GTK and libadwaita applications that follow the desktop default"
|
|
divider: false
|
|
controlWidth: 8
|
|
}
|
|
|
|
FontPicker {
|
|
width: parent.width
|
|
families: Fonts.interfaceFonts
|
|
current: DesktopStyle.applicationFont
|
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No application fonts found"
|
|
onPicked: family => DesktopStyle.setApplicationFont(family)
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Document"
|
|
detail: "Long-form reading, where applications ask for it"
|
|
divider: false
|
|
controlWidth: 8
|
|
}
|
|
|
|
FontPicker {
|
|
width: parent.width
|
|
families: Fonts.interfaceFonts
|
|
current: DesktopStyle.documentFont
|
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No document fonts found"
|
|
onPicked: family => DesktopStyle.setDocumentFont(family)
|
|
}
|
|
|
|
SettingRow {
|
|
label: "Monospace"
|
|
detail: "Terminals and code, wherever fixed width is asked for"
|
|
divider: false
|
|
controlWidth: 8
|
|
}
|
|
|
|
FontPicker {
|
|
width: parent.width
|
|
families: Fonts.monospaceFonts
|
|
current: DesktopStyle.monospaceFont
|
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No monospace fonts found"
|
|
onPicked: family => DesktopStyle.setMonospaceFont(family)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "type"
|
|
title: "Sizes"
|
|
subtitle: "The interface size moves the shell's whole scale; the rest are per role."
|
|
|
|
SliderRow { setting: "interfaceFontSize" }
|
|
SliderRow { setting: "applicationFontSize" }
|
|
SliderRow { setting: "documentFontSize" }
|
|
SliderRow { setting: "monospaceFontSize"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "type"
|
|
title: "Rendering"
|
|
|
|
ChoiceRow { setting: "fontHinting" }
|
|
ChoiceRow { setting: "fontAntialiasing"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "type"
|
|
title: "Icons & pointer"
|
|
subtitle: DesktopStyle.lastError !== ""
|
|
? DesktopStyle.lastError
|
|
: "Installed themes only. The pointer updates in applications and Hyprland together."
|
|
|
|
ActionRow {
|
|
label: "Application icons"
|
|
detail: DesktopStyle.iconTheme
|
|
action: root.expandedPicker === "icon-theme" ? "Close" : "Choose"
|
|
enabled: DesktopStyle.catalogLoaded
|
|
onTriggered: root.expandedPicker = root.expandedPicker === "icon-theme" ? "" : "icon-theme"
|
|
}
|
|
|
|
SearchPicker {
|
|
visible: root.expandedPicker === "icon-theme"
|
|
width: parent.width
|
|
items: DesktopStyle.iconThemes
|
|
current: DesktopStyle.iconTheme
|
|
placeholder: "Search icon themes"
|
|
emptyText: DesktopStyle.scanning ? "Reading installed icon themes…" : "No icon themes found"
|
|
onPicked: value => {
|
|
if (DesktopStyle.setIconTheme(value))
|
|
root.expandedPicker = "";
|
|
}
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Pointer theme"
|
|
detail: DesktopStyle.cursorTheme
|
|
action: root.expandedPicker === "cursor-theme" ? "Close" : "Choose"
|
|
enabled: DesktopStyle.catalogLoaded
|
|
divider: false
|
|
onTriggered: root.expandedPicker = root.expandedPicker === "cursor-theme" ? "" : "cursor-theme"
|
|
}
|
|
|
|
SearchPicker {
|
|
visible: root.expandedPicker === "cursor-theme"
|
|
width: parent.width
|
|
items: DesktopStyle.cursorThemes
|
|
current: DesktopStyle.cursorTheme
|
|
placeholder: "Search pointer themes"
|
|
emptyText: DesktopStyle.scanning ? "Reading installed pointer themes…" : "No pointer themes found"
|
|
onPicked: value => {
|
|
if (DesktopStyle.setCursorTheme(value))
|
|
root.expandedPicker = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "windows"
|
|
title: "Titlebars"
|
|
subtitle: "One rule for GNOME applications and Panama's own windows. There is no minimize or maximize toggle: Hyprland has no minimize, so the buttons would be lies."
|
|
|
|
ToggleRow { setting: "panamaTitlebar" }
|
|
ChoiceRow { setting: "titlebarButtonSide"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: root.tab === "windows"
|
|
title: "Windows"
|
|
subtitle: "Spacing and shape of tiled windows. Each change is applied to the compositor and confirmed before it is saved."
|
|
|
|
SliderRow { setting: "windowRounding" }
|
|
SliderRow { setting: "gapsIn" }
|
|
SliderRow { setting: "gapsOut" }
|
|
SliderRow { setting: "borderSize"; zeroLabel: "None" }
|
|
SliderRow { setting: "roundingPower" }
|
|
SliderRow { setting: "inactiveOpacity" }
|
|
SliderRow { setting: "activeOpacity" }
|
|
SliderRow { setting: "fullscreenOpacity"; divider: false }
|
|
}
|
|
|
|
// Effects belong to the theme now: saving snapshots all ten values into the
|
|
// record, and applying a theme that carries them puts them back. They sit
|
|
// in the editor rather than beside the galleries for exactly that reason.
|
|
SettingsCard {
|
|
visible: root.tab === "editor"
|
|
title: "Effects"
|
|
subtitle: "Part of your theme — corners, blur, shadows, glow and motion save with it. Each costs frame time; turning one off is a legitimate way to buy it back while gaming."
|
|
|
|
ToggleRow { setting: "blurEnabled" }
|
|
SliderRow { setting: "blurSize" }
|
|
SliderRow { setting: "blurPasses" }
|
|
ToggleRow { setting: "shadowEnabled" }
|
|
SliderRow { setting: "shadowRange"; zeroLabel: "None" }
|
|
SliderRow { setting: "shadowRenderPower" }
|
|
ToggleRow { setting: "shadowSharp" }
|
|
ToggleRow { setting: "glowEnabled" }
|
|
SliderRow { setting: "glowRange"; zeroLabel: "None" }
|
|
ToggleRow { setting: "animationsEnabled"; divider: false }
|
|
}
|
|
|
|
}
|