This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-kde/gui/main.qml

1017 lines
37 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
ApplicationWindow {
id: root
visible: true
width: 1000
height: 600
title: "Ollie"
font.family: theme.fontFamily || Qt.application.font.family
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
// Apply theme palette to all Qt Quick Controls
palette.active.window: theme.windowColor
palette.active.windowText: theme.windowTextColor
palette.active.base: theme.baseColor
palette.active.text: theme.textColor
palette.active.button: theme.buttonColor
palette.active.buttonText: theme.buttonTextColor
palette.active.highlight: theme.highlightColor
palette.active.highlightedText: theme.highlightedTextColor
palette.active.mid: theme.midColor
palette.active.dark: theme.darkColor
palette.active.light: theme.lightColor
palette.active.shadow: theme.shadowColor
palette.active.link: theme.linkColor
palette.active.toolTipBase: theme.tooltipBaseColor
palette.active.toolTipText: theme.tooltipTextColor
palette.active.placeholderText: theme.placeholderTextColor
palette.active.alternateBase: theme.alternateBaseColor
palette.inactive.window: theme.windowColor
palette.inactive.windowText: theme.windowTextColor
palette.inactive.base: theme.baseColor
palette.inactive.text: theme.textColor
palette.inactive.button: theme.buttonColor
palette.inactive.buttonText: theme.buttonTextColor
palette.inactive.highlight: theme.highlightColor
palette.inactive.highlightedText: theme.highlightedTextColor
palette.inactive.mid: theme.midColor
palette.inactive.dark: theme.darkColor
palette.inactive.light: theme.lightColor
palette.inactive.shadow: theme.shadowColor
palette.inactive.link: theme.linkColor
palette.inactive.toolTipBase: theme.tooltipBaseColor
palette.inactive.toolTipText: theme.tooltipTextColor
palette.inactive.placeholderText: theme.placeholderTextColor
palette.inactive.alternateBase: theme.alternateBaseColor
palette.disabled.window: theme.windowColor
palette.disabled.windowText: theme.midColor
palette.disabled.base: theme.baseColor
palette.disabled.text: theme.midColor
palette.disabled.button: theme.buttonColor
palette.disabled.buttonText: theme.midColor
palette.disabled.highlight: theme.highlightColor
palette.disabled.highlightedText: theme.highlightedTextColor
palette.disabled.mid: theme.midColor
palette.disabled.dark: theme.darkColor
palette.disabled.light: theme.lightColor
palette.disabled.shadow: theme.shadowColor
Shortcut {
sequence: "Escape"
onActivated: {
if (completionPopup.visible) {
completionPopup.close()
} else if (ollie.activeState !== "idle") {
ollie.interrupt()
}
}
}
// --- Slash command definitions ---
readonly property var slashCommands: [
{ name: "model", desc: "Set model" },
{ name: "backend", desc: "Set backend" },
{ name: "agent", desc: "Switch agent" },
{ name: "stop", desc: "Stop current run" },
{ name: "kill", desc: "Kill session" },
{ name: "new", desc: "New session" },
{ name: "compact", desc: "Compact context" },
{ name: "help", desc: "Show help" }
]
// --- Completion popup ---
Popup {
id: completionPopup
x: 80
y: parent.height - 280
width: Math.min(360, parent.width - 160)
height: Math.min(200, visibleMatches * 28 + 32)
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
background: Rectangle {
radius: 6
color: theme.isDark ? "#1e1e1e" : "#ffffff"
border.color: theme.midColor
border.width: 1
}
ListView {
id: completionList
anchors.fill: parent
anchors.margins: 4
model: completionModel
currentIndex: -1
clip: true
delegate: ItemDelegate {
width: completionList.width
height: 28
highlighted: index === completionList.currentIndex
contentItem: RowLayout {
spacing: 8
Label {
text: model.name
font.bold: true
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
color: theme.textColor
}
Item { Layout.fillWidth: true }
Label {
text: model.desc || ""
opacity: 0.5
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.8 : 10
color: theme.textColor
}
}
onClicked: {
completionList.currentIndex = index
acceptCompletion()
}
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Down) {
if (completionList.currentIndex < completionList.count - 1) {
completionList.currentIndex++
event.accepted = true
}
} else if (event.key === Qt.Key_Up) {
if (completionList.currentIndex > 0) {
completionList.currentIndex--
event.accepted = true
}
} else if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
if (completionList.currentIndex >= 0) {
acceptCompletion()
event.accepted = true
}
} else if (event.key === Qt.Key_Escape) {
completionPopup.close()
event.accepted = true
}
}
Keys.onTabPressed: {
if (completionList.currentIndex >= 0) {
acceptCompletion()
event.accepted = true
}
}
}
ListModel {
id: completionModel
}
function open(prefix) {
updateCompletions(prefix)
visible = true
promptInput.forceActiveFocus()
}
function close() {
visible = false
completionModel.clear()
completionList.currentIndex = -1
}
function updateCompletions(prefix) {
completionModel.clear()
var parts = prefix.split(" ")
var cmd = parts[0].toLowerCase()
var arg = parts.length > 1 ? parts.slice(1).join(" ") : ""
// If there's an argument part after a known command, show argument completions
if (arg !== "") {
if (cmd === "model") {
var models = ollie.getAvailableModels(ollie.availableBackends[currentBackendIndex] || "")
for (var i = 0; i < models.length; ++i) {
if (models[i].toLowerCase().indexOf(arg.toLowerCase()) >= 0) {
completionModel.append({ name: models[i], desc: "", isArg: true })
}
}
} else if (cmd === "backend") {
var bks = ollie.availableBackends
for (var j = 0; j < bks.length; ++j) {
if (bks[j].toLowerCase().indexOf(arg.toLowerCase()) >= 0) {
completionModel.append({ name: bks[j], desc: "", isArg: true })
}
}
}
} else {
// Show command completions filtered by prefix
for (var k = 0; k < root.slashCommands.length; ++k) {
var sc = root.slashCommands[k]
if (sc.name.indexOf(cmd) >= 0) {
completionModel.append({ name: sc.name, desc: sc.desc, isArg: false })
}
}
}
if (completionModel.count > 0) {
completionList.currentIndex = 0
visible = true
} else {
visible = false
}
}
function acceptCompletion() {
if (completionList.currentIndex < 0 || completionList.currentIndex >= completionModel.count) return
var item = completionModel.get(completionList.currentIndex)
var oldText = promptInput.text
var slashPos = oldText.lastIndexOf("/")
if (slashPos < 0) return
var beforeSlash = oldText.substring(0, slashPos)
var afterSlash = oldText.substring(slashPos + 1)
var parts = afterSlash.split(" ")
if (item.isArg) {
// Replace the argument part
parts[parts.length - 1] = item.name
promptInput.text = beforeSlash + "/" + parts.join(" ")
} else {
// Replace command with command + space
promptInput.text = beforeSlash + "/" + item.name + " "
}
close()
}
property int visibleMatches: completionModel.count
}
// Deferred scroll timer — fires once after model changes settle
Timer {
id: scrollTimer
interval: 0
repeat: false
onTriggered: chatView.positionViewAtEnd()
}
// Session tree model — flat list with level info
ListModel {
id: sessionTreeModel
}
function buildSessionTree() {
console.log("buildSessionTree: sessions count = " + ollie.sessions.length)
sessionTreeModel.clear()
var sessions = ollie.sessions
for (var i = 0; i < sessions.length; ++i) {
var sess = sessions[i]
var sid = sess.id || ""
console.log(" session[" + i + "] = " + sid)
// Auto-expand active session
var expanded = (sid === ollie.activeSessionId)
// Add session node
sessionTreeModel.append({
type: "session",
id: sid,
level: 0,
expanded: expanded,
state: sess.state || "",
model: sess.model || "",
displayName: sid
})
// Fetch agents and add them
try {
var agents = ollie.getAgents(sid)
console.log(" agents for " + sid + ": " + agents.length)
for (var j = 0; j < agents.length; ++j) {
var aid = agents[j]
sessionTreeModel.append({
type: "agent",
parentId: sid,
agentId: aid,
level: 1,
state: "idle",
displayName: "Agent " + aid
})
}
} catch(e) {
console.log(" error getting agents for " + sid + ": " + e)
}
}
console.log(" total tree items: " + sessionTreeModel.count)
}
function toggleExpand(index) {
var item = sessionTreeModel.get(index)
if (item.type === "session") {
sessionTreeModel.set(index, { expanded: !item.expanded })
}
}
function activateAgent(parentId, agentId) {
ollie.switchAgent(parentId, agentId)
}
Connections {
target: ollie
function onSessionsChanged() {
buildSessionTree()
}
}
Component.onCompleted: {
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
if (log) {
chatModel.appendData(log)
chatModel.trimFront(20)
}
scrollTimer.start()
buildSessionTree()
}
Connections {
target: ollie
function onActiveSessionIdChanged() {
chatModel.reset()
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
if (log) {
chatModel.appendData(log)
chatModel.trimFront(20)
}
scrollTimer.start()
// Expand the active session in the tree
buildSessionTree()
}
function onStreamingDone() {
chatModel.appendData("\n[info]\nsession ended\n")
}
}
// --- Settings Dialog ---
Dialog {
id: settingsDialog
title: "Settings"
anchors.centerIn: parent
width: 380
modal: true
standardButtons: Dialog.Close
contentItem: ColumnLayout {
spacing: 16
Layout.margins: 16
Label {
text: "Color Scheme"
font.bold: true
}
Label {
text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/"
opacity: 0.6
wrapMode: Text.Wrap
Layout.fillWidth: true
font.pixelSize: Qt.application.font.pixelSize * 0.9
}
ColumnLayout {
spacing: 8
RadioButton {
id: systemRadio
text: "Use system theme"
checked: theme.mode === "system"
onClicked: theme.mode = "system"
}
RadioButton {
id: lightRadio
text: "Breeze Light"
checked: theme.mode === "light"
onClicked: theme.mode = "light"
}
RadioButton {
id: darkRadio
text: "Breeze Dark"
checked: theme.mode === "dark"
onClicked: theme.mode = "dark"
}
}
Label {
text: theme.isDark ? "Current: dark" : "Current: light"
opacity: 0.5
font.pixelSize: Qt.application.font.pixelSize * 0.85
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
Layout.topMargin: 8
}
Label {
text: "Font"
font.bold: true
}
GridLayout {
columns: 2
columnSpacing: 12
rowSpacing: 8
Layout.fillWidth: true
Label { text: "Family:" }
ComboBox {
id: fontFamilyCombo
Layout.fillWidth: true
model: theme.availableFamilies
editable: true
currentIndex: {
let fam = theme.fontFamily || Qt.application.font.family
let idx = model.indexOf(fam)
return idx >= 0 ? idx : 0
}
onActivated: theme.fontFamily = currentText
onAccepted: theme.fontFamily = editText
}
Label { text: "Style:" }
ComboBox {
id: fontStyleCombo
Layout.fillWidth: true
model: theme.availableStyles
currentIndex: {
let st = theme.fontStyle
let idx = model.indexOf(st)
return idx >= 0 ? idx : 0
}
onActivated: theme.fontStyle = currentText
}
Label { text: "Size:" }
SpinBox {
id: fontSizeSpin
from: 6
to: 72
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
onValueModified: theme.fontSize = value
}
}
Button {
text: "Reset to system font"
onClicked: theme.resetFont()
}
}
}
// --- New Session Dialog ---
Dialog {
id: newSessionDialog
title: "New Session"
anchors.centerIn: parent
width: 420
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: ColumnLayout {
spacing: 16
Layout.margins: 16
// Directory
RowLayout {
Layout.fillWidth: true
Label { text: "Directory:"; Layout.preferredWidth: 80 }
TextField {
id: dirField
Layout.fillWidth: true
placeholderText: "/path/to/project"
readOnly: false
}
Button {
text: "Browse..."
onClicked: {
var folder = theme.chooseFolder("Select Project Directory", dirField.text || "")
if (folder !== "")
dirField.text = folder
}
}
}
// Name
RowLayout {
Layout.fillWidth: true
Label { text: "Name:"; Layout.preferredWidth: 80 }
TextField {
id: nameField
Layout.fillWidth: true
placeholderText: "(auto-generated)"
}
}
// Backend
RowLayout {
Layout.fillWidth: true
Label { text: "Backend:"; Layout.preferredWidth: 80 }
ComboBox {
id: backendCombo
Layout.fillWidth: true
enabled: ollie.rootBackendsLoaded
model: ollie.availableBackends
currentIndex: 0
editable: true
onAccepted: backendCombo.editText = backendCombo.currentText
}
}
// Model list (populated reactively from backend selection)
ListModel { id: modelList }
// Model
RowLayout {
Layout.fillWidth: true
Label { text: "Model:"; Layout.preferredWidth: 80 }
ComboBox {
id: modelCombo
Layout.fillWidth: true
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
model: modelList
currentIndex: -1
editable: true
onAccepted: modelCombo.editText = modelCombo.currentText
}
}
// Agent
RowLayout {
Layout.fillWidth: true
Label { text: "Agent:"; Layout.preferredWidth: 80 }
ComboBox {
id: agentCombo
Layout.fillWidth: true
enabled: ollie.rootAgentsLoaded
model: ollie.availableAgents
currentIndex: 0
}
}
// Remote
RowLayout {
Layout.fillWidth: true
Label { text: "Remote:"; Layout.preferredWidth: 80 }
TextField {
id: remoteField
Layout.fillWidth: true
placeholderText: "user@host:port"
}
}
Component.onCompleted: {
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
}
Connections {
target: backendCombo
function onCurrentTextChanged() {
var be = backendCombo.currentText.trim()
modelList.clear()
if (be !== "" && ollie.rootModelsLoaded) {
var models = ollie.getAvailableModels(be)
for (var i = 0; i < models.length; ++i) {
modelList.append({ modelData: models[i] })
}
if (models.length > 0) {
modelCombo.currentIndex = 0
} else {
modelCombo.currentIndex = -1
}
} else {
modelCombo.currentIndex = -1
}
}
}
}
onAccepted: {
var cwd = dirField.text.trim()
if (cwd === "") {
console.log("New session: directory is required")
return
}
var name = nameField.text.trim()
var backend = backendCombo.currentText.trim()
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
var agent = agentCombo.currentText
var remote = remoteField.text.trim()
var ok = ollie.createSession(cwd, name, backend, model, agent, remote)
if (ok) {
ollie.refreshSessions()
} else {
console.log("New session: creation failed")
}
}
}
SplitView {
anchors.fill: parent
// Left pane: session list
ColumnLayout {
SplitView.preferredWidth: root.width * 0.15
SplitView.minimumWidth: 300
spacing: 0
ToolBar {
Layout.fillWidth: true
RowLayout {
anchors.fill: parent
Label { text: "Sessions"; Layout.fillWidth: true; font.bold: true; leftPadding: 8 }
ToolButton {
text: "\u2795"
onClicked: newSessionDialog.open()
ToolTip.text: "New Session"
ToolTip.visible: hovered
}
ToolButton {
text: "\u21BB"
onClicked: ollie.refreshSessions()
}
ToolButton {
text: "\u2699"
onClicked: settingsDialog.open()
ToolTip.text: "Settings"
ToolTip.visible: hovered
}
}
}
ListView {
id: sessionList
Layout.fillWidth: true
Layout.fillHeight: true
model: sessionTreeModel
clip: true
delegate: ItemDelegate {
width: sessionList.width
// Guard against undefined during initial render
height: {
if (!type) return 0
if (type === "agent" && !parentSessionExpanded(parentId))
return 0
return 28
}
visible: height > 0
contentItem: RowLayout {
anchors.fill: parent
spacing: 0
// Indent + expand/collapse indicator
Label {
Layout.preferredWidth: (level || 0) * 16 + 16
text: type === "session" ? (expanded ? "▼" : "▶") : ""
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
color: theme.textColor
opacity: 0.6
verticalAlignment: Text.AlignVCenter
}
// Name
Label {
Layout.fillWidth: true
text: displayName || ""
color: theme.textColor
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
verticalAlignment: Text.AlignVCenter
}
// State dot
Rectangle {
Layout.preferredWidth: 8
Layout.preferredHeight: 8
Layout.alignment: Qt.AlignVCenter
radius: 4
color: {
var s = state || "idle"
if (s === "idle") return "#4caf50"
if (s.startsWith("calling")) return "#ff9800"
return "#2196f3"
}
}
}
onClicked: {
if (type === "session") {
toggleExpand(index)
} else if (type === "agent") {
activateAgent(parentId, agentId)
}
}
highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId)
function parentSessionExpanded(pid) {
for (var i = 0; i < sessionTreeModel.count; ++i) {
var item = sessionTreeModel.get(i)
if (item.id === pid && item.type === "session")
return item.expanded
}
return false
}
}
}
}
// Right pane: chat + input
ColumnLayout {
spacing: 0
// Header
RowLayout {
Layout.fillWidth: true
Layout.margins: 6
spacing: 8
Rectangle {
width: 10; height: 10; radius: 5
color: {
if (ollie.activeState === "idle") return "#4caf50"
if (ollie.activeState.startsWith("calling")) return "#ff9800"
return "#2196f3"
}
}
Label {
text: ollie.activeState
opacity: 0.7
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
}
Item { Layout.fillWidth: true }
}
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
// Chat display — block-based ListView
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ListView {
id: chatView
model: chatModel
spacing: 4
anchors.fill: parent
anchors.margins: 4
boundsBehavior: Flickable.StopAtBounds
onCountChanged: {
if (count > 0)
scrollTimer.start()
}
Connections {
target: chatModel
function onDataChanged() {
if (chatView.atYEnd && chatView.count > 0)
scrollTimer.start()
}
}
delegate: ColumnLayout {
id: blockItem
width: chatView.width - 8
spacing: 2
property bool blockExpanded: {
switch (blockType) {
case "user": return true
case "assistant": return true
case "tool": return true
case "info": return true
case "retry": return true
default: return false
}
}
// Clickable header
Rectangle {
Layout.fillWidth: true
height: 22
radius: 3
color: {
var bg = theme.baseColor
if (theme.isDark) {
switch (blockType) {
case "user": return Qt.lighter(bg, 1.8)
case "assistant": return Qt.lighter(bg, 1.5)
case "reasoning": return Qt.lighter(bg, 1.3)
case "call": return Qt.lighter(bg, 1.7)
case "tool": return Qt.lighter(bg, 1.2)
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
case "info": return Qt.lighter(bg, 1.4)
case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5)
default: return bg
}
} else {
switch (blockType) {
case "user": return Qt.darker(bg, 1.12)
case "assistant": return Qt.darker(bg, 1.06)
case "reasoning": return Qt.darker(bg, 1.03)
case "call": return Qt.darker(bg, 1.1)
case "tool": return Qt.darker(bg, 1.04)
case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7)
case "info": return Qt.darker(bg, 1.02)
case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5)
default: return bg
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: blockItem.blockExpanded = !blockItem.blockExpanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 6
spacing: 4
Label {
text: blockItem.blockExpanded ? "▼" : "▶"
font.pixelSize: 8
opacity: 0.6
}
Label {
Layout.fillWidth: true
text: {
switch (blockType) {
case "user": return "User"
case "assistant": return "Assistant" + (blockName ? " (" + blockName + ")" : "")
case "reasoning": return "Reasoning"
case "call": return "Call: " + blockName
case "tool": return "Tool: " + blockName
case "error": return "Error"
case "info": return "Info"
case "retry": return "Retry"
default: return ""
}
}
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
font.bold: true
color: theme.textColor
opacity: 0.9
}
}
}
// Rendered HTML (fenced code/diff)
TextEdit {
Layout.fillWidth: true
visible: blockItem.blockExpanded && blockRenderedHtml && blockRenderedHtml.length > 0
text: blockRenderedHtml
textFormat: Text.RichText
readOnly: true
wrapMode: TextEdit.Wrap
font.family: theme.fontFamily || "monospace"
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
color: theme.textColor
}
// Plain text fallback
Label {
Layout.fillWidth: true
visible: blockItem.blockExpanded && (!blockRenderedHtml || blockRenderedHtml.length === 0)
text: blockContent || ""
wrapMode: Text.Wrap
textFormat: Text.PlainText
font.family: theme.fontFamily || "monospace"
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
color: theme.textColor
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
// Input
RowLayout {
Layout.fillWidth: true
Layout.margins: 6
spacing: 4
ScrollView {
id: promptScroll
Layout.fillWidth: true
Layout.maximumHeight: 200
Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200)
clip: true
TextArea {
id: promptInput
Layout.fillWidth: true
placeholderText: ollie.activeSessionId ? "Message..." : "No session"
enabled: ollie.activeSessionId !== ""
wrapMode: TextEdit.Wrap
property string _lastText: ""
property int _lastCursorPos: 0
onTextChanged: {
var oldText = _lastText
var newText = text
var cursorPos = cursorPosition
// Detect slash prefix (text after last "/" up to cursor)
if (newText.indexOf("/") >= 0) {
var slashPos = newText.lastIndexOf("/")
if (slashPos < cursorPos) {
var prefix = newText.substring(slashPos + 1, cursorPos)
if (prefix !== oldText.substring(oldText.lastIndexOf("/"), cursorPos) ||
oldText !== _lastText) {
completionPopup.open(prefix)
}
} else {
completionPopup.close()
}
} else {
completionPopup.close()
}
_lastText = newText
_lastCursorPos = cursorPos
}
onCursorPositionChanged: {
var newText = text
if (newText.indexOf("/") >= 0) {
var slashPos = newText.lastIndexOf("/")
if (slashPos < cursorPosition) {
var prefix = newText.substring(slashPos + 1, cursorPosition)
completionPopup.open(prefix)
} else {
completionPopup.close()
}
} else {
completionPopup.close()
}
}
Keys.onPressed: function(event) {
if (completionPopup.visible) {
if (event.key === Qt.Key_Tab) {
completionPopup.acceptCompletion()
event.accepted = true
} else if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
if (completionPopup.completionList.currentIndex >= 0) {
completionPopup.acceptCompletion()
event.accepted = true
}
} else if (event.key === Qt.Key_Escape) {
completionPopup.close()
event.accepted = true
} else if (event.key === Qt.Key_Down) {
if (completionPopup.completionList.currentIndex < completionPopup.completionList.count - 1) {
completionPopup.completionList.currentIndex++
event.accepted = true
}
} else if (event.key === Qt.Key_Up) {
if (completionPopup.completionList.currentIndex > 0) {
completionPopup.completionList.currentIndex--
event.accepted = true
}
}
}
}
Keys.onReturnPressed: function(event) {
if (event.modifiers & Qt.ShiftModifier) {
event.accepted = false
} else {
if (text.trim() !== "") {
ollie.submit(text.trim())
text = ""
}
event.accepted = true
}
}
}
}
}
}
}
}