diff --git a/Modules/Bar/Extras/BarPillHorizontal.qml b/Modules/Bar/Extras/BarPillHorizontal.qml index f32bfc482..4962153cb 100644 --- a/Modules/Bar/Extras/BarPillHorizontal.qml +++ b/Modules/Bar/Extras/BarPillHorizontal.qml @@ -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) { diff --git a/Modules/Bar/Extras/BarPillVertical.qml b/Modules/Bar/Extras/BarPillVertical.qml index deb304be1..f24ff0867 100644 --- a/Modules/Bar/Extras/BarPillVertical.qml +++ b/Modules/Bar/Extras/BarPillVertical.qml @@ -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) { diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index 8a43014f6..537c53bd7 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -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() { diff --git a/Modules/Bar/Widgets/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml index ce374bd4f..e76fa2342 100644 --- a/Modules/Bar/Widgets/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -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) } diff --git a/Modules/Bar/Widgets/Clock.qml b/Modules/Bar/Widgets/Clock.qml index 0b314ffd7..29bce8c61 100644 --- a/Modules/Bar/Widgets/Clock.qml +++ b/Modules/Bar/Widgets/Clock.qml @@ -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) } } diff --git a/Modules/Bar/Widgets/ControlCenter.qml b/Modules/Bar/Widgets/ControlCenter.qml index b730152af..69c1b39ba 100644 --- a/Modules/Bar/Widgets/ControlCenter.qml +++ b/Modules/Bar/Widgets/ControlCenter.qml @@ -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) diff --git a/Modules/Bar/Widgets/DarkMode.qml b/Modules/Bar/Widgets/DarkMode.qml index 333b3a9bc..b35dd236d 100644 --- a/Modules/Bar/Widgets/DarkMode.qml +++ b/Modules/Bar/Widgets/DarkMode.qml @@ -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 diff --git a/Modules/Bar/Widgets/KeepAwake.qml b/Modules/Bar/Widgets/KeepAwake.qml index 72321b7c2..ba09d92a9 100644 --- a/Modules/Bar/Widgets/KeepAwake.qml +++ b/Modules/Bar/Widgets/KeepAwake.qml @@ -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 diff --git a/Modules/Bar/Widgets/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml index 002fc67c3..53914fcb9 100644 --- a/Modules/Bar/Widgets/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -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 - } } diff --git a/Modules/Bar/Widgets/NightLight.qml b/Modules/Bar/Widgets/NightLight.qml index 72f44867c..b972d37d9 100644 --- a/Modules/Bar/Widgets/NightLight.qml +++ b/Modules/Bar/Widgets/NightLight.qml @@ -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) { diff --git a/Modules/Bar/Widgets/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml index c5a9f47b6..2fee32ae7 100644 --- a/Modules/Bar/Widgets/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -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 diff --git a/Modules/Bar/Widgets/PowerProfile.qml b/Modules/Bar/Widgets/PowerProfile.qml index 0158c0247..fbd2a719b 100644 --- a/Modules/Bar/Widgets/PowerProfile.qml +++ b/Modules/Bar/Widgets/PowerProfile.qml @@ -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 diff --git a/Modules/Bar/Widgets/ScreenRecorder.qml b/Modules/Bar/Widgets/ScreenRecorder.qml index 01971e257..5149a3a6b 100644 --- a/Modules/Bar/Widgets/ScreenRecorder.qml +++ b/Modules/Bar/Widgets/ScreenRecorder.qml @@ -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) diff --git a/Modules/Bar/Widgets/SessionMenu.qml b/Modules/Bar/Widgets/SessionMenu.qml index fb309d29b..f8f099293 100644 --- a/Modules/Bar/Widgets/SessionMenu.qml +++ b/Modules/Bar/Widgets/SessionMenu.qml @@ -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 diff --git a/Modules/Bar/Widgets/Taskbar.qml b/Modules/Bar/Widgets/Taskbar.qml index 30057eb7b..5f7d7ed38 100644 --- a/Modules/Bar/Widgets/Taskbar.qml +++ b/Modules/Bar/Widgets/Taskbar.qml @@ -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() } } } diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 3904a9cf1..927ffae9b 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -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() } } } diff --git a/Modules/Bar/Widgets/WallpaperSelector.qml b/Modules/Bar/Widgets/WallpaperSelector.qml index 79e794ea8..4be3e9a1b 100644 --- a/Modules/Bar/Widgets/WallpaperSelector.qml +++ b/Modules/Bar/Widgets/WallpaperSelector.qml @@ -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 diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml index 610a0351e..00fefab69 100644 --- a/Modules/Bar/Widgets/WiFi.qml +++ b/Modules/Bar/Widgets/WiFi.qml @@ -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) } diff --git a/Modules/Dock/Dock.qml b/Modules/Dock/Dock.qml index f6135c371..6b8d74843 100644 --- a/Modules/Dock/Dock.qml +++ b/Modules/Dock/Dock.qml @@ -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 } diff --git a/Modules/OSD/OSD.qml b/Modules/OSD/OSD.qml index 1ad395a75..059dcb5dd 100644 --- a/Modules/OSD/OSD.qml +++ b/Modules/OSD/OSD.qml @@ -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() + } + }) } } diff --git a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml index 68310021e..a198b1e34 100644 --- a/Modules/Settings/Bar/BarWidgetSettingsDialog.qml +++ b/Modules/Settings/Bar/BarWidgetSettingsDialog.qml @@ -69,6 +69,7 @@ Popup { NIconButton { icon: "close" + tooltipText: "Close" onClicked: widgetSettings.close() } } diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml index a701c8980..6a635aeaf 100644 --- a/Modules/Toast/ToastScreen.qml +++ b/Modules/Toast/ToastScreen.qml @@ -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() { diff --git a/Modules/Tooltip/Tooltip.qml b/Modules/Tooltip/Tooltip.qml new file mode 100644 index 000000000..171971f63 --- /dev/null +++ b/Modules/Tooltip/Tooltip.qml @@ -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() + } +} diff --git a/Services/BarService.qml b/Services/BarService.qml index 9f8611ed3..63e607cc2 100644 --- a/Services/BarService.qml +++ b/Services/BarService.qml @@ -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" + } + } } diff --git a/Services/PanelService.qml b/Services/PanelService.qml index 5d33aeb79..59db74b43 100644 --- a/Services/PanelService.qml +++ b/Services/PanelService.qml @@ -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 diff --git a/Widgets/NButton.qml b/Widgets/NButton.qml index f05247ae3..89e71c12d 100644 --- a/Widgets/NButton.qml +++ b/Widgets/NButton.qml @@ -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() } } } diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 0e6c2c8e6..5c82dfb33 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -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 diff --git a/Widgets/NSearchableComboBox.qml b/Widgets/NSearchableComboBox.qml index ed2fe3742..e949405d2 100644 --- a/Widgets/NSearchableComboBox.qml +++ b/Widgets/NSearchableComboBox.qml @@ -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() + } + }) } } } diff --git a/Widgets/NTooltip.qml b/Widgets/NTooltip.qml deleted file mode 100644 index f0cd55df5..000000000 --- a/Widgets/NTooltip.qml +++ /dev/null @@ -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 - } - } -} diff --git a/shell.qml b/shell.qml index cd1689bc5..e333472ce 100644 --- a/shell.qml +++ b/shell.qml @@ -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() }