better support for opening chat in floating window

This commit is contained in:
2026-08-28 14:15:57 +02:00
parent 8a66c3b064
commit 327270f3fa
14 changed files with 419 additions and 168 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ ScrollBar {
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
readonly property real axisContentPos: isHorizontal ? flickable.contentX : flickable.contentY readonly property real axisContentPos: isHorizontal ? (flickable.contentX - flickable.originX) : (flickable.contentY - flickable.originY)
readonly property real axisLength: isHorizontal ? root.width : root.height readonly property real axisLength: isHorizontal ? root.width : root.height
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize) readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize) readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
@@ -102,9 +102,9 @@ ScrollBar {
var newPos = contentPosFromThumbStart(thumbStart); var newPos = contentPosFromThumbStart(thumbStart);
if (root.isHorizontal) if (root.isHorizontal)
root.flickable.contentX = newPos; root.flickable.contentX = newPos + root.flickable.originX;
else else
root.flickable.contentY = newPos; root.flickable.contentY = newPos + root.flickable.originY;
} }
function visualThumbStart() { function visualThumbStart() {
@@ -87,10 +87,6 @@ Item {
signal requestClose signal requestClose
function scrollToBottom(): void {
Qt.callLater(() => listLoader.item?.positionViewAtBeginning());
}
function send(text: string): void { function send(text: string): void {
if (text.trim() === "") if (text.trim() === "")
return; return;
@@ -129,11 +125,15 @@ Item {
isRound: true isRound: true
type: IconButton.Tonal type: IconButton.Tonal
enabled: !ChatState.isWindow
visible: enabled
onClicked: root.requestClose() onClicked: root.requestClose()
} }
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: ChatState.isWindow ? false : true
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Tokens.font.size.larger font.pointSize: Tokens.font.size.larger
text: qsTr(root.chatData.title) text: qsTr(root.chatData.title)
@@ -151,21 +151,20 @@ Item {
anchors.topMargin: Tokens.spacing.medium anchors.topMargin: Tokens.spacing.medium
active: root.chatData && root.chatData.loaded active: root.chatData && root.chatData.loaded
sourceComponent: ListView { sourceComponent: VerticalFadeListView {
id: list id: list
property bool userScrolledUp: false property bool userScrolledUp: false
property real lastCHeight: 0.0 property real lastCHeight: 0.0
function scrollToBottom(): void { function scrollToBottom(): void {
scrollAnim.to = 0;
scrollAnim.start(); scrollAnim.start();
} }
cacheBuffer: height * 20 cacheBuffer: Math.max(height * 20, 0)
clip: true clip: true
// fadeAmount: 0.05 fadeAmount: 0.05
// fadeThreshold: Tokens.padding.medium fadeThreshold: Tokens.padding.medium
model: root.chatData.messagesModel model: root.chatData.messagesModel
spacing: 0 spacing: 0
rotation: 180 rotation: 180
@@ -238,6 +237,7 @@ Item {
property: "contentY" property: "contentY"
target: list target: list
type: Anim.DefaultEffects type: Anim.DefaultEffects
to: list.originY
} }
WheelInverter { WheelInverter {
@@ -255,7 +255,7 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
visible: !root.chatData visible: root.chatData.messageCount < 1
} }
Item { Item {
@@ -2,6 +2,7 @@ import QtQuick
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Effects
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -9,12 +10,13 @@ CustomClippingRect {
id: root id: root
property bool expanded: false property bool expanded: false
property bool highlighted: false
required property ChatSession modelData required property ChatSession modelData
signal clicked(content: ChatSession) signal clicked(content: ChatSession)
signal remove(content: ChatSession) signal remove(content: ChatSession)
color: Colors.tPalette.m3surfaceContainer color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
implicitHeight: { implicitHeight: {
let h = 0; let h = 0;
@@ -33,6 +35,20 @@ CustomClippingRect {
return h; return h;
} }
radius: Tokens.rounding.large radius: Tokens.rounding.large
clip: true
CustomRect {
id: borderHighlight
anchors.fill: parent
border.width: root.highlighted ? 2 : 0
border.color: Colors.palette.m3secondary
radius: root.radius - border.width + 1
Behavior on border.width {
Anim {}
}
}
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: titleText.readOnly enabled: titleText.readOnly
@@ -49,6 +65,7 @@ CustomClippingRect {
anchors.topMargin: Tokens.padding.large anchors.topMargin: Tokens.padding.large
font.pointSize: Tokens.font.size.extraLarge font.pointSize: Tokens.font.size.extraLarge
text: root.modelData.icon || "check" text: root.modelData.icon || "check"
color: Colors.palette.m3onSurface
} }
Item { Item {
@@ -85,31 +102,73 @@ CustomClippingRect {
} }
} }
TextEditBase { Item {
id: titleText id: titleWrapper
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2 anchors.right: parent.right
font.pointSize: Tokens.font.size.normal implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
text: root.modelData.title
readOnly: true
Behavior on anchors.leftMargin { layer.enabled: true
Anim {} layer.effect: OpacityMask {
} maskSource: fadeMask
Behavior on anchors.topMargin {
Anim {}
} }
onEditingFinished: root.modelData.title = text TextFieldBase {
onReadOnlyChanged: { id: titleText
if (!readOnly) {
this.forceActiveFocus(); anchors.left: parent.left
cursorPosition = text.length; anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
} else { anchors.top: parent.top
root.forceActiveFocus(); anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
font.pointSize: Tokens.font.size.normal
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
this.forceActiveFocus();
cursorPosition = text.length;
} else {
root.forceActiveFocus();
}
}
}
}
CustomRect {
id: fadeMask
// anchors.top: titleText.top
// anchors.bottom: titleText.bottom
// anchors.left: titleText.left
// anchors.right: parent.right
anchors.fill: titleWrapper
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
} }
} }
} }
@@ -151,7 +210,7 @@ CustomClippingRect {
id: timestamp id: timestamp
anchors.left: parent.left anchors.left: parent.left
anchors.top: titleText.bottom anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall anchors.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small font.pointSize: Tokens.font.size.small
@@ -188,7 +247,7 @@ CustomClippingRect {
font.pointSize: Tokens.font.size.small font.pointSize: Tokens.font.size.small
maximumLineCount: 1 maximumLineCount: 1
opacity: !root.expanded ? 1 : 0 opacity: !root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? "" text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y { Behavior on y {
Anim {} Anim {}
@@ -213,7 +272,7 @@ CustomClippingRect {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
maximumLineCount: 5 maximumLineCount: 5
opacity: root.expanded ? 1 : 0 opacity: root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? "" text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? qsTr("No messages yet")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on y { Behavior on y {
@@ -286,7 +345,7 @@ CustomClippingRect {
implicitHeight: expandIcon.implicitHeight implicitHeight: expandIcon.implicitHeight
implicitWidth: expandIcon.implicitHeight implicitWidth: expandIcon.implicitHeight
radius: Tokens.rounding.full radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
StateLayer { StateLayer {
onClicked: root.expanded = !root.expanded onClicked: root.expanded = !root.expanded
@@ -298,6 +357,7 @@ CustomClippingRect {
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1 anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0 rotation: root.expanded ? 180 : 0
color: Colors.palette.m3onSurface
text: "expand_more" text: "expand_more"
Behavior on anchors.verticalCenterOffset { Behavior on anchors.verticalCenterOffset {
@@ -13,9 +13,10 @@ Item {
property alias list: list property alias list: list
property alias model: list.model property alias model: list.model
property bool highlight: false
signal deleteChatRequest(content: ChatSession) signal deleteChatRequest(content: ChatSession)
signal loadChatRequest(content: ChatSession) signal loadChatRequest(content: ChatSession, index: int)
signal newChatRequest signal newChatRequest
CustomText { CustomText {
@@ -40,16 +41,48 @@ 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 * 2
clip: true clip: true
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
add: Transition {
Anim {
from: list.width
property: "x"
}
}
remove: Transition {
Anim {
to: list.width
property: "x"
}
Anim {
to: 0
property: "opacity"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
move: Transition {
Anim {
property: "y"
}
}
delegate: ChatDelegate { delegate: ChatDelegate {
id: chat id: chat
implicitWidth: ListView.view.width required property int index
onClicked: content => root.loadChatRequest(content) implicitWidth: ListView.view.width
highlighted: root.highlight && ChatState.chatSession === modelData
onHighlightedChanged: if (highlighted)
list.currentIndex = index
onClicked: content => root.loadChatRequest(content, index)
onRemove: content => root.deleteChatRequest(content) onRemove: content => root.deleteChatRequest(content)
} }
} }
@@ -58,12 +91,12 @@ Item {
id: newChatBtn id: newChatBtn
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Tokens.padding.small
padding: 8
font.pointSize: Math.round(18 * 1.2) font.pointSize: Math.round(18 * 1.2)
icon: "add" icon: "add"
padding: 8 isRound: true
radius: Tokens.rounding.full
onClicked: { onClicked: {
root.newChatRequest(); root.newChatRequest();
@@ -18,45 +18,35 @@ Item {
Component.onCompleted: { Component.onCompleted: {
if (ChatState.inChat) { if (ChatState.inChat) {
stackLoader.item?.push(chatList); stack.push(chatList);
stackLoader.item?.push(chatContent, { stack.push(chatContent, {
"chatData": ChatState.chatSession "chatData": ChatState.chatSession
}); });
} else } else
stackLoader.item?.push(chatList); stack.push(chatList);
} }
Loader { StackView {
id: stackLoader id: stack
active: !ChatState.isWindow || root.width <= (ChatState.screen.width / 4)
anchors.fill: parent anchors.fill: parent
}
sourceComponent: StackView { IconButton {
id: stack anchors.left: parent.left
anchors.bottom: parent.bottom
icon: "open_in_new"
anchors.margins: Tokens.padding.small
isRound: true
padding: 8
font.pointSize: Math.round(18 * 1.2)
visible: !ChatState.isWindow
onClicked: {
Detach.create();
Visibilities.getForActive().sidebar = false;
} }
} }
Loader {
id: sidebarViewLoader
active: ChatState.isWindow && root.width > (ChatState.screen.width / 4)
anchors.fill: parent
sourceComponent: SidebarView {}
}
// IconButton {
// anchors.right: parent.right
// icon: "close"
// visible: !ChatState.isWindow
//
// onClicked: {
// Detach.create();
// Visibilities.getForActive().sidebar = false;
// }
// }
Component { Component {
id: chatList id: chatList
@@ -68,8 +58,8 @@ Item {
onDeleteChatRequest: chat => { onDeleteChatRequest: chat => {
Chat.chats.remove(chat); Chat.chats.remove(chat);
} }
onLoadChatRequest: chat => { onLoadChatRequest: (chat, index) => {
stackLoader.item?.push(chatContent, { stack.push(chatContent, {
"chatData": chat "chatData": chat
}); });
ChatState.inChat = true; ChatState.inChat = true;
@@ -77,7 +67,7 @@ Item {
} }
onNewChatRequest: { onNewChatRequest: {
const data = Chat.chats.insert(); const data = Chat.chats.insert();
stackLoader.item?.push(chatContent, { stack.push(chatContent, {
"chatData": data "chatData": data
}); });
ChatState.inChat = true; ChatState.inChat = true;
@@ -91,7 +81,7 @@ Item {
ChatContent { ChatContent {
onRequestClose: { onRequestClose: {
stackLoader.item?.pop(); stack.pop();
ChatState.inChat = false; ChatState.inChat = false;
} }
} }
@@ -8,6 +8,8 @@ Singleton {
id: root id: root
property ChatSession chatSession property ChatSession chatSession
readonly property string title: chatSession ? chatSession.title : "Chat"
property int currentIdx: -1
property bool inChat: false property bool inChat: false
property bool isWindow: false property bool isWindow: false
property ShellScreen screen property ShellScreen screen
@@ -0,0 +1,132 @@
import QtQuick
import ZShell.Llm
import ZShell.Config
import qs.Components
import qs.Services
import qs.Modules.Notifications.Sidebar.Chat
Item {
id: root
property int animOff
property Item currentItem
property bool loading: false
property int lastIdx: -1
readonly property Component conversationComp: ChatContent {}
function loadConversation(chat): void {
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
if (!chat)
return;
root.loading = true;
const incubator = root.conversationComp.incubateObject(container, {
chatData: chat
});
const attach = () => {
incubator.object.anchors.fill = container;
currentItem = incubator.object;
root.loading = false;
enterAnim.start();
};
if (incubator.status === Component.Ready)
attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
Item {
id: container
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer"
Component.onCompleted: {
if (ChatState.chatSession)
root.loadConversation(ChatState.chatSession);
}
}
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.size.extraLarge * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
Connections {
function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
target: ChatState
}
SequentialAnimation {
id: exitAnim
Anim {
property: "opacity"
target: container
to: 0
type: Anim.DefaultEffects
}
ScriptAction {
script: root.loadConversation(ChatState.chatSession)
}
}
SequentialAnimation {
id: enterAnim
PropertyAction {
property: "topMargin"
target: container.anchors
value: root.animOff
}
PropertyAction {
property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
}
@@ -5,28 +5,28 @@ import qs.Components
import qs.Services import qs.Services
ColumnLayout { ColumnLayout {
id: root id: root
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
} }
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3outlineVariant color: Colors.tPalette.m3outlineVariant
font.pointSize: 36 font.pointSize: 36
text: "chat" text: "chat"
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3onSurfaceVariant color: Colors.tPalette.m3onSurfaceVariant
text: qsTr("No messages yet") text: qsTr("Send a message to get started")
} }
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
} }
} }
@@ -11,7 +11,6 @@ Item {
property color mathColor: Colors.palette.m3onSurface property color mathColor: Colors.palette.m3onSurface
property real fontSize: Tokens.font.size.large property real fontSize: Tokens.font.size.large
implicitWidth: Math.max(fallbackText.implicitWidth, root.width)
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2 implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
LlmMathText { LlmMathText {
@@ -17,7 +17,8 @@ MouseArea {
required property ChatMessage modelData required property ChatMessage modelData
property bool reasoningExpanded: false property bool reasoningExpanded: false
readonly property var blocks: blockify(current.segments) readonly property var blocks: blockify(current.segments)
property real savedContentY: -1 property real savedOffset: -1
property bool stickActive: false
function handleReasoningToggle(expanded: bool): void { function handleReasoningToggle(expanded: bool): void {
const view = root.ListView.view; const view = root.ListView.view;
@@ -25,14 +26,16 @@ MouseArea {
return; return;
if (expanded) { if (expanded) {
root.savedContentY = view.contentY; root.savedOffset = view.contentY - root.y;
root.reasoningExpanded = true; root.reasoningExpanded = true;
} else { } else {
root.reasoningExpanded = false; root.reasoningExpanded = false;
if (root.savedContentY !== -1) { root.stickActive = false;
restoreAnim.to = root.savedContentY; settleTimer.stop();
if (!isNaN(root.savedOffset)) {
restoreAnim.to = root.y + root.savedOffset;
restoreAnim.restart(); restoreAnim.restart();
root.savedContentY = -1; root.savedOffset = NaN;
} }
} }
} }
@@ -69,6 +72,30 @@ MouseArea {
preventStealing: false preventStealing: false
implicitHeight: layout.implicitHeight + Tokens.padding.medium implicitHeight: layout.implicitHeight + Tokens.padding.medium
Timer {
id: settleTimer
interval: 16
onTriggered: {
root.stickActive = false;
}
}
onImplicitHeightChanged: {
if (root.reasoningExpanded) {
root.stickActive = true;
settleTimer.restart();
const view = root.ListView.view;
if (!view)
return;
const bottomEdge = root.y + root.height + Tokens.padding.large * 2;
const viewportBottom = view.contentY + view.height;
if (bottomEdge > viewportBottom)
view.contentY = bottomEdge - view.height;
}
}
Behavior on current { Behavior on current {
id: changeAnim id: changeAnim
@@ -119,14 +146,6 @@ MouseArea {
} }
} }
Binding {
property: "contentY"
restoreMode: Binding.RestoreNone
target: root.ListView.view
value: root.y + root.height + Tokens.padding.large * 2 - root.ListView.view.height
when: root.reasoningExpanded && root.ListView.view && (root.y + root.height + Tokens.padding.large * 2 > root.ListView.view.contentY + root.ListView.view.height)
}
Anim { Anim {
id: restoreAnim id: restoreAnim
@@ -31,7 +31,7 @@ Singleton {
minimumSize.height: Config.sidebar.sizes.width minimumSize.height: Config.sidebar.sizes.width
minimumSize.width: Config.sidebar.sizes.width minimumSize.width: Config.sidebar.sizes.width
surfaceFormat.opaque: false surfaceFormat.opaque: false
title: qsTr("ZShell - Chat") title: qsTr("ZShell - %1").arg(ChatState.title)
Component.onCompleted: ChatState.screen = screen Component.onCompleted: ChatState.screen = screen
onVisibleChanged: { onVisibleChanged: {
@@ -41,7 +41,7 @@ Singleton {
} }
} }
ChatPanel { SidebarView {
id: chat id: chat
anchors.fill: parent anchors.fill: parent
@@ -2,90 +2,109 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services import qs.Services
Item { RowLayout {
id: root id: root
property int breakpoint: 700 property int breakpoint: 700
property alias conversationModel: sidebar.model property alias conversationModel: sidebar.model
property var currentConversation: null // whatever model item is "open"
readonly property bool isWide: root.width >= root.breakpoint readonly property bool isWide: root.width >= root.breakpoint
property bool narrowShowsSidebar: true property bool narrowShowsSidebar: true
function closeConversation() { function openConversation(conv: ChatSession, index: int): void {
narrowShowsSidebar = true;
}
function openConversation(conv) {
currentConversation = conv;
narrowShowsSidebar = false; // in narrow mode, jump to the chat
} }
ChatList { ChatList {
id: sidebar id: sidebar
anchors.bottom: parent.bottom Layout.fillHeight: true
anchors.left: parent.left Layout.margins: Tokens.padding.medium
anchors.right: root.isWide ? undefined : parent.right Layout.rightMargin: Tokens.spacing.medium
anchors.top: parent.top Layout.maximumWidth: Config.sidebar.sizes.width
implicitWidth: 20 Layout.preferredWidth: root.width / 4
list.currentIndex: -1 Layout.minimumWidth: Config.sidebar.sizes.width / 2
list.highlightFollowsCurrentItem: false highlight: true
Behavior on anchors.right {
AnchorAnim {}
}
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {}
} }
list.highlight: CustomRect {
color: Colors.palette.m3primary
implicitHeight: sidebar.list.currentItem?.implicitHeight ?? 0
implicitWidth: sidebar.list.width
radius: Tokens.rounding.medium
x: sidebar.list.currentItem?.chat.x ?? 0
y: sidebar.list.currentItem?.y ?? 0
Behavior on y {
Anim {
duration: Tokens.anim.durations.small
easing: Tokens.anim.expressiveDefaultEffects
}
}
}
model: ScriptModel { model: ScriptModel {
values: Chat.chats.values values: Chat.chats.values
} }
anchors.onRightChanged: {
if (anchors.right === undefined)
implicitWidth = 20;
}
onLoadChatRequest: (chat, index) => { onLoadChatRequest: (chat, index) => {
sidebar.list.currentIndex = index; ChatState.currentIdx = index;
root.openConversation(chat); ChatState.chatSession = chat;
}
onDeleteChatRequest: chat => {
Chat.chats.remove(chat);
ChatState.currentIdx = -1;
ChatState.chatSession = null;
}
onNewChatRequest: {
const data = Chat.chats.insert();
ChatState.currentIdx = 0;
ChatState.chatSession = data;
} }
} }
CustomClippingWrapperRect { Item {
anchors.bottom: parent.bottom Layout.margins: Tokens.padding.extraLarge
anchors.left: sidebar.right Layout.leftMargin: Tokens.spacing.extraLarge
anchors.right: parent.right Layout.topMargin: Tokens.padding.large
anchors.top: parent.top Layout.preferredWidth: Config.sidebar.sizes.width * 2
child: conversationView Layout.fillWidth: true
Layout.fillHeight: true
ChatHost {
id: convHost
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.min(parent.width, 800)
clip: true
onImplicitWidthChanged: console.log(implicitWidth)
}
ColumnLayout {
id: noChatLayout
anchors.centerIn: parent
opacity: ChatState.chatSession ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {}
}
MaterialIcon {
Layout.alignment: Qt.AlignCenter
text: "chat_bubble_off"
font.pointSize: Tokens.font.size.extraLarge
color: Colors.tPalette.m3onSurfaceVariant
}
CustomText {
Layout.alignment: Qt.AlignCenter
text: qsTr("Start or create a chat")
color: Colors.tPalette.m3onSurfaceVariant
}
}
} }
ChatContent { Component {
id: conversationView id: conversationView
anchors.fill: root ChatContent {}
anchors.leftMargin: root.isWide ? Config.sidebar.sizes.width / 2 : 0
chatData: root.currentConversation
} }
} }
@@ -178,9 +178,6 @@ Item {
sourceComponent: MouseArea { sourceComponent: MouseArea {
onWheel: event => event.accepted = true onWheel: event => event.accepted = true
preventStealing: true preventStealing: true
acceptedButtons: Qt.NoButton
cursorShape: undefined
enabled: dialogContent.opacity > 0
clip: true clip: true
implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2 implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
+2 -2
View File
@@ -16,7 +16,7 @@ ConnectedRect {
property string iconText: "open_in_new" property string iconText: "open_in_new"
property int verticalPadding: Tokens.padding.small property int verticalPadding: Tokens.padding.small
signal clicked signal clicked(event: MouseEvent)
signal toggled(checked: bool) signal toggled(checked: bool)
Layout.fillWidth: true Layout.fillWidth: true
@@ -56,7 +56,7 @@ ConnectedRect {
} }
StateLayer { StateLayer {
onClicked: root.clicked() onClicked: e => root.clicked(e)
} }
CustomSwitch { CustomSwitch {