feat(bar): unified popup handling (follow-up to #1771 and #1773) (#1776)

This commit is contained in:
end-4
2025-08-11 22:10:52 +07:00
committed by GitHub
15 changed files with 597 additions and 60 deletions
@@ -3,8 +3,9 @@ import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
Item {
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
readonly property var chargeState: Battery.chargeState
@@ -18,6 +19,8 @@ Item {
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
implicitHeight: 32
hoverEnabled: true
RowLayout {
id: rowLayout
@@ -55,9 +58,7 @@ Item {
iconSize: Appearance.font.pixelSize.normal
color: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
}
}
}
Loader {
@@ -69,7 +70,8 @@ Item {
Connections {
target: root
function onIsChargingChanged() {
if (isCharging) boltIconLoader.active = true
if (isCharging)
boltIconLoader.active = true;
}
}
@@ -82,14 +84,18 @@ Item {
visible: opacity > 0 // Only show when charging
opacity: isCharging ? 1 : 0 // Keep opacity for visibility
onVisibleChanged: {
if (!visible) boltIconLoader.active = false
if (!visible)
boltIconLoader.active = false;
}
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
BatteryPopup {
id: batteryPopup
hoverTarget: root
}
}
@@ -0,0 +1,132 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Header
RowLayout {
id: header
spacing: 5
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: "battery_android_full"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: "Battery"
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
// This row is hidden when the battery is full.
RowLayout {
spacing: 5
Layout.fillWidth: true
property bool rowVisible: {
let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty;
let power = Battery.energyRate;
return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01);
}
visible: rowVisible
opacity: rowVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
MaterialSymbol {
text: "schedule"
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
function formatTime(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (h > 0)
return `${h}h, ${m}m`;
else
return `${m}m`;
}
if (Battery.isCharging)
return formatTime(Battery.timeToFull);
else
return formatTime(Battery.timeToEmpty);
}
}
}
RowLayout {
spacing: 5
Layout.fillWidth: true
property bool rowVisible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
visible: rowVisible
opacity: rowVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
MaterialSymbol {
text: "bolt"
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: {
if (Battery.chargeState == 4) {
return Translation.tr("Fully charged");
} else if (Battery.chargeState == 1) {
return Translation.tr("Charging:");
} else {
return Translation.tr("Discharging:");
}
}
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: {
if (Battery.chargeState == 4) {
return "";
} else {
return `${Battery.energyRate.toFixed(2)}W`;
}
}
}
}
}
}
@@ -1,8 +1,10 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
Item {
id: root
@@ -35,7 +37,16 @@ Item {
color: Appearance.colors.colOnLayer1
text: DateTime.date
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
ClockWidgetTooltip {
hoverTarget: mouseArea
}
}
}
@@ -0,0 +1,109 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
property string formattedTime: DateTime.time
property string formattedUptime: DateTime.uptime
property string todosSection: getUpcomingTodos()
function getUpcomingTodos() {
const unfinishedTodos = Todo.list.filter(function (item) {
return !item.done;
});
if (unfinishedTodos.length === 0) {
return Translation.tr("No pending tasks");
}
// Limit to first 5 todos to keep popup manageable
const limitedTodos = unfinishedTodos.slice(0, 5);
let todoText = limitedTodos.map(function (item, index) {
return `${index + 1}. ${item.content}`;
}).join('\n');
if (unfinishedTodos.length > 5) {
todoText += `\n${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
}
return todoText;
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Date + Time row
RowLayout {
spacing: 5
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: "calendar_month"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
horizontalAlignment: Text.AlignLeft
color: Appearance.colors.colOnSurfaceVariant
text: `${root.formattedDate} ${root.formattedTime}`
font.weight: Font.Medium
}
}
// Uptime row
RowLayout {
spacing: 5
Layout.fillWidth: true
MaterialSymbol {
text: "timelapse"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
text: Translation.tr("System uptime:")
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
color: Appearance.colors.colOnSurfaceVariant
text: root.formattedUptime
}
}
// Tasks
ColumnLayout {
spacing: 0
Layout.fillWidth: true
RowLayout {
spacing: 4
Layout.fillWidth: true
MaterialSymbol {
text: "checklist"
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
text: Translation.tr("To Do:")
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
color: Appearance.colors.colOnSurfaceVariant
text: root.todosSection
}
}
}
}
+89 -6
View File
@@ -1,20 +1,38 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
Item {
id: root
required property string iconName
required property double percentage
property var tooltipData: [
{
icon: "info",
label: "System resource",
value: ""
}
]
property var tooltipHeaderIcon
property var tooltipHeaderText
property bool shown: true
clip: true
visible: width > 0 && height > 0
implicitWidth: resourceRowLayout.x < 0 ? 0 : childrenRect.width
implicitHeight: childrenRect.height
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
implicitHeight: resourceRowLayout.implicitHeight
// Helper function to format KB to GB
function formatKB(kb) {
return (kb / (1024 * 1024)).toFixed(1) + " GB";
}
RowLayout {
spacing: 4
id: resourceRowLayout
spacing: 4
x: shown ? 0 : -resourceRowLayout.width
CircularProgress {
@@ -30,10 +48,9 @@ Item {
anchors.centerIn: parent
fill: 1
text: iconName
iconSize: Appearance.font.pixelSize.normal
iconSize: Appearance.font.pixelSize.large
color: Appearance.m3colors.m3onSecondaryContainer
}
}
StyledText {
@@ -45,7 +62,73 @@ Item {
Behavior on x {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: resourceRowLayout.x >= 0 && root.width > 0 && root.visible
}
StyledPopup {
hoverTarget: mouseArea
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Header
RowLayout {
id: header
spacing: 5
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: root.tooltipHeaderIcon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: root.tooltipHeaderText
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
// Info rows
Repeater {
model: root.tooltipData
delegate: RowLayout {
spacing: 5
Layout.fillWidth: true
MaterialSymbol {
text: modelData.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: modelData.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: modelData.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: modelData.value
}
}
}
}
}
Behavior on implicitWidth {
@@ -55,4 +138,4 @@ Item {
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}
}
@@ -0,0 +1,65 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
hoverTarget: mouseArea
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Header
RowLayout {
id: header
spacing: 5
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: root.tooltipHeaderIcon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: root.tooltipHeaderText
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
// Info rows
Repeater {
model: root.tooltipData
delegate: RowLayout {
spacing: 5
Layout.fillWidth: true
MaterialSymbol {
text: modelData.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: modelData.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: modelData.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: modelData.value
}
}
}
}
}
@@ -1,6 +1,7 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs
import QtQuick
import QtQuick.Layouts
@@ -22,6 +23,14 @@ Item {
Resource {
iconName: "memory"
percentage: ResourceUsage.memoryUsedPercentage
tooltipHeaderIcon: "memory"
tooltipHeaderText: Translation.tr("Memory usage")
tooltipData: [
{ icon: "clock_loader_60", label: Translation.tr("Used:"), value: formatKB(ResourceUsage.memoryUsed) },
{ icon: "check_circle", label: Translation.tr("Free:"), value: formatKB(ResourceUsage.memoryFree) },
{ icon: "empty_dashboard", label: Translation.tr("Total:"), value: formatKB(ResourceUsage.memoryTotal) },
]
}
Resource {
@@ -31,6 +40,16 @@ Item {
(MprisController.activePlayer?.trackTitle == null) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 4 : 0
tooltipHeaderIcon: "swap_horiz"
tooltipHeaderText: Translation.tr("Swap usage")
tooltipData: ResourceUsage.swapTotal > 0 ? [
{ icon: "clock_loader_60", label: Translation.tr("Used:"), value: formatKB(ResourceUsage.swapUsed) },
{ icon: "check_circle", label: Translation.tr("Free:"), value: formatKB(ResourceUsage.swapFree) },
{ icon: "empty_dashboard", label: Translation.tr("Total:"), value: formatKB(ResourceUsage.swapTotal) },
] : [
{ icon: "swap_horiz", label: Translation.tr("Swap:"), value: Translation.tr("Not configured") }
]
}
Resource {
@@ -40,6 +59,16 @@ Item {
!(MprisController.activePlayer?.trackTitle?.length > 0) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 4 : 0
tooltipHeaderIcon: "settings_slow_motion"
tooltipHeaderText: Translation.tr("CPU usage")
tooltipData: [
{ icon: "bolt", label: Translation.tr("Load:"), value: (ResourceUsage.cpuUsage > 0.8 ?
Translation.tr("High") :
ResourceUsage.cpuUsage > 0.4 ? Translation.tr("Medium") : Translation.tr("Low"))
+ ` (${Math.round(ResourceUsage.cpuUsage * 100)}%)`
}
]
}
}
@@ -18,10 +18,10 @@ MouseArea {
RowLayout {
id: rowLayout
anchors.centerIn: parent
MaterialSymbol {
fill: 0
text: WeatherIcons.codeToName[Weather.data?.wCode] ?? "question_mark"
text: WeatherIcons.codeToName[Weather.data.wCode] ?? "cloud"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter
@@ -36,25 +36,8 @@ MouseArea {
}
}
LazyLoader {
id: popupLoader
active: root.containsMouse
component: PopupWindow {
id: popupWindow
visible: true
implicitWidth: weatherPopup.implicitWidth
implicitHeight: weatherPopup.implicitHeight
anchor.item: root
anchor.edges: Edges.Top
anchor.rect.x: (root.implicitWidth - popupWindow.implicitWidth) / 2
anchor.rect.y: Config.options.bar.bottom ?
(-weatherPopup.implicitHeight - 15) :
(root.implicitHeight + 15 )
color: "transparent"
WeatherPopup {
id: weatherPopup
}
}
WeatherPopup {
id: weatherPopup
hoverTarget: root
}
}
@@ -7,9 +7,9 @@ import qs.modules.common.widgets
Rectangle {
id: root
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
implicitWidth: columnLayout.implicitWidth * 2
implicitHeight: columnLayout.implicitHeight * 2
color: Appearance.colors.colSurfaceContainerHigh
implicitWidth: columnLayout.implicitWidth + 14 * 2
implicitHeight: columnLayout.implicitHeight + 14 * 2
Layout.fillWidth: parent
property alias title: title.text
@@ -26,18 +26,19 @@ Rectangle {
id: symbol
fill: 0
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
id: title
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnLayer1
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
id: value
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnSurfaceVariant
}
}
}
@@ -6,41 +6,37 @@ import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Rectangle {
StyledPopup {
id: root
readonly property real margin: 10
implicitWidth: columnLayout.implicitWidth + margin * 2
implicitHeight: columnLayout.implicitHeight + margin * 2
color: Appearance.colors.colLayer0
radius: Appearance.rounding.small
border.width: 1
border.color: Appearance.colors.colLayer0Border
clip: true
ColumnLayout {
id: columnLayout
spacing: 5
anchors.centerIn: root
anchors.centerIn: parent
implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth)
implicitHeight: gridLayout.implicitHeight
spacing: 5
// Header
RowLayout {
id: header
spacing: 5
Layout.fillWidth: parent
Layout.alignment: Qt.AlignHCenter
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: "location_on"
iconSize: Appearance.font.pixelSize.huge
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: Weather.data.city
font.pixelSize: Appearance.font.pixelSize.title
font.family: Appearance.font.family.title
color: Appearance.colors.colOnLayer0
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
@@ -94,4 +90,4 @@ Rectangle {
}
}
}
}
}
@@ -162,6 +162,8 @@ Singleton {
property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency)
property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
property color colOnSurface: m3colors.m3onSurface
property color colOnSurfaceVariant: m3colors.m3onSurfaceVariant
property color colTooltip: m3colors.m3inverseSurface
property color colOnTooltip: m3colors.m3inverseOnSurface
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
@@ -0,0 +1,66 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
LazyLoader {
id: root
property MouseArea hoverTarget
default property Item contentItem
active: hoverTarget && hoverTarget.containsMouse
component: PanelWindow {
id: popupWindow
visible: true
color: "transparent"
exclusiveZone: 0
anchors.left: true
anchors.top: !Config.options.bar.bottom
anchors.bottom: Config.options.bar.bottom
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.hyprlandGapsOut * 2
implicitHeight: popupBackground.implicitHeight + Appearance.sizes.hyprlandGapsOut * 2
margins {
left: root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x
}
WlrLayershell.namespace: "quickshell:popup"
WlrLayershell.layer: WlrLayer.Overlay
RectangularShadow {
property var target: popupBackground
anchors.fill: target
radius: target.radius
blur: 0.9 * Appearance.sizes.hyprlandGapsOut
offset: Qt.vector2d(0.0, 1.0)
spread: 0.7
color: Appearance.colors.colShadow
cached: true
}
Rectangle {
id: popupBackground
readonly property real margin: 10
anchors.centerIn: parent
implicitWidth: root.contentItem.implicitWidth + margin * 2
implicitHeight: root.contentItem.implicitHeight + margin * 2
color: Appearance.colors.colSurfaceContainer
radius: Appearance.rounding.small
children: [root.contentItem]
border.width: 1
border.color: Appearance.colors.colLayer0Border
}
}
}
@@ -4,6 +4,8 @@ import qs
import qs.modules.common
import Quickshell
import Quickshell.Services.UPower
import QtQuick
import Quickshell.Io
Singleton {
property bool available: UPower.displayDevice.isLaptopBattery
@@ -21,6 +23,10 @@ Singleton {
property bool isCriticalAndNotCharging: isCritical && !isCharging
property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging
property real energyRate: UPower.displayDevice.changeRate
property real timeToEmpty: UPower.displayDevice.timeToEmpty
property real timeToFull: UPower.displayDevice.timeToFull
onIsLowAndNotChargingChanged: {
if (available && isLowAndNotCharging) Quickshell.execDetached([
"notify-send",
+25 -1
View File
@@ -310,5 +310,29 @@
"Sunrise": "Sunrise",
"Pressure": "Pressure",
"Visibility": "Visibility",
"Precipitation": "Precipitation"
"Precipitation": "Precipitation",
"Time to full:": "Time to full:",
"Time to empty:": "Time to empty:",
"Fully charged": "Fully charged",
"Charging:": "Charging:",
"Discharging:": "Discharging:",
"Uptime:": "Uptime:",
"Upcoming Tasks:": "Upcoming Tasks:",
"No pending tasks": "No pending tasks",
"... and %1 more": "... and %1 more",
"Memory Usage": "Memory Usage",
"Used:": "Used:",
"Free:": "Free:",
"Total:": "Total:",
"Usage:": "Usage:",
"Swap Usage": "Swap Usage",
"Swap:": "Swap:",
"Not configured": "Not configured",
"CPU Usage": "CPU Usage",
"Current:": "Current:",
"Load:": "Load:",
"High": "High",
"Medium": "Medium",
"Low": "Low",
"System Resource": "System Resource"
}
+25 -1
View File
@@ -302,6 +302,30 @@
"Humidity": "湿度",
"Wind": "风",
"Precipitation": "降水量",
"Time to full:": "距离充满:",
"Time to empty:": "距离耗尽:",
"Fully charged": "已充满电",
"Charging:": "充电功率:",
"Discharging:": "放电功率:",
"Uptime:": "运行时间:",
"Upcoming Tasks:": "待办任务:",
"No pending tasks": "没有待办任务",
"... and %1 more": "... 还有 %1 个",
"Memory Usage": "内存使用情况",
"Used:": "已用:",
"Free:": "可用:",
"Total:": "总计:",
"Usage:": "占比:",
"Swap Usage": "交换区使用情况",
"Swap:": "交换区:",
"Not configured": "未配置",
"CPU Usage": "CPU 使用情况",
"Current:": "当前占比:",
"Load:": "负载:",
"High": "高",
"Medium": "中",
"Low": "低",
"System Resource": "系统资源",
"Tint icons": "图标着色",
"Performance Profile toggle": "性能配置文件切换",
"**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**说明**:登录 Mistral 账户,在侧边栏中选择 Keys,点击创建新密钥",
@@ -345,4 +369,4 @@
"Invalid tool. Supported tools:\n- %1": "无效工具。支持的工具:\n- %1",
"Keep right sidebar loaded": "保持右侧边栏加载",
"Reject": "拒绝"
}
}