summaryrefslogtreecommitdiff
path: root/components/controls/Tooltip.qml
blob: d6650837c74fa874c5d5d9237df392d43cea2d02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import ".."
import qs.components.effects
import qs.services
import qs.config
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

Popup {
    id: root

    required property Item target
    required property string text
    property int delay: 500
    property int timeout: 0

    property bool tooltipVisible: false
    property Timer showTimer: Timer {
        interval: root.delay
        onTriggered: root.tooltipVisible = true
    }
    property Timer hideTimer: Timer {
        interval: root.timeout
        onTriggered: root.tooltipVisible = false
    }

    // Popup properties - doesn't affect layout
    parent: {
        let p = target;
        // Walk up to find the root Item (usually has anchors.fill: parent)
        while (p && p.parent) {
            const parentItem = p.parent;
            // Check if this looks like a root pane Item
            if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) {
                return parentItem;
            }
            p = parentItem;
        }
        // Fallback
        return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target;
    }

    visible: tooltipVisible
    modal: false
    closePolicy: Popup.NoAutoClose
    padding: 0
    margins: 0
    background: Item {}

    // Update position when target moves or tooltip becomes visible
    onTooltipVisibleChanged: {
        if (tooltipVisible) {
            Qt.callLater(updatePosition);
        }
    }
    Connections {
        target: root.target
        function onXChanged() { if (root.tooltipVisible) root.updatePosition(); }
        function onYChanged() { if (root.tooltipVisible) root.updatePosition(); }
        function onWidthChanged() { if (root.tooltipVisible) root.updatePosition(); }
        function onHeightChanged() { if (root.tooltipVisible) root.updatePosition(); }
    }

    function updatePosition() {
        if (!target || !parent) return;
        
        // Wait for tooltipRect to have its size calculated
        Qt.callLater(() => {
            if (!target || !parent || !tooltipRect) return;
            
            // Get target position in parent's coordinate system
            const targetPos = target.mapToItem(parent, 0, 0);
            const targetCenterX = targetPos.x + target.width / 2;
            
            // Get tooltip size (use width/height if available, otherwise implicit)
            const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth;
            const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight;
            
            // Center tooltip horizontally on target
            let newX = targetCenterX - tooltipWidth / 2;
            
            // Position tooltip above target
            let newY = targetPos.y - tooltipHeight - Appearance.spacing.small;
            
            // Keep within bounds
            const padding = Appearance.padding.normal;
            if (newX < padding) {
                newX = padding;
            } else if (newX + tooltipWidth > (parent.width - padding)) {
                newX = parent.width - tooltipWidth - padding;
            }
            
            // Update popup position
            x = newX;
            y = newY;
        });
    }

    enter: Transition {
        Anim {
            property: "opacity"
            from: 0
            to: 1
            duration: Appearance.anim.durations.expressiveFastSpatial
            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
        }
    }

    exit: Transition {
        Anim {
            property: "opacity"
            from: 1
            to: 0
            duration: Appearance.anim.durations.expressiveFastSpatial
            easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
        }
    }

    // Monitor hover state
    Connections {
        target: root.target
        function onHoveredChanged() {
            if (target.hovered) {
                showTimer.start();
                if (timeout > 0) {
                    hideTimer.stop();
                    hideTimer.start();
                }
            } else {
                showTimer.stop();
                hideTimer.stop();
                tooltipVisible = false;
            }
        }
    }

    contentItem: StyledRect {
        id: tooltipRect

        implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2
        implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2

        color: Colours.palette.m3surfaceContainerHighest
        radius: Appearance.rounding.small
        antialiasing: true

        // Add elevation for depth
        Elevation {
            anchors.fill: parent
            radius: parent.radius
            z: -1
            level: 3
        }

        StyledText {
            id: tooltipText

            anchors.centerIn: parent
            
            text: root.text
            color: Colours.palette.m3onSurface
            font.pointSize: Appearance.font.size.small
        }
    }

    Component.onCompleted: {
        if (tooltipVisible) {
            updatePosition();
        }
    }
}