From d707dd3a5687add47802736a359572cb6d7fb3d8 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 17 Jul 2026 09:41:33 +0200 Subject: [PATCH] kf5: fix build error and sync features with KF6 - kate: use KTextEditor::MovingInterface for newMovingRange() on KF5 - gui: rewrite main-kf5.qml with tiling panes, session graph, per-pane chat model, drag-to-swap, sloppy focus (matching KF6 ChatPane.qml) --- gui/main-kf5.qml | 821 ++++++++++++++++++++++++++++++++------------ kate/ollie_kate.cpp | 9 +- 2 files changed, 614 insertions(+), 216 deletions(-) diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index ce61a41..8ebbb95 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -1,6 +1,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 import "emojis.js" as Emojis ApplicationWindow { @@ -190,6 +191,206 @@ ApplicationWindow { dbus.setConfig(dbus.activeSessionId, "cwd", path) } + // --- Session Graph Window --- + Window { + id: graphWindow + title: "Ollie \u2014 Session Topology" + width: 700 + height: 500 + visible: false + color: palette.window + + Item { + id: graphRoot + anchors.fill: parent + clip: true + + property var nodePositions: ({}) + property real panX: 0 + property real panY: 0 + property real zoom: 1.0 + + function layoutNodes() { + var positions = {} + var count = dbus.sessions.length + if (count === 0) { nodePositions = positions; graphCanvas.requestPaint(); return } + + var cx = width / 2 + var cy = height / 2 + var radius = Math.min(width, height) * 0.35 + + if (count === 1) { + positions[dbus.sessions[0].id] = {x: cx, y: cy} + } else { + for (var i = 0; i < count; i++) { + var angle = (2 * Math.PI * i / count) - Math.PI / 2 + positions[dbus.sessions[i].id] = { + x: cx + radius * Math.cos(angle), + y: cy + radius * Math.sin(angle) + } + } + } + nodePositions = positions + graphCanvas.requestPaint() + } + + Connections { + target: dbus + function onSessionsChanged() { graphRoot.layoutNodes() } + } + onWidthChanged: layoutNodes() + onHeightChanged: layoutNodes() + Component.onCompleted: layoutNodes() + + MouseArea { + anchors.fill: parent + property real lastX: 0 + property real lastY: 0 + onPressed: { lastX = mouse.x; lastY = mouse.y } + onPositionChanged: { + if (pressed) { + graphRoot.panX += (mouse.x - lastX) + graphRoot.panY += (mouse.y - lastY) + lastX = mouse.x + lastY = mouse.y + } + } + onWheel: { + var factor = wheel.angleDelta.y > 0 ? 1.15 : 0.87 + graphRoot.zoom = Math.max(0.3, Math.min(3.0, graphRoot.zoom * factor)) + } + } + + Item { + x: graphRoot.panX + y: graphRoot.panY + width: parent.width + height: parent.height + scale: graphRoot.zoom + transformOrigin: Item.TopLeft + + Canvas { + id: graphCanvas + anchors.fill: parent + onPaint: { + var ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + ctx.strokeStyle = Qt.rgba(0.5, 0.5, 0.5, 0.4) + ctx.lineWidth = 2 + + for (var i = 0; i < dbus.sessions.length; i++) { + var sid = dbus.sessions[i].id + var peers = dbus.peerList(sid) + var fromPos = graphRoot.nodePositions[sid] + if (!fromPos) continue + + for (var j = 0; j < peers.length; j++) { + var toPos = graphRoot.nodePositions[peers[j]] + if (!toPos) continue + + ctx.beginPath() + ctx.moveTo(fromPos.x, fromPos.y) + ctx.lineTo(toPos.x, toPos.y) + ctx.stroke() + + var dx = toPos.x - fromPos.x + var dy = toPos.y - fromPos.y + var len = Math.sqrt(dx * dx + dy * dy) + if (len < 1) continue + var ux = dx / len + var uy = dy / len + var ax = toPos.x - ux * 30 + var ay = toPos.y - uy * 30 + var arrowSize = 8 + ctx.beginPath() + ctx.moveTo(ax, ay) + ctx.lineTo(ax - arrowSize * ux + arrowSize * 0.5 * uy, + ay - arrowSize * uy - arrowSize * 0.5 * ux) + ctx.lineTo(ax - arrowSize * ux - arrowSize * 0.5 * uy, + ay - arrowSize * uy + arrowSize * 0.5 * ux) + ctx.closePath() + ctx.fillStyle = Qt.rgba(0.5, 0.5, 0.5, 0.6) + ctx.fill() + } + } + } + } + + Repeater { + model: dbus.sessions + Rectangle { + id: graphNode + width: 120 + height: 50 + radius: 8 + color: palette.base + border.color: { + var st = modelData.state + if (st === "idle") return "#4caf50" + if (st && st.startsWith("calling")) return "#ff9800" + return "#2196f3" + } + border.width: modelData.id === dbus.activeSessionId ? 3 : 2 + x: { + var pos = graphRoot.nodePositions[modelData.id] + return pos ? pos.x - width / 2 : 0 + } + y: { + var pos = graphRoot.nodePositions[modelData.id] + return pos ? pos.y - height / 2 : 0 + } + + SequentialAnimation on opacity { + running: modelData.state !== "idle" + loops: Animation.Infinite + NumberAnimation { to: 0.7; duration: 600; easing.type: Easing.InOutSine } + NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutSine } + } + + Column { + anchors.centerIn: parent + spacing: 2 + Label { + text: modelData.id + font.bold: true + font.pixelSize: 11 + anchors.horizontalCenter: parent.horizontalCenter + elide: Text.ElideRight + width: graphNode.width - 12 + horizontalAlignment: Text.AlignHCenter + } + Label { + text: modelData.state || "idle" + font.pixelSize: 9 + opacity: 0.7 + anchors.horizontalCenter: parent.horizontalCenter + color: { + var st = modelData.state + if (st === "idle") return "#4caf50" + if (st && st.startsWith("calling")) return "#ff9800" + return "#2196f3" + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: dbus.activeSessionId = modelData.id + cursorShape: Qt.PointingHandCursor + } + } + } + } + + Label { + anchors.centerIn: parent + text: "No active sessions" + opacity: 0.5 + visible: dbus.sessions.length === 0 + } + } + } + SplitView { anchors.fill: parent orientation: Qt.Horizontal @@ -228,6 +429,13 @@ ApplicationWindow { ToolTip.text: "Settings" ToolTip.visible: hovered } + + ToolButton { + icon.name: "network-workgroup" + onClicked: graphWindow.visible = !graphWindow.visible + ToolTip.text: "Session topology" + ToolTip.visible: hovered + } } // Session list @@ -340,181 +548,369 @@ ApplicationWindow { } } - // --- Main content --- + // --- Main content: tiling pane area --- Pane { + id: mainContent SplitView.fillWidth: true padding: 0 + property int focusedPaneIndex: 0 + property var panes: [] + property int paneCount: 0 + + // BSP tree: each node is {pane: item} or {split: 'h'|'v', left: node, right: node} + property var bspTree: null + property real gap: 2 + + function layoutTree(node, x, y, w, h) { + if (!node) return + if (node.pane) { + node.pane.x = x + node.pane.y = y + node.pane.width = w + node.pane.height = h + } else if (node.split) { + if (node.split === 'h') { + var lw = (w - gap) / 2 + layoutTree(node.left, x, y, lw, h) + layoutTree(node.right, x + lw + gap, y, w - lw - gap, h) + } else { + var th = (h - gap) / 2 + layoutTree(node.left, x, y, w, th) + layoutTree(node.right, x, y + th + gap, w, h - th - gap) + } + } + } + + function relayout() { + if (bspTree) + layoutTree(bspTree, 0, 0, width, height) + } + + function splitNode(tree, targetPane, newPane, direction) { + if (!tree) return null + if (tree.pane === targetPane) { + return {split: direction, left: {pane: targetPane}, right: {pane: newPane}} + } + if (tree.split) { + var newLeft = splitNode(tree.left, targetPane, newPane, direction) + if (newLeft) { tree.left = newLeft; return tree } + var newRight = splitNode(tree.right, targetPane, newPane, direction) + if (newRight) { tree.right = newRight; return tree } + } + return null + } + + function removeNode(tree, targetPane) { + if (!tree) return null + if (tree.pane === targetPane) return null + if (tree.split) { + if (tree.left && tree.left.pane === targetPane) return tree.right + if (tree.right && tree.right.pane === targetPane) return tree.left + var newLeft = removeNode(tree.left, targetPane) + if (newLeft !== tree.left) { tree.left = newLeft; return tree } + var newRight = removeNode(tree.right, targetPane) + if (newRight !== tree.right) { tree.right = newRight; return tree } + } + return tree + } + + onWidthChanged: relayout() + onHeightChanged: relayout() + onPaneCountChanged: relayout() + + // Container for panes (absolute positioning) + Item { + id: paneContainer + anchors.fill: parent + } + + Component.onCompleted: { + var pane = paneCmp.createObject(paneContainer, {sessionId: dbus.activeSessionId}) + panes = [pane] + paneCount = 1 + bspTree = {pane: pane} + relayout() + } + } + } + + // --- Split pane shortcuts --- + Shortcut { + sequence: "Ctrl+\\" + onActivated: { + var focusedPane = mainContent.panes[mainContent.focusedPaneIndex] + if (!focusedPane) return + + var pane = paneCmp.createObject(paneContainer, { + sessionId: dbus.activeSessionId, + isFocused: false + }) + if (!pane) return + + var dir = focusedPane.width >= focusedPane.height ? 'h' : 'v' + mainContent.bspTree = mainContent.splitNode(mainContent.bspTree, focusedPane, pane, dir) + + var panes = mainContent.panes + var insertAt = mainContent.focusedPaneIndex + 1 + panes.splice(insertAt, 0, pane) + mainContent.panes = panes + mainContent.paneCount = panes.length + mainContent.focusedPaneIndex = insertAt + mainContent.relayout() + } + } + + Shortcut { + sequence: "Ctrl+W" + onActivated: { + if (mainContent.panes.length <= 1) return + var idx = mainContent.focusedPaneIndex + if (idx < 0 || idx >= mainContent.panes.length) + idx = mainContent.panes.length - 1 + var pane = mainContent.panes[idx] + + mainContent.bspTree = mainContent.removeNode(mainContent.bspTree, pane) + + var panes = mainContent.panes + panes.splice(idx, 1) + mainContent.panes = panes + mainContent.paneCount = panes.length + pane.destroy() + mainContent.focusedPaneIndex = Math.min(idx, panes.length - 1) + mainContent.relayout() + } + } + + // Handle pane swap: find target pane at drop coordinates + function handlePaneSwap(sourcePane, rootX, rootY) { + for (var i = 0; i < mainContent.panes.length; i++) { + var p = mainContent.panes[i] + if (p === sourcePane) continue + var localPt = p.mapFromItem(null, rootX, rootY) + if (localPt.x >= 0 && localPt.x <= p.width && + localPt.y >= 0 && localPt.y <= p.height) { + var tmp = sourcePane.sessionId + sourcePane.sessionId = p.sessionId + p.sessionId = tmp + return + } + } + } + + // Sidebar click updates focused pane's session + Connections { + target: dbus + function onActiveSessionIdChanged() { + var idx = mainContent.focusedPaneIndex + if (idx >= 0 && idx < mainContent.panes.length) { + mainContent.panes[idx].sessionId = dbus.activeSessionId + } + } + } + + // --- Inline ChatPane Component --- + Component { + id: paneCmp + + Item { + id: paneRoot + + property string sessionId: "" + property bool isFocused: false + + signal startedDrag() + signal endedDrag(real globalX, real globalY) + + // Pane-local chat model + property var paneModel: chatModel // fallback; replaced on creation + property string paneState: "idle" + property string paneCwd: "" + property int chatOffset: 0 + + Component.onCompleted: { + // Create a dedicated ChatBlockModel for this pane + paneModel = Qt.createQmlObject( + 'import org.ollie.gui 1.0; ChatBlockModel {}', paneRoot, "paneModel") + if (sessionId !== "") refreshPaneChat() + } + + function refreshPaneChat() { + paneModel.rawText = "" + chatOffset = 0 + paneState = "idle" + paneCwd = "" + if (sessionId !== "") { + var chat = dbus.getChat(sessionId, 0) + if (chat) { + paneModel.rawText = chat.text + chatOffset = chat.offset + } + var cfg = dbus.getConfig(sessionId) + if (cfg && cfg.cwd) paneCwd = cfg.cwd + var st = dbus.getState(sessionId) + if (st) paneState = st + } + } + + onSessionIdChanged: refreshPaneChat() + + Connections { + target: dbus + function onChatAppended(sid, offset, text) { + if (sid !== paneRoot.sessionId) return + if (offset === paneRoot.chatOffset) { + paneRoot.paneModel.rawText = paneRoot.paneModel.rawText + text + paneRoot.chatOffset += text.length + } else { + var chat = dbus.getChat(paneRoot.sessionId, 0) + if (chat) { + paneRoot.paneModel.rawText = chat.text + paneRoot.chatOffset = chat.offset + } + } + } + function onSessionStateUpdated(sid, newState) { + if (sid === paneRoot.sessionId) + paneRoot.paneState = newState + } + } + + // Visual focus indicator + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: paneRoot.isFocused ? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.4) : "transparent" + border.width: 1 + z: 100 + } + ColumnLayout { anchors.fill: parent + anchors.margins: 2 spacing: 0 - // Header bar - RowLayout { + // Header (drag handle) + Rectangle { + id: paneHeader Layout.fillWidth: true - Layout.margins: 8 + implicitHeight: headerRow.implicitHeight + 12 + color: "transparent" - Rectangle { - width: 10 - height: 10 - radius: 5 - color: { - if (dbus.sessions.length === 0) return "gray" - var state = dbus.activeState - if (state === "idle") return "#4caf50" - if (state.startsWith("calling")) return "#ff9800" - return "#2196f3" - } - } + MouseArea { + id: paneHeaderDrag + anchors.fill: parent + property bool dragging: false + cursorShape: dragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor - Label { - text: dbus.activeCwd || "No session selected" - Layout.fillWidth: true - elide: Text.ElideMiddle - font.bold: true - - MouseArea { - anchors.fill: parent - onDoubleClicked: { - if (dbus.activeSessionId !== "") { - root.openCwdDialog() - } + onPositionChanged: { + if (pressed && !dragging) { + dragging = true + paneRoot.startedDrag() } } - } - - Label { - text: { - var parts = [] - if (dbus.activeBackend) parts.push(dbus.activeBackend) - if (dbus.activeModel) parts.push(dbus.activeModel) - return parts.join(" / ") || "" + onReleased: { + if (dragging) { + var rootPt = paneHeaderDrag.mapToItem(null, mouse.x, mouse.y) + paneRoot.endedDrag(rootPt.x, rootPt.y) + } + dragging = false } - opacity: 0.5 - font.pixelSize: Qt.application.font.pixelSize * 0.85 - visible: text !== "" } - Label { - text: dbus.activeState - opacity: 0.6 + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: 6 + + Rectangle { + width: 8; height: 8; radius: 4 + color: { + if (paneRoot.paneState === "idle") return "#4caf50" + if (paneRoot.paneState.startsWith("calling")) return "#ff9800" + return "#2196f3" + } + } + + Label { + text: paneRoot.paneCwd || paneRoot.sessionId || "No session" + Layout.fillWidth: true + elide: Text.ElideMiddle + font.bold: true + font.pixelSize: Qt.application.font.pixelSize * 0.9 + } + + Label { + text: paneRoot.paneState + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.8 + } } } - // Separator - Rectangle { - Layout.fillWidth: true - height: 1 - color: palette.mid - } + Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid } - // Chat view — structured blocks + // Chat view Item { Layout.fillWidth: true Layout.fillHeight: true - property bool atBottom: true - Binding { - target: chatModel - property: "rawText" - value: dbus.chatText || "" - } - Flickable { - id: chatFlick + id: paneFlick anchors.fill: parent anchors.margins: 4 clip: true contentWidth: width - contentHeight: chatColumn.implicitHeight + contentHeight: paneColumn.implicitHeight boundsBehavior: Flickable.StopAtBounds - // Guard: true while we're programmatically setting contentY property bool _settingY: false - // Guard: true while content is streaming (height growing) - property bool _contentGrowing: false onContentYChanged: { if (_settingY) return var bottom = contentHeight - height parent.atBottom = (bottom <= 0 || contentY >= bottom - 4) - - // Load earlier messages when scrolled near top - // Only on user scroll — not during content growth or programmatic scroll - if (!_contentGrowing && contentY < 100 && contentHeight > height && !parent.atBottom) { - var oldHeight = contentHeight - if (chatModel.loadEarlier()) { - _settingY = true - Qt.callLater(function() { - chatFlick.contentY += (chatFlick.contentHeight - oldHeight) - chatFlick._settingY = false - }) - } - } - } - - // Timer-based debounce: coalesce all contentHeight changes within - // a single frame (~16ms) to avoid rapid scroll/restore flickering - Timer { - id: scrollTimer - interval: 16 - repeat: false - onTriggered: { - chatFlick._contentGrowing = false - if (chatFlick.parent.atBottom) { - chatFlick._settingY = true - chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height) - chatFlick._settingY = false - } - } } onContentHeightChanged: { if (parent.atBottom) { - _contentGrowing = true - scrollTimer.restart() + _settingY = true + contentY = Math.max(0, contentHeight - height) + _settingY = false } } - // Direct wheel scroll without inertia MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton onWheel: { - var dy = wheel.angleDelta.y - chatFlick._settingY = true - chatFlick.contentY = Math.max(0, - Math.min(chatFlick.contentY - dy, - chatFlick.contentHeight - chatFlick.height)) - chatFlick._settingY = false - // Update atBottom - var bottom = chatFlick.contentHeight - chatFlick.height - chatFlick.parent.atBottom = (bottom <= 0 || chatFlick.contentY >= bottom - 4) - wheel.accepted = true + paneFlick._settingY = true + paneFlick.contentY = Math.max(0, + Math.min(paneFlick.contentY - wheel.angleDelta.y, + paneFlick.contentHeight - paneFlick.height)) + paneFlick._settingY = false + var bottom = paneFlick.contentHeight - paneFlick.height + paneFlick.parent.atBottom = (bottom <= 0 || paneFlick.contentY >= bottom - 4) } } Column { - id: chatColumn - width: chatFlick.width + id: paneColumn + width: paneFlick.width spacing: 8 - // Placeholder Label { - visible: chatRepeater.count === 0 - text: dbus.activeSessionId - ? "(waiting for output...)" - : "Select or create a session" + visible: paneChatRepeater.count === 0 + text: paneRoot.sessionId ? "(waiting...)" : "No session" color: theme.placeholderTextColor } Repeater { - id: chatRepeater - model: chatModel - + id: paneChatRepeater + model: paneRoot.paneModel delegate: Loader { - width: chatColumn.width + width: paneColumn.width property var block: model sourceComponent: { switch (blockType) { @@ -540,7 +936,7 @@ ApplicationWindow { icon.name: "go-down" visible: !parent.atBottom onClicked: { - chatFlick.contentY = Math.max(0, chatFlick.contentHeight - chatFlick.height) + paneFlick.contentY = Math.max(0, paneFlick.contentHeight - paneFlick.height) parent.atBottom = true } ToolTip.text: "Scroll to bottom" @@ -548,68 +944,24 @@ ApplicationWindow { } } - // Separator - Rectangle { - Layout.fillWidth: true - height: 1 - color: palette.mid - } + Rectangle { Layout.fillWidth: true; height: 1; color: palette.mid } // Prompt input RowLayout { Layout.fillWidth: true - Layout.margins: 8 - spacing: 6 + Layout.margins: 6 + spacing: 4 ScrollView { Layout.fillWidth: true Layout.maximumHeight: 100 TextArea { - id: promptInput - placeholderText: dbus.activeSessionId - ? "Message..." - : "No active session" - enabled: dbus.activeSessionId !== "" + id: panePrompt + placeholderText: paneRoot.sessionId ? "Message..." : "No session" + enabled: paneRoot.sessionId !== "" wrapMode: TextEdit.Wrap - Keys.onReturnPressed: { - if (event.modifiers & (Qt.ShiftModifier | Qt.ControlModifier)) { - promptInput.insert(promptInput.cursorPosition, "\n") - } else { - if (text.trim() !== "" && dbus.activeSessionId !== "") { - dbus.submit(dbus.activeSessionId, text.trim()) - text = "" - } - } - event.accepted = true - } - Keys.onPressed: { - if (event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) { - promptInput.insert(promptInput.cursorPosition, "\n") - event.accepted = true - } else if (emojiPopup.visible) { - if (event.key === Qt.Key_Down) { - emojiList.currentIndex = Math.min(emojiList.currentIndex + 1, emojiModel.count - 1) - event.accepted = true - } else if (event.key === Qt.Key_Up) { - emojiList.currentIndex = Math.max(emojiList.currentIndex - 1, 0) - event.accepted = true - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Tab) { - if (emojiModel.count > 0) { - var item = emojiModel.get(emojiList.currentIndex) - applyEmoji(item.emoji, item.code) - event.accepted = true - } - } else if (event.key === Qt.Key_Escape) { - emojiPopup.visible = false - event.accepted = true - } - } - } - Component.onCompleted: forceActiveFocus() - - // Emoji shortcode replacement + completion property var quickEmojis: ({ ";)": "\uD83D\uDE09", ":)": "\uD83D\uDE42", ":D": "\uD83D\uDE03", ":(": "\uD83D\uDE1E", @@ -653,7 +1005,7 @@ ApplicationWindow { if (_replacing) return var pos = cursorPosition - // Quick emoticons (instant replace) + // Quick emoticons for (var len = 2; len <= Math.min(4, pos); len++) { var candidate = text.substring(pos - len, pos) if (quickEmojis[candidate] !== undefined) { @@ -668,7 +1020,7 @@ ApplicationWindow { } } - // Extended :shortcode: completion + // :shortcode: completion popup var q = emojiQuery() if (q) { var matches = Emojis.emojiList.filter(function(e) { @@ -688,27 +1040,58 @@ ApplicationWindow { } } + Keys.onReturnPressed: { + if (emojiPopup.visible && emojiModel.count > 0) { + var item = emojiModel.get(emojiList.currentIndex) + applyEmoji(item.emoji, item.code) + event.accepted = true + return + } + if (event.modifiers & (Qt.ShiftModifier | Qt.ControlModifier)) { + panePrompt.insert(panePrompt.cursorPosition, "\n") + } else { + if (text.trim() !== "" && paneRoot.sessionId !== "") { + dbus.submit(paneRoot.sessionId, text.trim()) + text = "" + } + } + event.accepted = true + } + Keys.onPressed: { + if (event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) { + panePrompt.insert(panePrompt.cursorPosition, "\n") + event.accepted = true + } else if (emojiPopup.visible) { + if (event.key === Qt.Key_Down) { + emojiList.currentIndex = Math.min(emojiList.currentIndex + 1, emojiModel.count - 1) + event.accepted = true + } else if (event.key === Qt.Key_Up) { + emojiList.currentIndex = Math.max(emojiList.currentIndex - 1, 0) + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Tab) { + if (emojiModel.count > 0) { + var eItem = emojiModel.get(emojiList.currentIndex) + applyEmoji(eItem.emoji, eItem.code) + event.accepted = true + } + } else if (event.key === Qt.Key_Escape) { + emojiPopup.visible = false + event.accepted = true + } + } + } } } - ToolButton { - icon.name: "media-playback-stop" - enabled: dbus.activeState !== "idle" && dbus.activeSessionId !== "" - onClicked: dbus.interrupt(dbus.activeSessionId) - ToolTip.text: "Interrupt" - ToolTip.visible: hovered - } - - // Emoji completion popup + // Emoji popup ListModel { id: emojiModel } - Popup { id: emojiPopup y: -height - 4 x: 8 - width: 280 - height: Math.min(emojiList.contentHeight + 12, 240) + width: 260 + height: Math.min(emojiList.contentHeight + 12, 200) padding: 6 visible: false closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -729,42 +1112,59 @@ ApplicationWindow { delegate: ItemDelegate { width: emojiList.width - height: 32 + height: 28 highlighted: index === emojiList.currentIndex - contentItem: RowLayout { spacing: 8 - Label { - text: model.emoji - font.pixelSize: 18 - } - Label { - text: ":" + model.code + ":" - Layout.fillWidth: true - font.family: "monospace" - opacity: 0.8 - } - } - - onClicked: { - promptInput.applyEmoji(model.emoji, model.code) + Label { text: model.emoji; font.pixelSize: 16 } + Label { text: ":" + model.code + ":"; Layout.fillWidth: true; font.family: "monospace"; opacity: 0.8 } } + onClicked: panePrompt.applyEmoji(model.emoji, model.code) } } } + + ToolButton { + icon.name: "media-playback-stop" + enabled: paneRoot.paneState !== "idle" && paneRoot.sessionId !== "" + onClicked: dbus.interrupt(paneRoot.sessionId) + ToolTip.text: "Interrupt" + ToolTip.visible: hovered + } } - // Status bar + // Status Label { Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.bottomMargin: 4 - font.pixelSize: Qt.application.font.pixelSize * 0.85 - opacity: 0.5 - text: { - var id = dbus.activeSessionId - if (!id) return "ollie" - return dbus.activeState + " \u2502 " + id + Layout.leftMargin: 6 + Layout.bottomMargin: 2 + font.pixelSize: Qt.application.font.pixelSize * 0.75 + opacity: 0.4 + text: paneRoot.sessionId || "no session" + } + } + + // Sloppy focus + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onPositionChanged: sloppyTimer.restart() + onExited: sloppyTimer.stop() + } + Timer { + id: sloppyTimer + interval: 100 + onTriggered: { + paneRoot.focus = true + panePrompt.forceActiveFocus() + // Update focused index + for (var i = 0; i < mainContent.panes.length; i++) { + if (mainContent.panes[i] === paneRoot) { + mainContent.focusedPaneIndex = i + break + } } } } @@ -782,7 +1182,7 @@ ApplicationWindow { id: userBubble anchors.top: parent.top anchors.right: parent.right - width: Math.min(userContent.implicitWidth + 24, parent.width * 0.85) + width: Math.max(160, Math.min(userContent.implicitWidth + 24, parent.width * 0.85)) implicitHeight: userContent.implicitHeight + 16 color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.15) radius: 12 @@ -1164,7 +1564,6 @@ ApplicationWindow { anchors.fill: parent spacing: 0 - // Header with language + copy/exec buttons RowLayout { Layout.fillWidth: true Layout.margins: 4 @@ -1196,15 +1595,12 @@ ApplicationWindow { icon.name: "edit-copy" implicitWidth: 32 implicitHeight: 32 - onClicked: { - chatModel.copyToClipboard(subBlock.content) - } + onClicked: chatModel.copyToClipboard(subBlock.content) ToolTip.text: "Copy" ToolTip.visible: hovered } } - // Code content — show plain first, highlight lazily Text { id: codeText Layout.fillWidth: true @@ -1260,15 +1656,12 @@ ApplicationWindow { icon.name: "edit-copy" implicitWidth: 32 implicitHeight: 32 - onClicked: { - chatModel.copyToClipboard(subBlock.content) - } + onClicked: chatModel.copyToClipboard(subBlock.content) ToolTip.text: "Copy" ToolTip.visible: hovered } } - // Diff lines with coloring Column { Layout.fillWidth: true Layout.margins: 8 diff --git a/kate/ollie_kate.cpp b/kate/ollie_kate.cpp index 6b366cb..90ee77c 100644 --- a/kate/ollie_kate.cpp +++ b/kate/ollie_kate.cpp @@ -13,6 +13,7 @@ #include #if KTEXTEDITOR_VERSION_MAJOR < 6 #include +#include #endif #include @@ -1187,7 +1188,11 @@ void OllieKateView::annotateFileReferences(const QString &chatText) QRegularExpression::MultilineOption); auto *doc = view->document(); - auto *movingIface = qobject_cast(doc); +#if KTEXTEDITOR_VERSION_MAJOR < 6 + auto *movingIface = qobject_cast(doc); +#else + auto *movingIface = doc; +#endif if (!movingIface) return; KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); @@ -1209,7 +1214,7 @@ void OllieKateView::annotateFileReferences(const QString &chatText) annotatedLines.insert(line); KTextEditor::Range range(line, 0, line, doc->lineLength(line)); - auto *movingRange = doc->newMovingRange(range); + auto *movingRange = movingIface->newMovingRange(range); movingRange->setAttribute(attr); movingRange->setZDepth(-100.0); // behind text m_annotations.append(movingRange);