186 lines
7.1 KiB
QML
186 lines
7.1 KiB
QML
// Displays.
|
||
//
|
||
// Resolution, refresh rate, scale, and rotation, plus the gaming display
|
||
// policy that was already here.
|
||
//
|
||
// Every geometry change goes through an apply-then-confirm countdown. This is
|
||
// the one page where a wrong value can leave the screen unreadable or blank,
|
||
// and no other control in the app can undo it once that happens. Confirming is
|
||
// what writes the choice to the settings store; letting the countdown run
|
||
// leaves nothing behind.
|
||
|
||
import QtQuick
|
||
import qs.config
|
||
import qs.services
|
||
import qs.widgets
|
||
|
||
SettingsPage {
|
||
id: root
|
||
|
||
title: "Displays"
|
||
lede: SystemSettings.monitorDescription || "Reading the active display…"
|
||
|
||
readonly property var monitor: Displays.monitors.length > 0 ? Displays.monitors[0] : null
|
||
readonly property string currentMode: root.monitor
|
||
? `${root.monitor.width}x${root.monitor.height}@${Math.round(root.monitor.refreshRate)}`
|
||
: ""
|
||
|
||
// The confirmation sits above everything, because while it is counting down
|
||
// it is the only thing that matters on this page.
|
||
header: Component {
|
||
Rectangle {
|
||
visible: Displays.awaitingConfirmation
|
||
implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0
|
||
radius: Theme.cardRadius
|
||
color: Theme.mix(Theme.bgPanel, Theme.warn, 0.12)
|
||
border.width: 1
|
||
border.color: Theme.alpha(Theme.warn, 0.4)
|
||
|
||
Row {
|
||
id: confirmRow
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.margins: 16
|
||
spacing: 14
|
||
|
||
Column {
|
||
width: parent.width - keepButton.width - revertButton.width - 28
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 3
|
||
|
||
Text {
|
||
width: parent.width
|
||
text: "Keep this display setting?"
|
||
color: Theme.fg
|
||
font.family: Theme.fontFamily
|
||
font.pixelSize: Theme.fontSize
|
||
font.weight: Font.DemiBold
|
||
}
|
||
Text {
|
||
width: parent.width
|
||
text: "Reverting in " + Displays.secondsLeft + (Displays.secondsLeft === 1 ? " second" : " seconds")
|
||
+ " if you do nothing. If you cannot read this, just wait."
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontFamily
|
||
font.features: Theme.tabularFigures
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
wrapMode: Text.WordWrap
|
||
}
|
||
}
|
||
|
||
SettingsButton {
|
||
id: revertButton
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "Revert now"
|
||
onClicked: Displays.revert()
|
||
}
|
||
SettingsButton {
|
||
id: keepButton
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "Keep"
|
||
enabled: Displays.canConfirm
|
||
onClicked: Displays.confirm()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
SettingsCard {
|
||
title: root.monitor ? root.monitor.name : (SystemSettings.monitorName || "Active display")
|
||
subtitle: root.monitor
|
||
? `${root.monitor.description} · ${root.monitor.width} × ${root.monitor.height} at ${Math.round(root.monitor.refreshRate)} Hz · ${root.monitor.scale.toFixed(2)}× scale`
|
||
: "Reading the active display…"
|
||
|
||
TextRow {
|
||
label: "Color mode"
|
||
detail: "Wide-gamut SDR at 10-bit. Full-time HDR is left to the Hyprland config: it currently breaks screenshots, OBS, and the lock screen's blurred background."
|
||
value: SystemSettings.colorPreset || "wide"
|
||
}
|
||
TextRow {
|
||
label: "Variable refresh"
|
||
detail: SystemSettings.monitorVrrActive
|
||
? "Active for current fullscreen content"
|
||
: "Ready when game or video content requests it"
|
||
value: SystemSettings.monitorVrrActive ? "Active" : "Standby"
|
||
divider: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
||
}
|
||
ActionRow {
|
||
visible: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
||
label: "Using a custom display setting"
|
||
detail: "Forget it to go back to the resolution and scale Panama ships"
|
||
action: "Forget"
|
||
divider: false
|
||
onTriggered: Displays.forget(root.monitor.name)
|
||
}
|
||
}
|
||
|
||
SettingsCard {
|
||
visible: root.monitor !== null
|
||
title: "Resolution"
|
||
subtitle: "Applied straight away, then reverted automatically unless you confirm."
|
||
|
||
DisplayModePicker {
|
||
width: parent.width
|
||
monitor: root.monitor
|
||
enabled: !Displays.awaitingConfirmation && !Displays.busy
|
||
}
|
||
}
|
||
|
||
SettingsCard {
|
||
visible: root.monitor !== null
|
||
title: "Scale and rotation"
|
||
|
||
ChoiceGrid {
|
||
width: parent.width
|
||
label: "Scale"
|
||
detail: "Fractional scales that do not divide the resolution into whole pixels are rejected by the compositor, so only clean ones are offered."
|
||
options: Displays.scalesForMode(root.currentMode)
|
||
.map(scale => ({ value: scale, label: scale.toFixed(2) + "×" }))
|
||
current: root.monitor ? root.monitor.scale : 1
|
||
enabled: !Displays.awaitingConfirmation && !Displays.busy
|
||
onPicked: value => root.applyWith({ scale: value })
|
||
}
|
||
|
||
ChoiceGrid {
|
||
width: parent.width
|
||
label: "Rotation"
|
||
options: Displays.transforms
|
||
current: root.monitor ? root.monitor.transform : 0
|
||
enabled: !Displays.awaitingConfirmation && !Displays.busy
|
||
divider: false
|
||
onPicked: value => root.applyWith({ transform: value })
|
||
}
|
||
}
|
||
|
||
SettingsCard {
|
||
title: "Gaming display policy"
|
||
subtitle: "Applied immediately and restored when Panama starts."
|
||
|
||
ToggleRow { setting: "autoHdr" }
|
||
ChoiceRow { setting: "vrrPolicy" }
|
||
ChoiceRow { setting: "directScanoutPolicy"; divider: false }
|
||
}
|
||
|
||
SettingsCard {
|
||
visible: Displays.lastError !== ""
|
||
title: "Display problem"
|
||
subtitle: Displays.lastError
|
||
}
|
||
|
||
// Applies a change to one field, keeping the others at what is in effect.
|
||
function applyWith(change: var): void {
|
||
if (!root.monitor)
|
||
return;
|
||
const mode = change.mode ?? root.currentMode;
|
||
const requestedScale = change.scale ?? root.monitor.scale;
|
||
Displays.apply(
|
||
root.monitor.name,
|
||
mode,
|
||
Displays.isScaleClean(mode, requestedScale)
|
||
? requestedScale
|
||
: Displays.nearestCleanScale(mode, requestedScale),
|
||
change.transform ?? root.monitor.transform);
|
||
}
|
||
}
|