Tooltip: Refactoring in a single global tooltip.

This commit is contained in:
ItsLemmy
2025-09-27 21:12:39 -04:00
parent 92460fc5c3
commit 0593543d7a
30 changed files with 532 additions and 382 deletions
+11 -10
View File
@@ -46,6 +46,15 @@ Item {
width: pillHeight + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Connections {
target: root
function onTooltipTextChanged() {
if (PanelService.tooltip.visible) {
PanelService.tooltip.updateText(root.tooltipText)
}
}
}
Rectangle {
id: pill
width: revealed ? pillMaxWidth : 1
@@ -195,14 +204,6 @@ Item {
}
}
NTooltip {
id: tooltip
positionAbove: Settings.data.bar.position === "bottom"
target: pill
delay: Style.tooltipDelayLong
text: root.tooltipText
}
Timer {
id: showTimer
interval: Style.pillDelay
@@ -220,7 +221,7 @@ Item {
onEntered: {
hovered = true
root.entered()
tooltip.show()
PanelService.tooltip.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
if (disableOpen || forceClose) {
return
}
@@ -234,7 +235,7 @@ Item {
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
PanelService.tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
+11 -11
View File
@@ -58,6 +58,15 @@ Item {
width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
Connections {
target: root
function onTooltipTextChanged() {
if (PanelService.tooltip.visible) {
PanelService.tooltip.updateText(root.tooltipText)
}
}
}
Rectangle {
id: pill
width: revealed ? maxPillWidth : 1
@@ -236,15 +245,6 @@ Item {
}
}
NTooltip {
id: tooltip
target: pill
text: root.tooltipText
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: Style.tooltipDelayLong
}
Timer {
id: showTimer
interval: Style.pillDelay
@@ -262,7 +262,7 @@ Item {
onEntered: {
hovered = true
root.entered()
tooltip.show()
PanelService.tooltip.show(pill, root.tooltipText, BarService.getTooltipDirection(), Style.tooltipDelayLong)
if (disableOpen || forceClose) {
return
}
@@ -276,7 +276,7 @@ Item {
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
PanelService.tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
+3 -15
View File
@@ -328,29 +328,17 @@ Item {
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
tooltip.show()
if ((windowTitle !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
PanelService.tooltip.show(root, windowTitle, BarService.getTooltipDirection())
}
}
onExited: {
tooltip.hide()
PanelService.tooltip.hide()
}
}
}
}
NTooltip {
id: tooltip
text: windowTitle
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : windowActiveRect
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelay
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
+2 -2
View File
@@ -19,9 +19,9 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
tooltipText: I18n.tr("tooltips.bluetooth-devices")
tooltipDirection: BarService.getTooltipDirection()
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}
+3 -11
View File
@@ -106,14 +106,6 @@ Rectangle {
}
}
}
NTooltip {
id: tooltip
text: I18n.tr("clock.tooltip")
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
}
MouseArea {
id: clockMouseArea
@@ -122,14 +114,14 @@ Rectangle {
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show()
PanelService.tooltip.show(root, I18n.tr("clock.tooltip"), BarService.getTooltipDirection())
}
}
onExited: {
tooltip.hide()
PanelService.tooltip.hide()
}
onClicked: {
tooltip.hide()
PanelService.tooltip.hide()
PanelService.getPanel("calendarPanel")?.toggle(this)
}
}
+1 -3
View File
@@ -36,9 +36,7 @@ NIconButton {
// If we have a custom path or distro logo, don't use the theme icon.
icon: (customIconPath === "" && !useDistroLogo) ? customIcon : ""
tooltipText: I18n.tr("tooltips.open-control-center")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
+1 -3
View File
@@ -11,9 +11,7 @@ NIconButton {
icon: "dark-mode"
tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
+1 -3
View File
@@ -15,9 +15,7 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.disable-keep-awake") : I18n.tr("tooltips.enable-keep-awake")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
+18 -28
View File
@@ -42,6 +42,21 @@ Item {
// Fixed width - no expansion
readonly property real widgetWidth: Math.max(1, screen.width * 0.06)
readonly property string tooltipText: {
var title = getTitle()
var controls = ""
if (MediaService.canGoNext) {
controls += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
controls += "Middle click for previous."
}
if (controls !== "") {
return title + "\n\n" + controls
}
return title
}
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (widgetWidth * scaling)) : 0
@@ -360,39 +375,14 @@ Item {
}
onEntered: {
if ((tooltip.text !== "") && (barPosition === "left" || barPosition === "right")) {
tooltip.show()
} else if ((tooltip.text !== "") && (scrollingMode === "never")) {
tooltip.show()
if ((tooltipText !== "") && (barPosition === "left" || barPosition === "right") || (scrollingMode === "never")) {
PanelService.tooltip.show(root, tooltipText, BarService.getTooltipDirection())
}
}
onExited: {
tooltip.hide()
PanelService.tooltip.hide()
}
}
}
}
NTooltip {
id: tooltip
text: {
var title = getTitle()
var controls = ""
if (MediaService.canGoNext) {
controls += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
controls += "Middle click for previous."
}
if (controls !== "") {
return title + "\n\n" + controls
}
return title
}
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : mediaMini
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelay
}
}
+1 -3
View File
@@ -23,9 +23,7 @@ NIconButton {
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("tooltips.night-light-forced") : I18n.tr("tooltips.night-light-enabled")) : I18n.tr("tooltips.night-light-disabled")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
onClicked: {
// Check if wlsunset is available before enabling night light
if (!ProgramCheckerService.wlsunsetAvailable) {
+1 -3
View File
@@ -53,9 +53,7 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? I18n.tr("tooltips.open-notification-history-disable-dnd") : I18n.tr("tooltips.open-notification-history-enable-dnd")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
+1 -3
View File
@@ -19,9 +19,7 @@ NIconButton {
tooltipText: I18n.tr("tooltips.power-profile", {
"profile": PowerProfileService.getName()
})
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
compact: (Settings.data.bar.density === "compact")
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
+1 -3
View File
@@ -12,9 +12,7 @@ NIconButton {
icon: "camera-video"
tooltipText: ScreenRecorderService.isRecording ? I18n.tr("tooltips.click-to-stop-recording") : I18n.tr("tooltips.click-to-start-recording")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
+1 -3
View File
@@ -15,9 +15,7 @@ NIconButton {
baseSize: Style.capsuleHeight
icon: "power"
tooltipText: I18n.tr("tooltips.session-menu")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mError
colorBorder: Color.transparent
+2 -11
View File
@@ -98,17 +98,8 @@ Rectangle {
}
}
}
onEntered: taskbarTooltip.show()
onExited: taskbarTooltip.hide()
}
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
positionLeft: Settings.data.bar.position === "right"
positionRight: Settings.data.bar.position === "left"
onEntered: PanelService.tooltip.show(taskbarItem, taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown app.", BarService.getTooltipDirection())
onExited: PanelService.tooltip.hide()
}
}
}
+2 -9
View File
@@ -135,15 +135,8 @@ Rectangle {
}
}
}
onEntered: trayTooltip.show()
onExited: trayTooltip.hide()
}
NTooltip {
id: trayTooltip
target: trayIcon
text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item"
positionAbove: Settings.data.bar.position === "bottom"
onEntered: PanelService.tooltip.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection())
onExited: PanelService.tooltip.hide()
}
}
}
+1 -3
View File
@@ -15,9 +15,7 @@ NIconButton {
compact: (Settings.data.bar.density === "compact")
icon: "wallpaper-selector"
tooltipText: I18n.tr("tooltips.open-wallpaper-selector")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
tooltipDirection: BarService.getTooltipDirection()
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
+2 -5
View File
@@ -19,7 +19,8 @@ NIconButton {
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
tooltipText: I18n.tr("tooltips.manage-wifi")
tooltipDirection: BarService.getTooltipDirection()
icon: {
try {
if (NetworkService.ethernetConnected) {
@@ -40,10 +41,6 @@ NIconButton {
return "signal_wifi_bad"
}
}
tooltipText: I18n.tr("tooltips.manage-wifi")
tooltipPositionAbove: Settings.data.bar.position === "bottom"
tooltipPositionLeft: Settings.data.bar.position === "right"
tooltipPositionRight: Settings.data.bar.position === "left"
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}
+4 -12
View File
@@ -373,14 +373,6 @@ Variants {
}
}
// Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
}
Image {
id: appIcon
width: iconSize
@@ -481,8 +473,8 @@ Variants {
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.isVisible = true
const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
PanelService.tooltip.show(appButton, tooltipText, "top")
if (autoHide) {
showTimer.stop()
hideTimer.stop()
@@ -492,7 +484,7 @@ Variants {
onExited: {
anyAppHovered = false
appTooltip.hide()
PanelService.tooltip.hide()
if (autoHide && !dockHovered && !peekHovered && !menuHovered) {
hideTimer.restart()
}
@@ -508,7 +500,7 @@ Variants {
// Close any other existing context menu first
root.closeAllContextMenus()
// Hide tooltip when showing context menu
appTooltip.hide()
PanelService.tooltip.hide()
contextMenu.show(appButton, modelData.toplevel || modelData)
return
}
+9 -9
View File
@@ -410,10 +410,10 @@ Variants {
// Make visible and animate in
osdItem.visible = true
// Use Qt.callLater to ensure the visible change is processed before animation
Qt.callLater(function () {
osdItem.opacity = 1
osdItem.scale = 1.0
})
Qt.callLater(() => {
osdItem.opacity = 1
osdItem.scale = 1.0
})
// Start the auto-hide timer
hideTimer.start()
@@ -524,11 +524,11 @@ Variants {
root.item.showOSD()
} else {
// If item not ready yet, wait for it
Qt.callLater(function () {
if (root.item) {
root.item.showOSD()
}
})
Qt.callLater(() => {
if (root.item) {
root.item.showOSD()
}
})
}
}
@@ -69,6 +69,7 @@ Popup {
NIconButton {
icon: "close"
tooltipText: "Close"
onClicked: widgetSettings.close()
}
}
+5 -5
View File
@@ -93,11 +93,11 @@ Item {
// Activate the loader and show toast
windowLoader.active = true
// Need a small delay to ensure the window is created
Qt.callLater(function () {
if (windowLoader.item) {
windowLoader.item.showToast(data.message, data.description, data.type, data.duration)
}
})
Qt.callLater(() => {
if (windowLoader.item) {
windowLoader.item.showToast(data.message, data.description, data.type, data.duration)
}
})
}
function onToastHidden() {
+414
View File
@@ -0,0 +1,414 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
PopupWindow {
id: root
property string text: ""
property string direction: "auto" // "auto", "left", "right", "top", "bottom"
property int margin: Style.marginXS // distance from target
property int padding: Style.marginM
property int delay: 0
property int hideDelay: 0
property int maxWidth: 340
property real scaling: 1.0
property int animationDuration: Style.animationFast
property real animationScale: 0.85
// Internal properties
property var targetItem: null
property real anchorX: 0
property real anchorY: 0
property bool isPositioned: false
property bool pendingShow: false
property bool animatingOut: false
visible: false
color: Color.transparent
anchor.item: targetItem
anchor.rect.x: anchorX
anchor.rect.y: anchorY
implicitWidth: Math.min(tooltipText.implicitWidth + padding * 2 * scaling, maxWidth * scaling)
implicitHeight: tooltipText.implicitHeight + padding * 2 * scaling
// Timer for showing tooltip after delay
Timer {
id: showTimer
interval: root.delay
repeat: false
onTriggered: {
root.positionAndShow()
}
}
// Timer for hiding tooltip after delay
Timer {
id: hideTimer
interval: root.hideDelay
repeat: false
onTriggered: {
root.startHideAnimation()
}
}
// Show animation
ParallelAnimation {
id: showAnimation
PropertyAnimation {
target: tooltipContainer
property: "opacity"
from: 0.0
to: 1.0
duration: root.animationDuration
easing.type: Easing.OutCubic
}
PropertyAnimation {
target: tooltipContainer
property: "scale"
from: root.animationScale
to: 1.0
duration: root.animationDuration
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
// Hide animation
ParallelAnimation {
id: hideAnimation
PropertyAnimation {
target: tooltipContainer
property: "opacity"
from: 1.0
to: 0.0
duration: root.animationDuration * 0.75 // Slightly faster hide
easing.type: Easing.InCubic
}
PropertyAnimation {
target: tooltipContainer
property: "scale"
from: 1.0
to: root.animationScale
duration: root.animationDuration * 0.75
easing.type: Easing.InCubic
}
onFinished: {
root.completeHide()
}
}
// Function to show tooltip
function show(target, tipText, customDirection, showDelay) {
if (!target || !tipText || tipText === "")
return
if (showDelay !== undefined) {
delay = showDelay
} else {
delay = Style.tooltipDelay
}
// Stop any running timers and animations
hideTimer.stop()
showTimer.stop()
hideAnimation.stop()
animatingOut = false
// If we're already showing for a different target, hide immediately
if (visible && targetItem !== target) {
hideImmediately()
}
// Set properties
targetItem = target
text = tipText
pendingShow = true
if (customDirection !== undefined) {
direction = customDirection
} else {
direction = "auto"
}
// Start show timer
showTimer.start()
}
// Function to position and display the tooltip
function positionAndShow() {
if (!targetItem || !pendingShow)
return
// Get screen dimensions - try multiple methods
var screenWidth = Screen.width
var screenHeight = Screen.height
// Try to get screen from target item
if (targetItem) {
if (targetItem.screen) {
screenWidth = targetItem.screen.width
screenHeight = targetItem.screen.height
scaling = ScalingService.getScreenScale(targetItem.screen)
}
}
// Calculate tooltip dimensions
const tipWidth = Math.min(tooltipText.implicitWidth + padding * 2 * scaling, maxWidth * scaling)
const tipHeight = tooltipText.implicitHeight + padding * 2 * scaling
// Get target's global position
const targetGlobal = targetItem.mapToGlobal(0, 0)
const targetWidth = targetItem.width
const targetHeight = targetItem.height
var newAnchorX = 0
var newAnchorY = 0
if (direction === "auto") {
// Calculate available space in each direction
const spaceLeft = targetGlobal.x
const spaceRight = screenWidth - (targetGlobal.x + targetWidth)
const spaceTop = targetGlobal.y
const spaceBottom = screenHeight - (targetGlobal.y + targetHeight)
// Try positions in order of available space
const positions = [{
"dir": "bottom",
"space": spaceBottom,
"x": (targetWidth - tipWidth) / 2,
"y": targetHeight + margin * scaling,
"fits": spaceBottom >= tipHeight + margin * scaling
}, {
"dir": "top",
"space": spaceTop,
"x": (targetWidth - tipWidth) / 2,
"y": -tipHeight - margin * scaling,
"fits": spaceTop >= tipHeight + margin * scaling
}, {
"dir": "right",
"space": spaceRight,
"x": targetWidth + margin * scaling,
"y": (targetHeight - tipHeight) / 2,
"fits": spaceRight >= tipWidth + margin * scaling
}, {
"dir": "left",
"space": spaceLeft,
"x": -tipWidth - margin * scaling,
"y": (targetHeight - tipHeight) / 2,
"fits": spaceLeft >= tipWidth + margin * scaling
}]
// Find first position that fits
var selectedPosition = null
for (var i = 0; i < positions.length; i++) {
if (positions[i].fits) {
selectedPosition = positions[i]
break
}
}
// If none fit perfectly
if (!selectedPosition) {
// Sort by available space and use position with most space
positions.sort(function (a, b) {
return b.space - a.space
})
selectedPosition = positions[0]
}
newAnchorX = selectedPosition.x
newAnchorY = selectedPosition.y
// Adjust horizontal position to keep tooltip on screen
if (direction === "auto") {
const globalX = targetGlobal.x + newAnchorX
if (globalX < 0) {
newAnchorX = -targetGlobal.x + margin * scaling
} else if (globalX + tipWidth > screenWidth) {
newAnchorX = screenWidth - targetGlobal.x - tipWidth - margin * scaling
}
}
} else {
// Manual direction positioning
switch (direction) {
case "left":
newAnchorX = -tipWidth - margin * scaling
newAnchorY = (targetHeight - tipHeight) / 2
break
case "right":
newAnchorX = targetWidth + margin * scaling
newAnchorY = (targetHeight - tipHeight) / 2
break
case "top":
newAnchorX = (targetWidth - tipWidth) / 2
newAnchorY = -tipHeight - margin * scaling
break
case "bottom":
newAnchorX = (targetWidth - tipWidth) / 2
newAnchorY = targetHeight + margin * scaling
break
}
}
// Apply position
anchorX = newAnchorX
anchorY = newAnchorY
isPositioned = true
pendingShow = false
// Show tooltip and start animation
visible = true
// Initialize animation state
tooltipContainer.opacity = 0.0
tooltipContainer.scale = animationScale
// Start show animation
showAnimation.start()
// Force anchor update after showing
Qt.callLater(() => {
if (root.anchor && root.visible) {
root.anchor.updateAnchor()
}
})
}
// Function to hide tooltip
function hide() {
// Stop show timer if it's running
showTimer.stop()
pendingShow = false
// Stop hide timer if it's running
hideTimer.stop()
if (hideDelay > 0 && visible && !animatingOut) {
hideTimer.start()
} else {
startHideAnimation()
}
}
function startHideAnimation() {
if (!visible || animatingOut)
return
animatingOut = true
showAnimation.stop() // Stop show animation if running
hideAnimation.start()
}
function completeHide() {
// Hide the popup window
visible = false
animatingOut = false
pendingShow = false
// Clear the text and reset state
text = ""
isPositioned = false
// Reset container state
tooltipContainer.opacity = 1.0
tooltipContainer.scale = 1.0
}
// Quick hide without delay or animation
function hideImmediately() {
showTimer.stop()
hideTimer.stop()
showAnimation.stop()
hideAnimation.stop()
pendingShow = false
animatingOut = false
completeHide()
}
// Update text function for binding support
function updateText(newText) {
if (visible && targetItem) {
text = newText
positionAndShow()
}
}
// Reset function to clean up state
function reset() {
// Stop all timers and animations
showTimer.stop()
hideTimer.stop()
showAnimation.stop()
hideAnimation.stop()
// Clear all state
visible = false
pendingShow = false
animatingOut = false
text = ""
isPositioned = false
// Reset to defaults
direction = "auto"
delay = 0
hideDelay = 0
// Reset container state
tooltipContainer.opacity = 1.0
tooltipContainer.scale = 1.0
}
// Tooltip content container for animations
Item {
id: tooltipContainer
anchors.fill: parent
// Animation properties
opacity: 1.0
scale: 1.0
transformOrigin: Item.Center
Rectangle {
anchors.fill: parent
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling
// Only show content when we have text
visible: root.text !== ""
NText {
id: tooltipText
anchors.centerIn: parent
anchors.margins: root.padding * root.scaling
text: root.text
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, root.maxWidth * root.scaling - root.padding * 2 * root.scaling)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
Component.onCompleted: {
reset()
}
Component.onDestruction: {
reset()
}
}
+13
View File
@@ -202,4 +202,17 @@ Singleton {
}
return false
}
function getTooltipDirection() {
switch (Settings.data.bar.position) {
case "right":
return "left"
case "left":
return "right"
case "bottom":
return "top"
default:
return "bottom"
}
}
}
+3
View File
@@ -10,6 +10,9 @@ Singleton {
// This is not a panel...
property var lockScreen: null
// A ref. to our global tooltip
property var tooltip: null
// Panels
property var registeredPanels: ({})
property var openedPanel: null
+5 -10
View File
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
Rectangle {
id: root
@@ -126,12 +127,6 @@ Rectangle {
}
}
NTooltip {
id: tooltip
target: root
text: root.tooltipText
}
// Mouse interaction
MouseArea {
id: mouseArea
@@ -144,18 +139,18 @@ Rectangle {
onEntered: {
root.hovered = true
if (tooltipText) {
tooltip.show()
PanelService.tooltip.show(root, root.tooltipText)
}
}
onExited: {
root.hovered = false
if (tooltipText) {
tooltip.hide()
PanelService.tooltip.hide()
}
}
onPressed: mouse => {
if (tooltipText) {
tooltip.hide()
PanelService.tooltip.hide()
}
if (mouse.button === Qt.LeftButton) {
root.clicked()
@@ -169,7 +164,7 @@ Rectangle {
onCanceled: {
root.hovered = false
if (tooltipText) {
tooltip.hide()
PanelService.tooltip.hide()
}
}
}
+4 -15
View File
@@ -11,9 +11,7 @@ Rectangle {
property string icon
property string tooltipText
property bool tooltipPositionAbove: false
property bool tooltipPositionLeft: false
property bool tooltipPositionRight: false
property string tooltipDirection: "auto"
property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hovering: false
@@ -65,15 +63,6 @@ Rectangle {
}
}
NTooltip {
id: tooltip
target: root
positionAbove: root.tooltipPositionAbove
positionLeft: root.tooltipPositionLeft
positionRight: root.tooltipPositionRight
text: root.tooltipText
}
MouseArea {
// Always enabled to allow hover/tooltip even when the button is disabled
enabled: true
@@ -84,20 +73,20 @@ Rectangle {
onEntered: {
hovering = root.enabled ? true : false
if (tooltipText) {
tooltip.show()
PanelService.tooltip.show(parent, tooltipText, tooltipDirection)
}
root.entered()
}
onExited: {
hovering = false
if (tooltipText) {
tooltip.hide()
PanelService.tooltip.hide()
}
root.exited()
}
onClicked: function (mouse) {
if (tooltipText) {
tooltip.hide()
PanelService.tooltip.hide()
}
if (!root.enabled && !allowClickWhenDisabled) {
return
+5 -5
View File
@@ -255,11 +255,11 @@ RowLayout {
// Ensure the model is filtered when popup opens
filterModel()
// Small delay to ensure the popup is fully rendered
Qt.callLater(function () {
if (searchInput && searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
}
})
Qt.callLater(() => {
if (searchInput && searchInput.inputItem) {
searchInput.inputItem.forceActiveFocus()
}
})
}
}
}
-197
View File
@@ -1,197 +0,0 @@
import QtQuick
import qs.Commons
import qs.Services
Window {
id: root
property bool isVisible: false
property string text: I18n.tr("widgets.tooltip.placeholder")
property Item target: null
property int delay: Style.tooltipDelay
property bool positionAbove: false
property bool positionLeft: false
property bool positionRight: false
readonly property string barPosition: Settings.data.bar.position
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: Color.transparent
visible: false
onIsVisibleChanged: {
if (isVisible) {
if (delay > 0) {
timerShow.running = true
} else {
_showNow()
}
} else {
_hideNow()
}
}
function show() {
isVisible = true
}
function hide() {
isVisible = false
timerShow.running = false
}
function _showNow() {
// Compute new size everytime we show the tooltip
width = Math.max(50 * scaling, tooltipText.implicitWidth + Style.marginL * 2 * scaling)
height = Math.max(40 * scaling, tooltipText.implicitHeight + Style.marginM * 2 * scaling)
if (!target) {
return
}
if (positionLeft) {
// Position tooltip to the left of the target
var pos = target.mapToGlobal(0, 0)
x = pos.x - width - 12 // 12 px margin to the left
y = pos.y - height / 2 + target.height / 2
} else if (positionRight) {
// Position tooltip to the right of the target
var pos = target.mapToGlobal(target.width, 0)
x = pos.x + 12 // 12 px margin to the right
y = pos.y - height / 2 + target.height / 2
} else if (positionAbove) {
// Position tooltip above the target
var pos = target.mapToGlobal(0, 0)
x = pos.x - width / 2 + target.width / 2
y = pos.y - height - 12 // 12 px margin above
} else {
// Position tooltip below the target
var pos = target.mapToGlobal(0, target.height)
x = pos.x - width / 2 + target.width / 2
y = pos.y + 12 // 12 px margin below
}
// Start with animation values
tooltipRect.scaleValue = 0.8
tooltipRect.opacityValue = 0.0
visible = true
// Use a timer to trigger the animation after the component is visible
showTimer.start()
}
function _hideNow() {
// Start hide animation
tooltipRect.scaleValue = 0.8
tooltipRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
Connections {
target: root.target
function onXChanged() {
if (root.visible) {
root._showNow()
}
}
function onYChanged() {
if (root.visible) {
root._showNow()
}
}
function onWidthChanged() {
if (root.visible) {
root._showNow()
}
}
function onHeightChanged() {
if (root.visible) {
root._showNow()
}
}
}
Connections {
target: root
function onTextChanged() {
if (root.visible) {
root._showNow()
}
}
}
Timer {
id: timerShow
interval: delay
running: false
repeat: false
onTriggered: {
_showNow()
running = false
}
}
// Timer to hide tooltip after animation
Timer {
id: hideTimer
interval: Style.animationNormal
repeat: false
onTriggered: {
visible = false
}
}
// Timer to trigger show animation
Timer {
id: showTimer
interval: Style.animationFast / 15 // Very short delay to ensure component is visible
repeat: false
onTriggered: {
// Animate to final values
tooltipRect.scaleValue = 1.0
tooltipRect.opacityValue = 1.0
}
}
Rectangle {
id: tooltipRect
anchors.fill: parent
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
z: 1
// Animation properties
property real scaleValue: 1.0
property real opacityValue: 1.0
scale: scaleValue
opacity: opacityValue
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
NText {
id: tooltipText
anchors.centerIn: parent
text: root.text
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
}
+6
View File
@@ -39,6 +39,7 @@ import qs.Modules.Notification
import qs.Modules.OSD
import qs.Modules.Settings
import qs.Modules.Toast
import qs.Modules.Tooltip
import qs.Modules.Wallpaper
ShellRoot {
@@ -50,6 +51,10 @@ ShellRoot {
Bar {}
Dock {}
Tooltip {
id: globalTooltip
}
Notification {
id: notification
}
@@ -115,6 +120,7 @@ ShellRoot {
Component.onCompleted: {
// Save a ref. to our lockScreen so we can access it easily
PanelService.lockScreen = lockScreen
PanelService.tooltip = globalTooltip
BarWidgetRegistry.init()
}