Fun panels
This commit is contained in:
@@ -225,6 +225,21 @@ Panel {
|
||||
return String(Math.round(n * 100) / 100)
|
||||
}
|
||||
|
||||
// Playful mood-name for a given brightness percent. Bands intentionally
|
||||
// span ~10–20 points so casual tweaks change the label, while small
|
||||
// nudges within one band don't.
|
||||
function brightnessName(percent) {
|
||||
var p = Math.round(percent)
|
||||
if (p >= 95) return "Sun blast"
|
||||
if (p >= 80) return "Solar flare"
|
||||
if (p >= 65) return "Golden hour"
|
||||
if (p >= 45) return "Even day"
|
||||
if (p >= 30) return "Soft glow"
|
||||
if (p >= 20) return "Lamp light"
|
||||
if (p >= 10) return "Candlelit"
|
||||
return "Night owl"
|
||||
}
|
||||
|
||||
function updateDisplays(displaysJson) {
|
||||
try {
|
||||
root.displays = displaysJson ? JSON.parse(displaysJson) : []
|
||||
@@ -355,7 +370,7 @@ Panel {
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
focusTarget: keyCatcher
|
||||
contentWidth: panel.fittedContentWidth(Style.space(320))
|
||||
contentWidth: panel.fittedContentWidth(Style.space(380))
|
||||
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
|
||||
|
||||
PanelKeyCatcher {
|
||||
@@ -384,108 +399,121 @@ Panel {
|
||||
width: scrollArea.availableWidth
|
||||
spacing: Style.space(14)
|
||||
|
||||
// ---- Brightness ----
|
||||
Column {
|
||||
// ---------- Hero: sun icon · BRIGHTNESS · percentage ----------
|
||||
Item {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
visible: root.brightnessAvailable
|
||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroPercent.implicitHeight)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "Brightness"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
CursorSurface {
|
||||
id: brightnessRow
|
||||
visible: root.brightnessAvailable
|
||||
width: parent.width
|
||||
height: brightnessInner.implicitHeight + Style.spacing.controlGap
|
||||
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
|
||||
foreground: root.bar.foreground
|
||||
outline: true
|
||||
|
||||
Row {
|
||||
id: brightnessInner
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(6)
|
||||
anchors.rightMargin: Style.space(6)
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.heading
|
||||
width: Style.space(22)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
PanelSlider {
|
||||
id: brightnessSlider
|
||||
bar: root.bar
|
||||
width: parent.width - Style.space(22) - brightnessLabel.width - Style.space(16)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
step: 1
|
||||
value: root.brightnessPercent
|
||||
integer: true
|
||||
onMoved: function(v) { root.previewBrightness(v) }
|
||||
onReleased: function(v) {
|
||||
brightnessDebounce.stop()
|
||||
root.setBrightness(v)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: brightnessLabel
|
||||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
width: Style.space(36)
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (hovered) {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "brightness"
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
Text {
|
||||
id: heroIcon
|
||||
text: root.displays.length > 1 ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.brightnessAvailable
|
||||
text: "No controllable backlight found"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
id: heroLabel
|
||||
text: root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
anchors.left: heroIcon.right
|
||||
anchors.leftMargin: Style.space(14)
|
||||
anchors.right: heroPercent.left
|
||||
anchors.rightMargin: Style.space(10)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: heroPercent
|
||||
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.displayLarge
|
||||
font.bold: true
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Scale ----
|
||||
// ---------- Brightness slider (full width below hero) ----------
|
||||
CursorSurface {
|
||||
id: brightnessRow
|
||||
visible: root.brightnessAvailable
|
||||
width: parent.width
|
||||
height: brightnessSlider.implicitHeight + Style.spacing.controlGap
|
||||
hasCursor: root.cursorActive && root.focusSection === "brightness" && root.selectedIndex === -1
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(brightnessRow)
|
||||
foreground: root.bar.foreground
|
||||
outline: true
|
||||
|
||||
PanelSlider {
|
||||
id: brightnessSlider
|
||||
bar: root.bar
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(6)
|
||||
anchors.rightMargin: Style.space(6)
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
step: 1
|
||||
value: root.brightnessPercent
|
||||
integer: true
|
||||
onMoved: function(v) { root.previewBrightness(v) }
|
||||
onReleased: function(v) {
|
||||
brightnessDebounce.stop()
|
||||
root.setBrightness(v)
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: if (hovered) {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "brightness"
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.brightnessAvailable
|
||||
text: "No controllable backlight found"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
visible: root.brightnessAvailable
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
// ---------- Scale ----------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
spacing: Style.space(10)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "Scale"
|
||||
text: "SCALE"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
Row {
|
||||
id: scaleRow
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
|
||||
readonly property real cellWidth: root.scaleValues.length > 0
|
||||
? (width - spacing * (root.scaleValues.length - 1)) / root.scaleValues.length
|
||||
: 0
|
||||
|
||||
Repeater {
|
||||
model: root.scaleValues
|
||||
|
||||
@@ -495,22 +523,27 @@ Panel {
|
||||
|
||||
scaleValue: modelData
|
||||
scaleIndex: index
|
||||
width: scaleRow.cellWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Monitors ----
|
||||
PanelSeparator {
|
||||
visible: root.displays.length > 1
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
// ---------- Monitors ----------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
spacing: Style.space(10)
|
||||
visible: root.displays.length > 1
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "Monitors"
|
||||
text: "DISPLAYS"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
Repeater {
|
||||
@@ -537,10 +570,12 @@ Panel {
|
||||
required property int scaleIndex
|
||||
|
||||
text: scaleValue + "x"
|
||||
fontSize: Style.font.bodySmall
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
horizontalPadding: Style.spacing.controlPaddingX
|
||||
verticalPadding: Style.spacing.controlPaddingY
|
||||
verticalPadding: Style.spacing.controlPaddingY + Style.space(2)
|
||||
bordered: true
|
||||
|
||||
active: root.normalizeScale(root.monitorScale) === root.normalizeScale(scaleValue)
|
||||
hasCursor: root.cursorActive && root.focusSection === "scale" && root.selectedIndex === scaleIndex
|
||||
|
||||
+265
-95
@@ -28,14 +28,14 @@ Panel {
|
||||
|
||||
function batteryIcon() {
|
||||
var device = UPower.displayDevice
|
||||
if (!device || !device.isPresent) return ""
|
||||
if (!device || !device.isPresent) return ""
|
||||
|
||||
var chargingIcons = ["", "", "", "", "", "", "", "", "", ""]
|
||||
var defaultIcons = ["", "", "", "", "", "", "", "", "", ""]
|
||||
var index = Math.max(0, Math.min(9, Math.floor(device.percentage * 10)))
|
||||
|
||||
if (device.state === UPowerDeviceState.FullyCharged) return ""
|
||||
if (!UPower.onBattery && device.state !== UPowerDeviceState.Charging) return ""
|
||||
if (!UPower.onBattery && device.state !== UPowerDeviceState.Charging) return ""
|
||||
if (device.state === UPowerDeviceState.Charging) return chargingIcons[index]
|
||||
return defaultIcons[index]
|
||||
}
|
||||
@@ -47,17 +47,82 @@ Panel {
|
||||
if (!UPower.onBattery && percentage >= 1) {
|
||||
return "Fully charged"
|
||||
} else if (UPower.onBattery) {
|
||||
return "Battery"
|
||||
return "On battery"
|
||||
} else {
|
||||
return "Charging"
|
||||
}
|
||||
}
|
||||
|
||||
function profileIcon(name) {
|
||||
if (name === "power-saver") return ""
|
||||
if (name === "balanced") return ""
|
||||
if (name === "performance") return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property bool fullyCharged: {
|
||||
var device = UPower.displayDevice
|
||||
return device && device.isPresent && device.state === UPowerDeviceState.FullyCharged
|
||||
}
|
||||
|
||||
// 0..1 charge level, used by the visual progress bar.
|
||||
readonly property real batteryFraction: {
|
||||
var d = UPower.displayDevice
|
||||
return d && d.isPresent ? Math.max(0, Math.min(1, d.percentage)) : 0
|
||||
}
|
||||
|
||||
readonly property bool batteryLow: UPower.onBattery && batteryFraction > 0 && batteryFraction <= 0.2
|
||||
readonly property bool charging: {
|
||||
var d = UPower.displayDevice
|
||||
return d && d.isPresent && d.state === UPowerDeviceState.Charging
|
||||
}
|
||||
|
||||
readonly property color batteryFillColor: {
|
||||
if (batteryLow) return Color.urgent
|
||||
return root.bar ? root.bar.foreground : Color.foreground
|
||||
}
|
||||
|
||||
// Cute agent-flavored phrases shown in the hero status line, rotated on a
|
||||
// timer so the panel feels alive when current is flowing (either direction).
|
||||
readonly property var chargingPhrases: [
|
||||
"Pumping power",
|
||||
"Injecting electrons",
|
||||
"Pouring juice",
|
||||
"Amassing watts",
|
||||
"Hoarding joules",
|
||||
"Sucking volts",
|
||||
"Topping reserves",
|
||||
"Soaking amps",
|
||||
"Inhaling kilowatts"
|
||||
]
|
||||
readonly property var onBatteryPhrases: [
|
||||
"Slurping power",
|
||||
"Spending joules",
|
||||
"Draining watts",
|
||||
"Burning electrons",
|
||||
"Sipping juice",
|
||||
"Spending coulombs",
|
||||
"Bleeding amps",
|
||||
"Guzzling volts",
|
||||
"Munching reserves"
|
||||
]
|
||||
property int phraseIndex: 0
|
||||
|
||||
// Whichever list is "active" given the current power state.
|
||||
readonly property var activePhrases: {
|
||||
if (fullyCharged) return []
|
||||
if (charging) return chargingPhrases
|
||||
if (UPower.onBattery) return onBatteryPhrases
|
||||
return []
|
||||
}
|
||||
readonly property bool rotatingPhrases: activePhrases.length > 0
|
||||
|
||||
readonly property string heroStatusText: {
|
||||
if (fullyCharged) return "Fully charged"
|
||||
if (rotatingPhrases) return activePhrases[phraseIndex % activePhrases.length]
|
||||
return modeLabel()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!batteryProc.running) batteryProc.running = true
|
||||
if (!profilesProc.running) profilesProc.running = true
|
||||
@@ -72,6 +137,9 @@ Panel {
|
||||
if (idx <= 0) continue
|
||||
next[lines[i].substring(0, idx)] = lines[i].substring(idx + 1).trim()
|
||||
}
|
||||
// Keep last known good data if a refresh briefly returns nothing — happens
|
||||
// around AC plug/unplug events. Avoids the section collapsing mid-transition.
|
||||
if (Object.keys(next).length === 0) return
|
||||
if (targetName === "battery") batteryInfo = next
|
||||
else systemInfo = next
|
||||
}
|
||||
@@ -87,6 +155,9 @@ Panel {
|
||||
list.push(parts[0])
|
||||
if (parts[1] === "1") active = parts[0]
|
||||
}
|
||||
// Same guard as battery: preserve the last known profile list across
|
||||
// transient empty payloads so the buttons don't blink out.
|
||||
if (list.length === 0) return
|
||||
profiles = list
|
||||
activeProfile = active
|
||||
if (profileIndex >= profiles.length) profileIndex = Math.max(0, profiles.length - 1)
|
||||
@@ -139,6 +210,49 @@ Panel {
|
||||
|
||||
Timer { interval: 5000; running: root.opened; repeat: true; onTriggered: root.refresh() }
|
||||
|
||||
// Rotate the status phrase while the panel is open and we're in a
|
||||
// rotating state (charging or on battery). The text swap is wrapped in a
|
||||
// fade so the changeover reads as one organism rather than a hard cut.
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
running: root.opened && root.rotatingPhrases
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
onTriggered: phraseSwap.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: phraseSwap
|
||||
PropertyAnimation {
|
||||
target: heroStatus; property: "opacity"
|
||||
to: 0.0; duration: 180; easing.type: Easing.OutQuad
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
var n = root.activePhrases.length
|
||||
if (n > 0) root.phraseIndex = (root.phraseIndex + 1) % n
|
||||
}
|
||||
}
|
||||
PropertyAnimation {
|
||||
target: heroStatus; property: "opacity"
|
||||
to: 1.0; duration: 260; easing.type: Easing.InQuad
|
||||
}
|
||||
}
|
||||
|
||||
// If we leave a rotating state mid-swap, halt the animation and snap back
|
||||
// to full opacity so "FULLY CHARGED" is legible immediately rather than
|
||||
// appearing dimmed.
|
||||
Connections {
|
||||
target: root
|
||||
function onRotatingPhrasesChanged() {
|
||||
if (!root.rotatingPhrases) {
|
||||
phraseSwap.stop()
|
||||
heroStatus.opacity = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
@@ -158,7 +272,7 @@ Panel {
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
focusTarget: keyCatcher
|
||||
contentWidth: panel.fittedContentWidth(Style.space(340))
|
||||
contentWidth: panel.fittedContentWidth(Style.space(380))
|
||||
contentHeight: panel.fittedContentHeight(column.implicitHeight)
|
||||
|
||||
PanelKeyCatcher {
|
||||
@@ -177,106 +291,163 @@ Panel {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Style.space(16)
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: Style.space(28)
|
||||
spacing: Style.space(14)
|
||||
|
||||
// ---------- Hero: battery icon · status · percentage ----------
|
||||
Item {
|
||||
id: iconWrapper
|
||||
width: Style.space(28)
|
||||
height: Style.space(28)
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroPercent.implicitHeight)
|
||||
|
||||
Text {
|
||||
id: acIcon
|
||||
id: heroIcon
|
||||
text: root.batteryIcon()
|
||||
color: root.bar.foreground
|
||||
color: root.batteryLow ? Color.urgent : root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.iconLarge
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Style.font.display
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: heroStatus
|
||||
text: root.heroStatusText.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
anchors.left: heroIcon.right
|
||||
anchors.leftMargin: Style.space(14)
|
||||
anchors.right: heroPercent.left
|
||||
anchors.rightMargin: Style.space(10)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: heroPercent
|
||||
text: root.batteryInfo.percentage || "—"
|
||||
color: root.batteryLow ? Color.urgent : root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.displayLarge
|
||||
font.bold: true
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.modeLabel()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
anchors.left: iconWrapper.right
|
||||
anchors.leftMargin: Style.spacing.controlPaddingX
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.batteryInfo.percentage !== undefined
|
||||
text: root.batteryInfo.percentage || ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
visible: root.batteryInfo.percentage !== undefined && !root.fullyCharged
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(24)
|
||||
|
||||
Column {
|
||||
width: Style.space(140)
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" }
|
||||
InfoPair { label: "Threshold"; value: root.batteryInfo.threshold || "—" }
|
||||
}
|
||||
|
||||
Column {
|
||||
width: Style.space(140)
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair { label: UPower.onBattery ? "Time left" : "Time to full"; value: root.batteryInfo.time || "—" }
|
||||
InfoPair { label: UPower.onBattery ? "Discharging" : "Charging"; value: root.batteryInfo.rate || "" }
|
||||
}
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
visible: !root.fullyCharged
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(12)
|
||||
PanelSectionHeader {
|
||||
visible: !root.fullyCharged
|
||||
text: "POWER PROFILE"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
Row {
|
||||
// ---------- Battery progress bar ----------
|
||||
Item {
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
Repeater {
|
||||
model: root.profiles
|
||||
Button {
|
||||
required property var modelData
|
||||
required property int index
|
||||
text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1)
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
horizontalPadding: Style.spacing.controlPaddingX
|
||||
verticalPadding: Style.spacing.controlPaddingY
|
||||
active: root.activeProfile === modelData
|
||||
hasCursor: root.cursorActive && root.profileIndex === index
|
||||
onClicked: root.setProfile(modelData)
|
||||
onHovered: function(h) {
|
||||
if (h) {
|
||||
root.cursorActive = true
|
||||
root.profileIndex = index
|
||||
implicitHeight: Style.space(8)
|
||||
|
||||
Rectangle {
|
||||
id: barTrack
|
||||
anchors.fill: parent
|
||||
radius: height / 2
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: barFill
|
||||
anchors.left: barTrack.left
|
||||
anchors.verticalCenter: barTrack.verticalCenter
|
||||
height: barTrack.height
|
||||
radius: barTrack.radius
|
||||
color: root.batteryFillColor
|
||||
width: Math.max(barTrack.height, barTrack.width * root.batteryFraction)
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 320; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 220 } }
|
||||
|
||||
// Subtle pulse while charging — visible signal that energy is flowing in.
|
||||
SequentialAnimation on opacity {
|
||||
running: root.charging && !root.fullyCharged && root.opened
|
||||
loops: Animation.Infinite
|
||||
alwaysRunToEnd: true
|
||||
NumberAnimation { from: 1.0; to: 0.55; duration: 950; easing.type: Easing.InOutSine }
|
||||
NumberAnimation { from: 0.55; to: 1.0; duration: 950; easing.type: Easing.InOutSine }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Stats ----------
|
||||
// Visibility is intentionally only gated by "we've ever loaded data" so
|
||||
// the section never collapses mid-transition. fullyCharged is *not* part
|
||||
// of the condition: UPower briefly reports FullyCharged on plug-in when
|
||||
// the battery sits above the charge-control start threshold, and we
|
||||
// refuse to flicker the whole panel for that ~1s window.
|
||||
Row {
|
||||
visible: root.batteryInfo.percentage !== undefined
|
||||
width: parent.width
|
||||
spacing: Style.space(20)
|
||||
|
||||
Column {
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair { label: "Battery size"; value: root.batteryInfo.size || "" }
|
||||
InfoPair { label: "Threshold"; value: root.batteryInfo.threshold || "—" }
|
||||
}
|
||||
|
||||
Column {
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair { label: UPower.onBattery ? "Time left" : "Time to full"; value: root.batteryInfo.time || "—" }
|
||||
InfoPair { label: UPower.onBattery ? "Discharging" : "Charging"; value: root.batteryInfo.rate || "" }
|
||||
}
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
// ---------- Power profile picker ----------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(10)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "POWER PROFILE"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
Row {
|
||||
id: profileRow
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
|
||||
readonly property real cellWidth: root.profiles.length > 0
|
||||
? (width - spacing * (root.profiles.length - 1)) / root.profiles.length
|
||||
: 0
|
||||
|
||||
Repeater {
|
||||
model: root.profiles
|
||||
Button {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: profileRow.cellWidth
|
||||
iconText: root.profileIcon(String(modelData))
|
||||
iconSize: Style.font.title
|
||||
text: String(modelData).charAt(0).toUpperCase() + String(modelData).slice(1)
|
||||
fontSize: Style.font.bodySmall
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
horizontalPadding: Style.spacing.controlPaddingX
|
||||
verticalPadding: Style.spacing.controlPaddingY + Style.space(2)
|
||||
bordered: true
|
||||
active: root.activeProfile === modelData
|
||||
hasCursor: root.cursorActive && root.profileIndex === index
|
||||
onClicked: root.setProfile(modelData)
|
||||
onHovered: function(h) {
|
||||
if (h) {
|
||||
root.cursorActive = true
|
||||
root.profileIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,7 +456,6 @@ Panel {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoPair: Row {
|
||||
property string label: ""
|
||||
|
||||
Reference in New Issue
Block a user