refractor

This commit is contained in:
end-4 2025-05-08 14:46:21 +02:00
parent 4a87cf5c8b
commit 706fd5cab8
3 changed files with 282 additions and 235 deletions

View file

@ -241,241 +241,12 @@ Rectangle {
delegate: Loader {
Layout.fillWidth: true
property var segment: modelData
sourceComponent: modelData.type === "code" ? codeBlockComponent : textBlockComponent
}
}
}
Component { // Text block
id: textBlockComponent
TextArea {
Layout.fillWidth: true
readOnly: !root.editing
selectByMouse: root.enableMouseSelection || root.editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.reading
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.m3colors.m3secondaryContainer
wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
textFormat: root.renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
text: messageData.thinking ? qsTr("Waiting for response...") : segment.content
onTextChanged: {
segment.content = text
}
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
messageText.copy()
event.accepted = true
}
}
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
}
MouseArea { // Pointing hand for links
anchors.fill: parent
acceptedButtons: Qt.NoButton // Only for hover
hoverEnabled: true
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
(root.enableMouseSelection || root.editing) ? Qt.IBeamCursor : Qt.ArrowCursor
}
}
}
Component { // Code block
id: codeBlockComponent
ColumnLayout {
spacing: codeBlockComponentSpacing
anchors.left: parent.left
anchors.right: parent.right
Rectangle { // Code background
Layout.fillWidth: true
topLeftRadius: codeBlockBackgroundRounding
topRightRadius: codeBlockBackgroundRounding
bottomLeftRadius: Appearance.rounding.unsharpen
bottomRightRadius: Appearance.rounding.unsharpen
color: Appearance.m3colors.m3surfaceContainerHighest
implicitHeight: codeBlockTitleBarRowLayout.implicitHeight + codeBlockHeaderPadding * 2
RowLayout { // Language and buttons
id: codeBlockTitleBarRowLayout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: codeBlockHeaderPadding
anchors.rightMargin: codeBlockHeaderPadding
spacing: 5
StyledText {
id: codeBlockLanguage
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: false
Layout.topMargin: 7
Layout.bottomMargin: 7
Layout.leftMargin: 10
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2
text: segment.lang ? Repository.definitionForName(segment.lang).name : "plain"
}
Item { Layout.fillWidth: true }
AiMessageControlButton {
id: copyCodeButton
buttonIcon: "content_copy"
onClicked: {
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segment.content)}'`)
}
StyledToolTip {
content: qsTr("Copy code")
}
}
}
}
RowLayout { // Line numbers and code
spacing: codeBlockComponentSpacing
Rectangle { // Line numbers
implicitWidth: 40
Layout.fillHeight: true
Layout.fillWidth: false
topLeftRadius: Appearance.rounding.unsharpen
bottomLeftRadius: codeBlockBackgroundRounding
topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: Appearance.rounding.unsharpen
color: Appearance.colors.colLayer2
ColumnLayout {
id: lineNumberColumnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Repeater {
model: codeTextArea.text.split("\n").length
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignRight
text: index + 1
}
}
}
}
Rectangle { // Code background
Layout.fillWidth: true
topLeftRadius: Appearance.rounding.unsharpen
bottomLeftRadius: Appearance.rounding.unsharpen
topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: codeBlockBackgroundRounding
color: Appearance.colors.colLayer2
implicitHeight: codeTextArea.implicitHeight
ScrollView {
id: codeScrollView
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: parent.width
implicitHeight: codeTextArea.implicitHeight + 1
contentWidth: codeTextArea.width - 1
// contentHeight: codeTextArea.contentHeight
clip: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal: ScrollBar {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
padding: 5
policy: ScrollBar.AsNeeded
opacity: visualSize == 1 ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
contentItem: Rectangle {
implicitHeight: 6
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2Active
}
}
TextArea { // Code
id: codeTextArea
Layout.fillWidth: true
readOnly: !root.editing
selectByMouse: root.enableMouseSelection || root.editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.monospace
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.m3colors.m3secondaryContainer
// wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
text: segment.content
onTextChanged: {
segment.content = text
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Tab) {
// Insert 4 spaces at cursor
const cursor = codeTextArea.cursorPosition;
codeTextArea.insert(cursor, " ");
codeTextArea.cursorPosition = cursor + 4;
event.accepted = true;
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
messageText.copy();
event.accepted = true;
}
}
SyntaxHighlighter {
id: highlighter
textEdit: codeTextArea
repository: Repository
definition: Repository.definitionForName(segment.lang || "plaintext")
// definition: Repository.definitionForName("cpp")
theme: Appearance.syntaxHighlightingTheme
}
}
}
// MouseArea to block scrolling
MouseArea {
id: codeBlockMouseArea
anchors.fill: parent
acceptedButtons: root.editing ? Qt.NoButton : Qt.LeftButton
cursorShape: (root.enableMouseSelection || root.editing) ? Qt.IBeamCursor : Qt.ArrowCursor
onWheel: (event) => {
event.accepted = false
}
}
}
property var messageData: root.messageData
property var editing: root.editing
property var renderMarkdown: root.renderMarkdown
property var enableMouseSelection: root.enableMouseSelection
source: modelData.type === "code" ? "MessageCodeBlock.qml" : "MessageTextBlock.qml"
}
}
}

View file

@ -0,0 +1,213 @@
pragma ComponentBehavior: Bound
import "root:/"
import "root:/services"
import "root:/modules/common/"
import "root:/modules/common/widgets"
import "../"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
import org.kde.syntaxhighlighting
ColumnLayout {
// These are needed on the parent loader
property bool editing: parent.editing ?? false
property bool renderMarkdown: parent.renderMarkdown ?? true
property bool enableMouseSelection: parent.enableMouseSelection ?? false
property var segment: parent.segment ?? {}
property var messageData: parent.messageData ?? {}
spacing: codeBlockComponentSpacing
anchors.left: parent.left
anchors.right: parent.right
Rectangle { // Code background
Layout.fillWidth: true
topLeftRadius: codeBlockBackgroundRounding
topRightRadius: codeBlockBackgroundRounding
bottomLeftRadius: Appearance.rounding.unsharpen
bottomRightRadius: Appearance.rounding.unsharpen
color: Appearance.m3colors.m3surfaceContainerHighest
implicitHeight: codeBlockTitleBarRowLayout.implicitHeight + codeBlockHeaderPadding * 2
RowLayout { // Language and buttons
id: codeBlockTitleBarRowLayout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: codeBlockHeaderPadding
anchors.rightMargin: codeBlockHeaderPadding
spacing: 5
StyledText {
id: codeBlockLanguage
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: false
Layout.topMargin: 7
Layout.bottomMargin: 7
Layout.leftMargin: 10
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2
text: segment.lang ? Repository.definitionForName(segment.lang).name : "plain"
}
Item { Layout.fillWidth: true }
AiMessageControlButton {
id: copyCodeButton
buttonIcon: "content_copy"
onClicked: {
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(segment.content)}'`)
}
StyledToolTip {
content: qsTr("Copy code")
}
}
}
}
RowLayout { // Line numbers and code
spacing: codeBlockComponentSpacing
Rectangle { // Line numbers
implicitWidth: 40
Layout.fillHeight: true
Layout.fillWidth: false
topLeftRadius: Appearance.rounding.unsharpen
bottomLeftRadius: codeBlockBackgroundRounding
topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: Appearance.rounding.unsharpen
color: Appearance.colors.colLayer2
ColumnLayout {
id: lineNumberColumnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Repeater {
model: codeTextArea.text.split("\n").length
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignRight
text: index + 1
}
}
}
}
Rectangle { // Code background
Layout.fillWidth: true
topLeftRadius: Appearance.rounding.unsharpen
bottomLeftRadius: Appearance.rounding.unsharpen
topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: codeBlockBackgroundRounding
color: Appearance.colors.colLayer2
implicitHeight: codeTextArea.implicitHeight
ScrollView {
id: codeScrollView
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: parent.width
implicitHeight: codeTextArea.implicitHeight + 1
contentWidth: codeTextArea.width - 1
// contentHeight: codeTextArea.contentHeight
clip: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal: ScrollBar {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
padding: 5
policy: ScrollBar.AsNeeded
opacity: visualSize == 1 ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
contentItem: Rectangle {
implicitHeight: 6
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2Active
}
}
TextArea { // Code
id: codeTextArea
Layout.fillWidth: true
readOnly: !editing
selectByMouse: enableMouseSelection || editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.monospace
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.m3colors.m3secondaryContainer
// wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
text: segment.content
onTextChanged: {
segment.content = text
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Tab) {
// Insert 4 spaces at cursor
const cursor = codeTextArea.cursorPosition;
codeTextArea.insert(cursor, " ");
codeTextArea.cursorPosition = cursor + 4;
event.accepted = true;
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
messageText.copy();
event.accepted = true;
}
}
SyntaxHighlighter {
id: highlighter
textEdit: codeTextArea
repository: Repository
definition: Repository.definitionForName(segment.lang || "plaintext")
// definition: Repository.definitionForName("cpp")
theme: Appearance.syntaxHighlightingTheme
}
}
}
// MouseArea to block scrolling
MouseArea {
id: codeBlockMouseArea
anchors.fill: parent
acceptedButtons: editing ? Qt.NoButton : Qt.LeftButton
cursorShape: (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
onWheel: (event) => {
event.accepted = false
}
}
}
}
}

View file

@ -0,0 +1,63 @@
pragma ComponentBehavior: Bound
import "root:/"
import "root:/services"
import "root:/modules/common/"
import "root:/modules/common/widgets"
import "../"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
TextArea {
// These are needed on the parent loader
property bool editing: parent.editing ?? false
property bool renderMarkdown: parent.renderMarkdown ?? true
property bool enableMouseSelection: parent.enableMouseSelection ?? false
property var segment: parent.segment ?? {}
property var messageData: parent.messageData ?? {}
Layout.fillWidth: true
readOnly: !editing
selectByMouse: enableMouseSelection || editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.reading
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.m3colors.m3secondaryContainer
wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
text: messageData.thinking ? qsTr("Waiting for response...") : segment.content
onTextChanged: {
segment.content = text
}
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
messageText.copy()
event.accepted = true
}
}
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
}
MouseArea { // Pointing hand for links
anchors.fill: parent
acceptedButtons: Qt.NoButton // Only for hover
hoverEnabled: true
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
(enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
}
}