wbar: add tooltip and stuff

This commit is contained in:
end-4
2025-11-12 21:38:30 +01:00
parent 20cae142d7
commit 945c6a0782
21 changed files with 377 additions and 73 deletions
@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
@@ -13,15 +14,24 @@ Item {
property real horizontalPadding: 10
property real verticalPadding: 5
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
property var anchorEdges: Edges.Top
property var anchorGravity: anchorEdges
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
property Item contentItem: StyledToolTipContent {
id: contentItem
anchors.centerIn: parent
text: root.text
shown: false
Component.onCompleted: shown = true
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
Loader {
id: tooltipLoader
anchors.fill: parent
active: internalVisibleCondition
active: root.internalVisibleCondition
sourceComponent: PopupWindow {
visible: true
anchor {
@@ -35,18 +45,10 @@ Item {
}
color: "transparent"
implicitWidth: contentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: contentItem.implicitHeight + root.verticalPadding * 2
implicitWidth: root.contentItem.implicitWidth + root.horizontalPadding * 2
implicitHeight: root.contentItem.implicitHeight + root.verticalPadding * 2
StyledToolTipContent {
id: contentItem
anchors.centerIn: parent
text: root.text
shown: false
Component.onCompleted: shown = true
horizontalPadding: root.horizontalPadding
verticalPadding: root.verticalPadding
}
data: [root.contentItem]
}
}
}
@@ -44,7 +44,7 @@ ColumnLayout {
Layout.fillHeight: false
Layout.fillWidth: true
Layout.bottomMargin: 6
model: root.devices.map(node => node.description)
model: root.devices.map(node => (node.nickname || node.description || Translation.tr("Unknown")))
currentIndex: root.devices.findIndex(item => {
if (root.isSink) {
return item.id === Pipewire.preferredDefaultAudioSink?.id
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import qs.services
import qs.modules.common
@@ -9,7 +10,9 @@ BarButton {
id: root
required property string iconName
property bool multiple: false
property bool separateLightDark: false
property alias tryCustomIcon: iconWidget.tryCustomIcon
leftInset: 2
rightInset: 2
implicitWidth: height - topInset - bottomInset + leftInset + rightInset
@@ -20,9 +23,37 @@ BarButton {
contentItem.scale = root.down ? 5/6 : 1 // If/When we do dragging, the scale is 1.25
}
background: Item {
BackgroundAcrylicRectangle {
id: mainBgRect
anchors.fill: parent
layer.enabled: root.multiple
layer.effect: OpacityMask {
invert: true
maskSource: Item {
width: mainBgRect.width
height: mainBgRect.height
Rectangle {
anchors.fill: parent
anchors.rightMargin: 3
radius: mainBgRect.radius
}
}
}
}
Loader {
anchors.fill: parent
anchors.rightMargin: 5
active: root.multiple
sourceComponent: BackgroundAcrylicRectangle {
}
}
}
contentItem: Item {
id: contentItem
anchors.centerIn: root.background
anchors.centerIn: parent
implicitHeight: iconWidget.implicitHeight
implicitWidth: iconWidget.implicitWidth
@@ -41,4 +72,15 @@ BarButton {
separateLightDark: root.separateLightDark
}
}
component BackgroundAcrylicRectangle: AcrylicRectangle {
shiny: ((root.hovered && !root.down) || root.checked)
color: root.colBackground
border.width: 1
border.color: root.colBackgroundBorder
Behavior on border.color {
animation: Looks.transition.color.createObject(this)
}
}
}
@@ -8,11 +8,12 @@ Kirigami.Icon {
id: root
required property string iconName
property bool separateLightDark: false
property bool tryCustomIcon: true
property real implicitSize: 26
implicitWidth: implicitSize
implicitHeight: implicitSize
roundToIconSize: false
source: `${Looks.iconsPath}/${root.iconName}${!root.separateLightDark ? "" : Looks.dark ? "-dark" : "-light"}.svg`
fallback: root.iconName
source: tryCustomIcon ? `${Looks.iconsPath}/${root.iconName}${!root.separateLightDark ? "" : Looks.dark ? "-dark" : "-light"}.svg` : fallback
}
@@ -8,20 +8,70 @@ import qs.modules.waffle.looks
Button {
id: root
signal altAction()
signal middleClickAction()
property color colBackground
property color colBackgroundBorder
Layout.fillHeight: true
topInset: 4
bottomInset: 4
signal hoverTimedOut()
property bool shouldShowTooltip: false
property Timer hoverTimer: Timer {
id: hoverTimer
running: root.hovered
interval: 400
onTriggered: {
root.hoverTimedOut()
}
}
onHoverTimedOut: {
root.shouldShowTooltip = true
}
onHoveredChanged: {
if (!root.hovered) {
root.shouldShowTooltip = false
root.hoverTimer.stop()
}
}
colBackground: {
if (root.down) {
return Looks.colors.bg1Active
} else if ((root.hovered && !root.down) || root.checked) {
return Looks.colors.bg1Hover
} else {
return ColorUtils.transparentize(Looks.colors.bg1)
}
}
colBackgroundBorder: ColorUtils.transparentize(Looks.colors.bg1Border, root.checked ? Looks.contentTransparency : 1)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => {
root.down = true;
}
onReleased: (event) => {
root.down = false;
}
onClicked: (event) => {
if (event.button === Qt.LeftButton) root.clicked();
if (event.button === Qt.RightButton) root.altAction();
if (event.button === Qt.MiddleButton) root.middleClickAction();
}
}
background: AcrylicRectangle {
shiny: ((root.hovered && !root.down) || root.checked)
color: {
if (root.down) {
return Looks.colors.bg1Active
} else if ((root.hovered && !root.down) || root.checked) {
return Looks.colors.bg1Hover
} else {
return ColorUtils.transparentize(Looks.colors.bg1)
}
color: root.colBackground
border.width: 1
border.color: root.colBackgroundBorder
Behavior on border.color {
animation: Looks.transition.color.createObject(this)
}
}
}
@@ -0,0 +1,8 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.waffle.looks
WPopupToolTip {
anchorEdges: Config.options.waffles.bar.bottom ? Edges.Top : Edges.Bottom
}
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
@@ -10,4 +11,14 @@ AppButton {
iconName: "system-search"
separateLightDark: true
onClicked: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen; // For now...
}
BarToolTip {
id: tooltip
text: Translation.tr("Search")
extraVisibleCondition: root.shouldShowTooltip
}
}
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
@@ -10,4 +11,14 @@ AppButton {
leftInset: Config.options.waffles.bar.leftAlignApps ? 12 : 0
iconName: "start-here"
onClicked: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen; // For now...
}
BarToolTip {
id: tooltip
text: Translation.tr("Start")
extraVisibleCondition: root.shouldShowTooltip
}
}
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Layouts
import qs
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
@@ -7,35 +8,88 @@ import qs.modules.waffle.looks
BarButton {
id: root
// padding: 12
checked: GlobalStates.sidebarRightOpen
onClicked: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen; // For now...
}
contentItem: Item {
anchors.centerIn: root.background
anchors.fill: parent
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Row {
id: column
anchors.centerIn: parent
spacing: 4
FluentIcon {
icon: WIcons.internetIcon
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
FluentIcon {
icon: {
const muted = Audio.sink?.audio.muted ?? false;
const volume = Audio.sink?.audio.volume ?? 0;
if (muted) return volume > 0 ? "speaker-off" : "speaker-none";
if (volume == 0) return "speaker-none";
if (volume < 0.5) return "speaker-1";
return "speaker";
spacing: 4
IconHoverArea {
id: internetHoverArea
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: WIcons.internetIcon
}
}
FluentIcon {
icon: WIcons.batteryIcon
IconHoverArea {
id: volumeHoverArea
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: {
const muted = Audio.sink?.audio.muted ?? false;
const volume = Audio.sink?.audio.volume ?? 0;
if (muted)
return volume > 0 ? "speaker-off" : "speaker-none";
if (volume == 0)
return "speaker-none";
if (volume < 0.5)
return "speaker-1";
return "speaker";
}
}
}
IconHoverArea {
id: batteryHoverArea
iconItem: FluentIcon {
anchors.verticalCenter: parent.verticalCenter
icon: WIcons.batteryIcon
}
}
}
}
component IconHoverArea: MouseArea {
id: hoverArea
required property var iconItem
anchors {
top: parent.top
bottom: parent.bottom
}
hoverEnabled: true
implicitHeight: hoverArea.iconItem.implicitHeight
implicitWidth: hoverArea.iconItem.implicitWidth
onPressed: (event) => event.accepted = false; // Don't consume clicks
children: [iconItem]
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && internetHoverArea.containsMouse
text: Translation.tr("%1\nInternet access").arg(Network.ethernet ? Translation.tr("Network") : Network.networkName)
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && volumeHoverArea.containsMouse
text: Translation.tr("Speakers (%1): %2") //
.arg(Audio.sink?.nickname || Audio.sink?.description || Translation.tr("Unknown")) //
.arg(`${Math.round(Audio.sink?.audio.volume * 100) || 0}%`) //
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && batteryHoverArea.containsMouse
text: Translation.tr("Battery: %1").arg(`${Math.round(Battery.percentage * 100) || 0}%`)
}
}
@@ -1,25 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import Quickshell
AppButton {
id: root
required property var appEntry
readonly property bool isSeparator: appEntry.appId === "SEPARATOR"
readonly property var desktopEntry: DesktopEntries.heuristicLookup(appEntry.appId)
signal hoverPreviewRequested()
iconName: AppSearch.guessIcon(appEntry.appId)
Timer {
running: root.hovered
interval: 250
onTriggered: {
root.hoverPreviewRequested()
}
}
}
@@ -16,4 +16,9 @@ AppButton {
onClicked: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip
text: Translation.tr("Task View") // Should be a preview of workspaces, but we'll have this for now...
}
}
@@ -33,4 +33,10 @@ BarButton {
}
}
}
BarToolTip {
id: tooltip
extraVisibleCondition: root.shouldShowTooltip
text: `${Qt.locale().toString(DateTime.clock.date, "dddd, MMMM d, yyyy")}\n\n${Qt.locale().toString(DateTime.clock.date, "ddd hh:mm AP")}`
}
}
@@ -3,6 +3,7 @@ import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.waffle.looks
import qs.modules.waffle.bar.tasks
Rectangle {
id: root
@@ -52,4 +52,9 @@ AppButton {
}
}
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip
text: Translation.tr("Widgets")
}
}
@@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.waffle.looks
import qs.modules.waffle.bar
import Quickshell
AppButton {
id: root
required property var appEntry
readonly property bool isSeparator: appEntry.appId === "SEPARATOR"
readonly property var desktopEntry: DesktopEntries.heuristicLookup(appEntry.appId)
property bool active: root.appEntry.toplevels.some(t => t.activated)
property bool hasWindows: appEntry.toplevels.length > 0
signal hoverPreviewRequested()
multiple: appEntry.toplevels.length > 1
checked: active
iconName: AppSearch.guessIcon(appEntry.appId)
tryCustomIcon: false
onHoverTimedOut: {
root.hoverPreviewRequested()
}
onClicked: {
root.hoverTimer.stop() // Prevents preview showing up when clicking to focus
if (root.multiple) {
root.hoverPreviewRequested()
} else if (root.appEntry.toplevels.length === 1) {
root.appEntry.toplevels[0].activate()
} else {
root.desktopEntry.execute()
}
}
// Active indicator
Rectangle {
id: activeIndicator
opacity: root.hasWindows ? 1 : 0
anchors {
horizontalCenter: root.background.horizontalCenter
bottom: root.background.bottom
bottomMargin: 1
}
implicitWidth: root.active ? 16 : 6
implicitHeight: 3
radius: height / 2
color: root.active ? Looks.colors.accent : Looks.colors.accentUnfocused
Behavior on implicitWidth {
animation: Looks.transition.enter.createObject(this)
}
Behavior on color {
animation: Looks.transition.color.createObject(this)
}
Behavior on opacity {
animation: Looks.transition.opacity.createObject(this)
}
}
BarToolTip {
extraVisibleCondition: root.shouldShowTooltip && !root.hasWindows
text: desktopEntry ? desktopEntry.name : appEntry.appId
}
}
@@ -70,7 +70,7 @@ PopupWindow {
fill: contentItem
margins: -border.width
}
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.contentTransparency)
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.shadowTransparency)
border.width: 1
color: "transparent"
radius: Looks.radius.large + border.width
@@ -13,6 +13,10 @@ MouseArea {
implicitWidth: row.implicitWidth
hoverEnabled: true
function showPreviewPopup(appEntry, button) {
previewPopup.show(appEntry, button);
}
// Apps row
RowLayout {
id: row
@@ -20,7 +24,7 @@ MouseArea {
spacing: 0
Repeater {
// TODO: Include only apps (and windows) in current workspace only
// TODO: Include only apps (and windows) in current workspace only | wait, does that even make sense in a Hyprland workflow?
model: ScriptModel {
objectProp: "appId"
values: TaskbarApps.apps.filter(app => app.appId !== "SEPARATOR")
@@ -30,7 +34,7 @@ MouseArea {
appEntry: modelData
onHoverPreviewRequested: {
previewPopup.show(appEntry, this)
root.showPreviewPopup(appEntry, this)
}
}
}
@@ -6,6 +6,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks
import qs.modules.waffle.bar
import Quickshell
import Quickshell.Wayland
@@ -17,6 +17,7 @@ Singleton {
property real backgroundTransparency: 0.17
property real contentTransparency: 0.25
property real shadowTransparency: 0.6
colors: QtObject {
id: colors
property color bg0: root.dark ? "#1C1C1C" : "#EEEEEE"
@@ -33,7 +34,9 @@ Singleton {
property color fg1: root.dark ? "#D1D1D1" : "#626262"
property color danger: "#C42B1C"
property color dangerActive: "#B62D1F"
property color brand: Appearance.m3colors.m3primary
// property color accent: root.dark ? "#A5C6D8" : "#5377A3"
property color accent: Appearance.m3colors.m3primary
property color accentUnfocused: root.dark ? "#989898" : "#848484"
}
radius: QtObject {
@@ -106,7 +109,7 @@ Singleton {
property Component move: Component {
NumberAnimation {
duration: 100
duration: 170
easing.type: Easing.BezierSpline
easing.bezierCurve: transition.easing.bezierCurve.easeInOut
}
@@ -0,0 +1,51 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.waffle.looks
PopupToolTip {
id: root
property real padding: 2
verticalPadding: padding
horizontalPadding: padding
contentItem: Item {
anchors.centerIn: parent
implicitWidth: realContent.implicitWidth + root.verticalPadding * 2
implicitHeight: realContent.implicitHeight + root.horizontalPadding * 2
Rectangle {
id: ambientShadow
z: 0
anchors {
fill: realContent
margins: -border.width
}
border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.shadowTransparency)
border.width: 1
color: "transparent"
radius: realContent.radius + border.width
}
Rectangle {
id: realContent
z: 1
anchors.centerIn: parent
implicitWidth: tooltipText.implicitWidth + 10 * 2
implicitHeight: tooltipText.implicitHeight + 8 * 2
color: Looks.colors.bg1
radius: Looks.radius.medium
WText {
id: tooltipText
text: root.text
anchors.centerIn: parent
}
}
}
}
@@ -94,7 +94,7 @@ Singleton {
if (!str || str.length == 0) return "image-missing";
// Quickshell's desktop entry lookup
const entry = DesktopEntries.heuristicLookup(str);
const entry = DesktopEntries.byId(str);
if (entry) return entry.icon;
// Normal substitutions
@@ -149,6 +149,9 @@ Singleton {
if (iconExists(guess)) return guess;
}
// Quickshell's desktop entry lookup
const heuristicEntry = DesktopEntries.heuristicLookup(str);
if (heuristicEntry) return heuristicEntry.icon;
// Give up
return "application-x-executable";