rotated view + wheel inverter
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import ZShell.Config
|
|
||||||
import ZShell.Llm
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Llm
|
||||||
import qs.Modules.Notifications.Sidebar.Chat.Content
|
import qs.Modules.Notifications.Sidebar.Chat.Content
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -15,7 +16,7 @@ Item {
|
|||||||
signal requestClose
|
signal requestClose
|
||||||
|
|
||||||
function scrollToBottom(): void {
|
function scrollToBottom(): void {
|
||||||
Qt.callLater(list.positionViewAtBeginning);
|
Qt.callLater(() => listLoader.item?.positionViewAtBeginning());
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(text: string): void {
|
function send(text: string): void {
|
||||||
@@ -26,8 +27,6 @@ Item {
|
|||||||
input.text = "";
|
input.text = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: input.focus = true
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: header
|
id: header
|
||||||
|
|
||||||
@@ -54,10 +53,8 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalFadeListView {
|
Loader {
|
||||||
id: list
|
id: listLoader
|
||||||
|
|
||||||
property bool userScrolledUp: false
|
|
||||||
|
|
||||||
anchors.bottom: input.top
|
anchors.bottom: input.top
|
||||||
anchors.bottomMargin: Tokens.spacing.medium
|
anchors.bottomMargin: Tokens.spacing.medium
|
||||||
@@ -65,58 +62,83 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: header.bottom
|
anchors.top: header.bottom
|
||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
cacheBuffer: height * 20
|
active: root.chatData && root.chatData.loaded
|
||||||
clip: true
|
|
||||||
fadeAmount: 0.05
|
|
||||||
fadeThreshold: Tokens.padding.medium
|
|
||||||
model: root.chatData.messagesModel
|
|
||||||
spacing: 0
|
|
||||||
verticalLayoutDirection: VerticalFadeListView.BottomToTop
|
|
||||||
|
|
||||||
add: Transition {
|
sourceComponent: ListView {
|
||||||
Anim {
|
id: list
|
||||||
from: list.width
|
|
||||||
property: "x"
|
property bool userScrolledUp: false
|
||||||
to: 0
|
|
||||||
|
function scrollToBottom(): void {
|
||||||
|
Qt.callLater(() => positionViewAtBeginning());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
delegate: MessageDelegate {}
|
cacheBuffer: height * 20
|
||||||
displaced: Transition {
|
clip: true
|
||||||
Anim {
|
// fadeAmount: 0.05
|
||||||
property: "y"
|
// fadeThreshold: Tokens.padding.medium
|
||||||
|
model: root.chatData.messagesModel
|
||||||
|
spacing: 0
|
||||||
|
rotation: 180
|
||||||
|
|
||||||
|
// add: Transition {
|
||||||
|
// Anim {
|
||||||
|
// from: 10
|
||||||
|
// property: "y"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
delegate: MessageDelegate {
|
||||||
|
rotation: 180
|
||||||
}
|
}
|
||||||
}
|
|
||||||
move: Transition {
|
// displaced: Transition {
|
||||||
|
// Anim {
|
||||||
|
// property: "y"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// move: Transition {
|
||||||
|
// Anim {
|
||||||
|
// property: "y"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
positionViewAtBeginning();
|
||||||
|
forceActiveFocus();
|
||||||
|
}
|
||||||
|
onAtYEndChanged: {
|
||||||
|
if (atYEnd)
|
||||||
|
userScrolledUp = false;
|
||||||
|
}
|
||||||
|
onContentHeightChanged: {
|
||||||
|
if (!userScrolledUp && atYEnd)
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
onCountChanged: {
|
||||||
|
if (!userScrolledUp)
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
onMovingChanged: {
|
||||||
|
if (moving)
|
||||||
|
userScrolledUp = !atYEnd;
|
||||||
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
property: "y"
|
id: scrollAnim
|
||||||
|
|
||||||
|
property: "contentY"
|
||||||
|
target: list
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
|
||||||
|
WheelInverter {
|
||||||
|
target: list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: positionViewAtBeginning()
|
// Trigger the load even before we're ready to show the list.
|
||||||
onAtYEndChanged: {
|
Component.onCompleted: if (root.chatData)
|
||||||
if (atYEnd)
|
root.chatData.messagesModel
|
||||||
userScrolledUp = false;
|
|
||||||
}
|
|
||||||
onContentHeightChanged: {
|
|
||||||
if (!userScrolledUp && atYEnd)
|
|
||||||
root.scrollToBottom();
|
|
||||||
}
|
|
||||||
onCountChanged: {
|
|
||||||
if (!userScrolledUp)
|
|
||||||
root.scrollToBottom();
|
|
||||||
}
|
|
||||||
onMovingChanged: {
|
|
||||||
if (moving)
|
|
||||||
userScrolledUp = !atYEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
id: scrollAnim
|
|
||||||
|
|
||||||
property: "contentY"
|
|
||||||
target: list
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EmptyBackground {
|
EmptyBackground {
|
||||||
@@ -133,7 +155,7 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
implicitHeight: scrollToBottom.implicitHeight
|
implicitHeight: scrollToBottom.implicitHeight
|
||||||
implicitWidth: scrollToBottom.implicitWidth
|
implicitWidth: scrollToBottom.implicitWidth
|
||||||
scale: list.visibleArea.yPosition + list.visibleArea.heightRatio < 1 && list.contentHeight > list.height ? 1 : 0
|
scale: !listLoader.item.atYBeginning && listLoader.item.contentHeight > listLoader.item.height ? 1 : 0
|
||||||
visible: scale > 0
|
visible: scale > 0
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
@@ -157,7 +179,7 @@ Item {
|
|||||||
type: IconButton.Tonal
|
type: IconButton.Tonal
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
list.positionViewAtBeginning();
|
listLoader.item.positionViewAtBeginning();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ Item {
|
|||||||
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2
|
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2
|
||||||
anchors.right: root.isUser ? parent.right : undefined
|
anchors.right: root.isUser ? parent.right : undefined
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
// enabled: root.segment.running
|
||||||
|
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
// User messages stay a plain editable text field.
|
// User messages stay a plain editable text field.
|
||||||
TextEditBase {
|
TextEditBase {
|
||||||
id: msgText
|
id: msgText
|
||||||
@@ -103,12 +109,13 @@ Item {
|
|||||||
Actions {
|
Actions {
|
||||||
id: actionsRow
|
id: actionsRow
|
||||||
|
|
||||||
|
readonly property bool shouldBeActive: (root.segment.type === LlmSegment.Type.Content) && !root.segment.running && root.repeater.count === (root.index + 1)
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
|
||||||
anchors.top: bubble.bottom
|
anchors.top: bubble.bottom
|
||||||
visible: {
|
opacity: shouldBeActive ? 1 : 0
|
||||||
return (root.segment.type === LlmSegment.Type.Content) && root.segment.status !== LlmSegment.Status.Running && root.repeater.count === (root.index + 1);
|
visible: opacity > 0
|
||||||
}
|
|
||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
edit: msgText
|
edit: msgText
|
||||||
hovered: root.hovered
|
hovered: root.hovered
|
||||||
@@ -116,5 +123,9 @@ Item {
|
|||||||
segment: root.segment
|
segment: root.segment
|
||||||
current: root.current
|
current: root.current
|
||||||
message: root.message
|
message: root.message
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import qs.Services
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property ChatGeneration current: modelData.activeGeneration
|
property ChatGeneration current: modelData.activeGeneration
|
||||||
required property int index
|
required property int index
|
||||||
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
readonly property bool isUser: modelData.role === ChatMessage.Role.User
|
||||||
required property ChatMessage modelData
|
required property ChatMessage modelData
|
||||||
@@ -60,22 +60,79 @@ MouseArea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: ListView.view.width
|
implicitWidth: ListView.view.width
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
preventStealing: true
|
preventStealing: false
|
||||||
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
implicitHeight: layout.implicitHeight + Tokens.padding.medium
|
||||||
|
|
||||||
Binding {
|
Behavior on current {
|
||||||
property: "contentY"
|
id: changeAnim
|
||||||
restoreMode: Binding.RestoreNone
|
|
||||||
target: root.ListView.view
|
property var object: targetProperty
|
||||||
value: root.y - Tokens.padding.large * 2
|
readonly property bool next: {
|
||||||
when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY)
|
const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue);
|
||||||
|
return cond;
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: SequentialAnimation {
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "x"
|
||||||
|
to: changeAnim.next ? -root.width / 2 : root.width / 2
|
||||||
|
from: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyAction {}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "x"
|
||||||
|
from: changeAnim.next ? root.width / 2 : -root.width / 2
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Behavior on implicitHeight {
|
||||||
|
// enabled: !root.isUser && root.current.streaming
|
||||||
|
//
|
||||||
|
// Anim {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
onCurrentChanged: {
|
||||||
|
console.log(modelData.generations.indexOf(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binding {
|
||||||
|
// property: "contentY"
|
||||||
|
// restoreMode: Binding.RestoreNone
|
||||||
|
// target: root.ListView.view
|
||||||
|
// value: root.y - Tokens.padding.large * 2
|
||||||
|
// when: root.reasoningExpanded && root.ListView.view && ((root.y - Tokens.padding.large * 2) < root.ListView.view.contentY)
|
||||||
|
// }
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
id: restoreAnim
|
id: restoreAnim
|
||||||
|
|
||||||
@@ -91,6 +148,20 @@ MouseArea {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
// onImplicitHeightChanged: console.log("LAYOUT:", layout.implicitHeight)
|
||||||
|
|
||||||
|
// add: Transition {
|
||||||
|
// Anim {
|
||||||
|
// from: 10
|
||||||
|
// property: "y"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// move: Transition {
|
||||||
|
// Anim {
|
||||||
|
// property: "y"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: segmentRep
|
id: segmentRep
|
||||||
|
|
||||||
@@ -105,12 +176,8 @@ MouseArea {
|
|||||||
roleValue: "process"
|
roleValue: "process"
|
||||||
|
|
||||||
delegate: ProcessBlock {
|
delegate: ProcessBlock {
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
segments: modelData.segments
|
|
||||||
isActive: index === root.blocks.length - 1
|
|
||||||
width: root.width
|
width: root.width
|
||||||
|
blocks: root.blocks
|
||||||
|
|
||||||
onExpandedChanged: root.handleReasoningToggle(expanded)
|
onExpandedChanged: root.handleReasoningToggle(expanded)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,92 +5,106 @@ import ZShell.Llm
|
|||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
CustomClippingRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var segments
|
required property int index
|
||||||
required property bool isActive
|
required property var modelData
|
||||||
|
required property var blocks
|
||||||
|
readonly property var segments: modelData.segments
|
||||||
|
readonly property bool isActive: index === blocks.length - 1
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1]
|
readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1]
|
||||||
readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
|
readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
|
||||||
|
|
||||||
implicitHeight: expandedRect.implicitHeight + collapsedText.implicitHeight + expandedRect.anchors.topMargin
|
implicitHeight: expanded ? expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin * 2 : layout.implicitHeight + Tokens.spacing.small
|
||||||
|
|
||||||
LoadingIndicator {
|
Behavior on implicitHeight {
|
||||||
id: spinnerReasoning
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
anchors.centerIn: expandBtn
|
|
||||||
implicitSize: collapsedText.implicitHeight
|
|
||||||
opacity: root.isActive ? 1 : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
RowLayout {
|
||||||
id: expandBtn
|
id: layout
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: collapsedText.verticalCenter
|
|
||||||
font.pointSize: Tokens.font.size.large
|
|
||||||
anchors.topMargin: 0
|
|
||||||
icon: "keyboard_arrow_down"
|
|
||||||
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
|
||||||
opacity: root.isActive ? 0 : 1
|
|
||||||
rotation: root.expanded ? 180 : 0
|
|
||||||
type: IconButton.Text
|
|
||||||
|
|
||||||
Behavior on rotation {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: collapsedText
|
|
||||||
|
|
||||||
anchors.left: expandBtn.right
|
|
||||||
anchors.margins: Tokens.padding.medium
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: 0
|
anchors.left: parent.left
|
||||||
color: Colors.palette.m3outline
|
anchors.right: parent.right
|
||||||
font.pointSize: Tokens.font.size.small
|
|
||||||
text: {
|
Item {
|
||||||
if (root.isActive) {
|
implicitHeight: expandBtn.implicitHeight
|
||||||
if (root.lastSegment.type === LlmSegment.Type.ToolCall)
|
implicitWidth: expandBtn.implicitWidth
|
||||||
return qsTr("Using %1...").arg(root.lastSegment.name);
|
|
||||||
return qsTr("Thinking...");
|
LoadingIndicator {
|
||||||
|
id: spinnerReasoning
|
||||||
|
|
||||||
|
implicitSize: collapsedText.implicitHeight
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: root.isActive ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: expandBtn
|
||||||
|
|
||||||
|
font.pointSize: Tokens.font.size.large
|
||||||
|
icon: "keyboard_arrow_down"
|
||||||
|
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||||
|
anchors.centerIn: parent
|
||||||
|
opacity: root.isActive ? 0 : 1
|
||||||
|
rotation: root.expanded ? 180 : 0
|
||||||
|
type: IconButton.Text
|
||||||
|
|
||||||
|
Behavior on rotation {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: root.expanded = !root.expanded
|
||||||
}
|
}
|
||||||
return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
CustomText {
|
||||||
Anim {}
|
id: collapsedText
|
||||||
|
|
||||||
|
color: Colors.palette.m3outline
|
||||||
|
font.pointSize: Tokens.font.size.small
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: Tokens.spacing.medium
|
||||||
|
text: {
|
||||||
|
if (root.isActive) {
|
||||||
|
if (root.lastSegment.type === LlmSegment.Type.ToolCall)
|
||||||
|
return qsTr("Using %1...").arg(root.lastSegment.name);
|
||||||
|
return qsTr("Thinking...");
|
||||||
|
}
|
||||||
|
return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomRect {
|
||||||
id: expandedRect
|
id: expandedRect
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: collapsedText.bottom
|
anchors.top: layout.bottom
|
||||||
anchors.topMargin: Tokens.spacing.medium
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.topMargin: Tokens.spacing.small
|
||||||
|
anchors.bottomMargin: Tokens.spacing.small
|
||||||
color: Colors.palette.m3surfaceContainerLow
|
color: Colors.palette.m3surfaceContainerLow
|
||||||
implicitHeight: root.expanded ? expandedContent.contentHeight + expandedContent.anchors.margins * 2 : 0
|
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
||||||
opacity: root.expanded ? 1 : 0
|
opacity: root.expanded ? 1 : 0
|
||||||
radius: Tokens.rounding.medium
|
radius: Tokens.rounding.medium
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
qml_module(ZShell-components
|
qml_module(ZShell-components
|
||||||
URI ZShell.Components
|
URI ZShell.Components
|
||||||
SOURCES
|
SOURCES
|
||||||
lazylistview.hpp lazylistview.cpp
|
lazylistview.hpp lazylistview.cpp
|
||||||
wavyline.hpp wavyline.cpp
|
wavyline.hpp wavyline.cpp
|
||||||
buttonrow.hpp buttonrow.cpp
|
buttonrow.hpp buttonrow.cpp
|
||||||
carouselview.hpp carouselview.cpp
|
carouselview.hpp carouselview.cpp
|
||||||
LIBRARIES
|
wheelinverter.hpp
|
||||||
Qt::Quick
|
LIBRARIES
|
||||||
|
Qt::Quick
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
class WheelInverter : public QQuickItem {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(
|
||||||
|
QQuickItem* target READ target WRITE setTarget NOTIFY targetChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
using QQuickItem::QQuickItem;
|
||||||
|
|
||||||
|
QQuickItem* target() const { return m_target; }
|
||||||
|
|
||||||
|
void setTarget(QQuickItem* target) {
|
||||||
|
if (m_target == target) return;
|
||||||
|
|
||||||
|
if (m_target) m_target->removeEventFilter(this);
|
||||||
|
|
||||||
|
m_target = target;
|
||||||
|
|
||||||
|
if (m_target) m_target->installEventFilter(this);
|
||||||
|
|
||||||
|
emit targetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
class InvertedWheelEvent : public QWheelEvent {
|
||||||
|
public:
|
||||||
|
using QWheelEvent::QWheelEvent;
|
||||||
|
|
||||||
|
bool invertedByWheelInverter = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool eventFilter(QObject* watched, QEvent* event) override {
|
||||||
|
if (watched != m_target || event->type() != QEvent::Wheel)
|
||||||
|
return QQuickItem::eventFilter(watched, event);
|
||||||
|
|
||||||
|
auto* wheel = static_cast<QWheelEvent*>(event);
|
||||||
|
|
||||||
|
const bool inverted = dynamic_cast<InvertedWheelEvent*>(wheel) !=
|
||||||
|
nullptr;
|
||||||
|
|
||||||
|
if (inverted) return false;
|
||||||
|
|
||||||
|
auto* window = m_target ? m_target->window() : nullptr;
|
||||||
|
if (!window) {
|
||||||
|
qInfo() << "[WheelInverter] no window";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF windowPos =
|
||||||
|
window->mapFromGlobal(wheel->globalPosition());
|
||||||
|
|
||||||
|
auto* invertedEvent = new InvertedWheelEvent(
|
||||||
|
windowPos,
|
||||||
|
wheel->globalPosition(),
|
||||||
|
-wheel->pixelDelta(),
|
||||||
|
-wheel->angleDelta(),
|
||||||
|
wheel->buttons(),
|
||||||
|
wheel->modifiers(),
|
||||||
|
wheel->phase(),
|
||||||
|
wheel->inverted(),
|
||||||
|
wheel->source(),
|
||||||
|
wheel->pointingDevice());
|
||||||
|
|
||||||
|
invertedEvent->setTimestamp(wheel->timestamp());
|
||||||
|
|
||||||
|
QCoreApplication::postEvent(window, invertedEvent);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void targetChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QPointer<QQuickItem> m_target;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::components
|
||||||
@@ -23,6 +23,7 @@ class ChatSession : public QObject {
|
|||||||
Q_PROPERTY(QDateTime createdAt READ createdAt CONSTANT)
|
Q_PROPERTY(QDateTime createdAt READ createdAt CONSTANT)
|
||||||
Q_PROPERTY(QDateTime updatedAt READ updatedAt NOTIFY updatedAtChanged)
|
Q_PROPERTY(QDateTime updatedAt READ updatedAt NOTIFY updatedAtChanged)
|
||||||
Q_PROPERTY(bool pinned READ pinned NOTIFY pinnedChanged)
|
Q_PROPERTY(bool pinned READ pinned NOTIFY pinnedChanged)
|
||||||
|
Q_PROPERTY(bool loaded READ isLoaded NOTIFY loaded)
|
||||||
Q_PROPERTY(int messageCount READ messageCount NOTIFY messageCountChanged)
|
Q_PROPERTY(int messageCount READ messageCount NOTIFY messageCountChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
ZShell::llm::ChatMessageModel* messagesModel READ messagesModel CONSTANT)
|
ZShell::llm::ChatMessageModel* messagesModel READ messagesModel CONSTANT)
|
||||||
|
|||||||
Reference in New Issue
Block a user