44import QtQuick
55import QtQuick.Controls
66import ChatView
7+ import UIControls
78
89Item {
910 id: nav
1011
1112 property var chatModel
12- property var userIndices: []
13- property int hoveredDotIndex: - 1
13+ property var entries: []
1414 property color dotColor: " #92BD6C"
15+ property int currentMessageIndex: - 1
16+
17+ readonly property int dotCount: entries .length
18+ readonly property int verticalPadding: 8
19+ readonly property int minDotSpacing: 18
20+ readonly property real availableHeight: Math .max (0 , height - 2 * verticalPadding)
21+ readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1 ) * minDotSpacing : 0
22+ readonly property bool needsScrolling: naturalHeight > availableHeight
23+ readonly property real contentHeight: needsScrolling
24+ ? naturalHeight + 2 * verticalPadding
25+ : Math .max (height, 2 * verticalPadding)
1526
1627 signal messageClicked (int messageIndex)
1728
1829 implicitWidth: 16
1930
2031 function rebuild () {
21- if (chatModel)
22- userIndices = chatModel .userMessageIndices ()
23- else
24- userIndices = []
32+ entries = chatModel ? chatModel .userMessagePreviews (80 ) : []
33+ Qt .callLater (scrollCurrentIntoView)
34+ }
35+
36+ function updateCurrentFromModelIndex (modelIdx ) {
37+ if (modelIdx < 0 ) {
38+ currentMessageIndex = - 1
39+ return
40+ }
41+ let best = - 1
42+ for (let i = 0 ; i < entries .length ; ++ i) {
43+ const e = entries[i]
44+ if (! e)
45+ continue
46+ const mi = e .messageIndex
47+ if (mi <= modelIdx)
48+ best = mi
49+ else
50+ break
51+ }
52+ currentMessageIndex = best
53+ }
54+
55+ function uiIndexOf (messageIndex ) {
56+ for (let i = 0 ; i < entries .length ; ++ i) {
57+ const e = entries[i]
58+ if (e && e .messageIndex === messageIndex)
59+ return i
60+ }
61+ return - 1
62+ }
63+
64+ function dotCenterY (uiIndex ) {
65+ const count = dotCount
66+ if (count <= 1 )
67+ return contentHeight / 2
68+ const spacing = needsScrolling
69+ ? minDotSpacing
70+ : availableHeight / (count - 1 )
71+ return verticalPadding + spacing * uiIndex
72+ }
73+
74+ function scrollCurrentIntoView () {
75+ if (! needsScrolling || currentMessageIndex < 0 )
76+ return
77+ const ui = uiIndexOf (currentMessageIndex)
78+ if (ui < 0 )
79+ return
80+ const y = dotCenterY (ui)
81+ const margin = 24
82+ if (y < flick .contentY + margin)
83+ flick .contentY = Math .max (0 , y - margin)
84+ else if (y > flick .contentY + flick .height - margin)
85+ flick .contentY = Math .min (
86+ Math .max (0 , flick .contentHeight - flick .height ),
87+ y - flick .height + margin)
2588 }
2689
2790 onChatModelChanged: rebuild ()
91+ onCurrentMessageIndexChanged: scrollCurrentIntoView ()
2892 Component .onCompleted : rebuild ()
2993
3094 Connections {
@@ -34,70 +98,90 @@ Item {
3498 function onRowsRemoved () { nav .rebuild () }
3599 function onModelReset () { nav .rebuild () }
36100 function onModelReseted () { nav .rebuild () }
37- function onLayoutChanged () { nav .rebuild () }
101+ function onDataChanged () { nav .rebuild () }
38102 }
39103
40- Rectangle {
41- id: spine
42-
43- visible: nav .userIndices .length > 1
44- x: nav .width / 2 - width / 2
45- y: 14
46- width: 1
47- height: Math .max (0 , nav .height - 28 )
48- color: palette .mid
49- opacity: 0.4
50- }
51-
52- Repeater {
53- model: nav .userIndices
54-
55- delegate: Item {
56- id: dotItem
57-
58- required property var modelData
59- required property int index
60-
61- width: nav .width
62- height: 14
63- x: 0
64- y: {
65- const count = nav .userIndices .length
66- const dotH = height
67- if (count <= 1 )
68- return (nav .height - dotH) / 2
69- const top = 7
70- const bottom = nav .height - dotH - 7
71- return top + (bottom - top) * index / (count - 1 )
72- }
73-
74- Rectangle {
75- id: dot
76- anchors .centerIn : parent
77- width: dotArea .containsMouse ? 10 : 7
78- height: width
79- radius: width / 2
80- color: dotArea .containsMouse ? Qt .lighter (nav .dotColor , 1.15 ) : nav .dotColor
81- border .color : Qt .darker (nav .dotColor , 1.4 )
82- border .width : 1
83- opacity: dotArea .containsMouse ? 1.0 : 0.9
84-
85- Behavior on width { NumberAnimation { duration: 120 } }
86- Behavior on color { ColorAnimation { duration: 120 } }
87- }
88-
89- MouseArea {
90- id: dotArea
91-
92- anchors .fill : parent
93- hoverEnabled: true
94- cursorShape: Qt .PointingHandCursor
95- onClicked: nav .messageClicked (dotItem .modelData )
104+ Flickable {
105+ id: flick
106+
107+ anchors .fill : parent
108+ contentWidth: width
109+ contentHeight: nav .contentHeight
110+ interactive: nav .needsScrolling
111+ clip: true
112+ boundsBehavior: Flickable .StopAtBounds
113+
114+ Rectangle {
115+ id: spine
116+
117+ visible: nav .dotCount > 1
118+ anchors .horizontalCenter : parent .horizontalCenter
119+ y: nav .verticalPadding
120+ width: 1
121+ height: Math .max (0 , flick .contentHeight - 2 * nav .verticalPadding )
122+ color: palette .mid
123+ opacity: 0.4
124+ }
96125
97- ToolTip .visible : containsMouse
98- ToolTip .delay : 400
99- ToolTip .text : qsTr (" Jump to message #%1" ).arg (dotItem .index + 1 )
126+ Repeater {
127+ model: nav .entries
128+
129+ delegate: Item {
130+ id: dotItem
131+
132+ required property var modelData
133+ required property int index
134+
135+ readonly property int msgIndex: modelData && modelData .messageIndex !== undefined
136+ ? modelData .messageIndex : - 1
137+ readonly property string preview: modelData && modelData .preview !== undefined
138+ ? modelData .preview : " "
139+ readonly property bool isCurrent: nav .currentMessageIndex === msgIndex
140+
141+ width: 16
142+ height: 14
143+ anchors .horizontalCenter : parent .horizontalCenter
144+ y: nav .dotCenterY (index) - height / 2
145+
146+ Rectangle {
147+ id: dot
148+
149+ anchors .centerIn : parent
150+ width: dotItem .isCurrent ? 11 : (dotArea .containsMouse ? 10 : 7 )
151+ height: width
152+ radius: width / 2
153+ color: dotArea .containsMouse
154+ ? Qt .lighter (nav .dotColor , 1.2 )
155+ : nav .dotColor
156+ border .color : dotItem .isCurrent
157+ ? Qt .darker (nav .dotColor , 1.7 )
158+ : Qt .darker (nav .dotColor , 1.4 )
159+ border .width : dotItem .isCurrent ? 2 : 1
160+ opacity: dotItem .isCurrent || dotArea .containsMouse ? 1.0 : 0.55
161+
162+ Behavior on width { NumberAnimation { duration: 120 } }
163+ Behavior on opacity { NumberAnimation { duration: 120 } }
164+ Behavior on color { ColorAnimation { duration: 120 } }
165+ }
166+
167+ MouseArea {
168+ id: dotArea
169+
170+ anchors .fill : parent
171+ hoverEnabled: true
172+ cursorShape: Qt .PointingHandCursor
173+ onClicked: nav .messageClicked (dotItem .msgIndex )
174+
175+ QoAToolTip {
176+ visible: dotArea .containsMouse
177+ delay: 350
178+ text: dotItem .preview .length > 0
179+ ? qsTr (" #%1 · %2" ).arg (dotItem .index + 1 ).arg (dotItem .preview )
180+ : qsTr (" Jump to message #%1" ).arg (dotItem .index + 1 )
181+ }
182+ }
100183 }
101184 }
102185 }
186+
103187}
0 commit comments