import QtQuick import QtQuick.Controls.Fusion import QtQuick.Layouts import QtQuick.Dialogs import QtMultimedia ApplicationWindow { id: window width: 960 height: 720 visible: true title: qsTr("xTq") RelayConnection { id: connection onErrorOccurred: function(message) { errorDialog.text = message errorDialog.open() } onBeepRequested: { beepSound.play() } onConnectedChanged: { if (!connected) { connectDialog.open() } } onAboutToChangeBuffer: function(oldBuffer) { // Save the current buffer's input before switching connection.saveBufferInput( oldBuffer, inputText.text, inputText.selectionStart, inputText.selectionEnd ) } onBufferTextChanged: { connection.populateBufferDocument(bufferText.textDocument) scrollToBottom() } onCompletionResult: function(completion) { inputText.text = completion inputText.cursorPosition = inputText.text.length } onLogViewChanged: function(logText) { logView.text = logText logView.visible = true bufferText.visible = false logButton.checked = true var vbar = bufferScroll.ScrollBar.vertical vbar.position = 1.0 - vbar.size } } SoundEffect { id: beepSound source: "qrc:/beep.wav" volume: 0.5 } ColumnLayout { anchors.fill: parent anchors.margins: 6 spacing: 6 // Topic label Label { id: topicLabel Layout.fillWidth: true text: connection.topic textFormat: Text.RichText wrapMode: Text.Wrap onLinkActivated: function(link) { Qt.openUrlExternally(link) } visible: text.length > 0 padding: 4 background: Rectangle { color: palette.base border.color: palette.mid border.width: 1 } } // Split view between buffer list and buffer display area SplitView { id: mainSplit Layout.fillWidth: true Layout.fillHeight: true orientation: Qt.Horizontal // Buffer list Rectangle { SplitView.preferredWidth: 200 SplitView.minimumWidth: 150 color: "transparent" border.color: palette.mid border.width: 1 ListView { id: bufferList anchors.fill: parent anchors.margins: 1 clip: true model: connection.bufferListModel Connections { target: connection.bufferListModel function onBufferActivated() { bufferList.currentIndex = connection.bufferListModel.getCurrentBufferIndex() } } delegate: ItemDelegate { width: bufferList.width text: model.displayText font.bold: model.isBold highlighted: ListView.isCurrentItem contentItem: Label { text: parent.text font: parent.font color: model.highlightColor.valid ? model.highlightColor : palette.text elide: Text.ElideRight verticalAlignment: Text.AlignVCenter } onClicked: { bufferList.currentIndex = index connection.activateBuffer(model.bufferName) } } ScrollBar.vertical: ScrollBar {} } } // Buffer display area ScrollView { id: bufferScroll SplitView.fillWidth: true ScrollBar.vertical.policy: ScrollBar.AlwaysOn TextArea { id: bufferText textFormat: Text.RichText readOnly: true wrapMode: Text.Wrap selectByMouse: true onLinkActivated: function(link) { Qt.openUrlExternally(link) } Connections { target: connection function onCurrentBufferChanged() { connection.populateBufferDocument(bufferText.textDocument) bufferText.visible = true logView.visible = false logButton.checked = false scrollToBottom() // Restore input for the new buffer inputText.text = connection.getBufferInput() inputText.cursorPosition = connection.getBufferInputStart() inputText.select(connection.getBufferInputStart(), connection.getBufferInputEnd()) } } } TextArea { id: logView visible: false textFormat: Text.RichText readOnly: true wrapMode: Text.Wrap selectByMouse: true font.family: "monospace" onLinkActivated: function(link) { Qt.openUrlExternally(link) } } } } // Status area RowLayout { Layout.fillWidth: true spacing: 6 Label { id: promptLabel text: connection.prompt } Item { Layout.fillWidth: true } ToolButton { id: boldButton text: "B" font.bold: true checkable: true ToolTip.text: "Bold" ToolTip.visible: hovered onClicked: { inputText.font.bold = checked } } ToolButton { id: italicButton text: "I" font.italic: true checkable: true ToolTip.text: "Italic" ToolTip.visible: hovered onClicked: { inputText.font.italic = checked } } ToolButton { id: underlineButton text: "U" font.underline: true checkable: true ToolTip.text: "Underline" ToolTip.visible: hovered onClicked: { inputText.font.underline = checked } } Item { Layout.fillWidth: true } Label { id: statusLabel text: connection.status horizontalAlignment: Text.AlignRight } ToolButton { id: logButton text: "Log" checkable: true ToolTip.text: "View buffer log" ToolTip.visible: hovered onClicked: { if (checked) { connection.toggleBufferLog() } else { logView.visible = false bufferText.visible = true logView.text = "" } } } ToolButton { text: "↓" ToolTip.text: "Scroll to bottom" ToolTip.visible: hovered enabled: { var vbar = bufferScroll.ScrollBar.vertical return vbar.position + vbar.size < 1.0 } onClicked: scrollToBottom() } } // Input area ScrollView { Layout.fillWidth: true Layout.preferredHeight: Math.min(inputText.contentHeight + 12, 120) TextArea { id: inputText wrapMode: Text.Wrap selectByMouse: true textFormat: Text.RichText Keys.onReturnPressed: function(event) { if (event.modifiers & Qt.ShiftModifier) { // Allow Shift+Enter for newline return } event.accepted = true if (text.length > 0) { connection.sendInput(text) text = "" } } Keys.onUpPressed: { var history = connection.getInputHistoryUp() if (history.length > 0) { text = history } } Keys.onDownPressed: { var history = connection.getInputHistoryDown() if (history.length > 0) { text = history } } Keys.onTabPressed: { connection.requestCompletion(text, cursorPosition) } } } } // Keyboard shortcuts Shortcut { sequences: ["F5", "Alt+PgUp", "Ctrl+PgUp"] onActivated: connection.activatePreviousBuffer() } Shortcut { sequences: ["F6", "Alt+PgDown", "Ctrl+PgDown"] onActivated: connection.activateNextBuffer() } Shortcut { sequences: ["Ctrl+Tab", "Alt+Tab"] onActivated: connection.activateLastBuffer() } Shortcut { sequence: "Alt+A" onActivated: connection.activateNextWithActivity() } Shortcut { sequence: "Alt+!" onActivated: connection.activateNextHighlighted() } Shortcut { sequence: "Alt+H" onActivated: connection.toggleUnimportant() } Shortcut { sequence: "PgUp" onActivated: { var vbar = bufferScroll.ScrollBar.vertical vbar.position = Math.max(0, vbar.position - vbar.size) } } Shortcut { sequence: "PgDown" onActivated: { var vbar = bufferScroll.ScrollBar.vertical vbar.position = Math.min(1 - vbar.size, vbar.position + vbar.size) } } // Helper function to scroll to bottom function scrollToBottom() { var vbar = bufferScroll.ScrollBar.vertical vbar.position = 1.0 - vbar.size } // Connection dialog Dialog { id: connectDialog title: "Connect to relay" anchors.centerIn: parent modal: true closePolicy: Popup.NoAutoClose onOpened: connectHost.forceActiveFocus() onAccepted: { connection.host = connectHost.text connection.port = connectPort.text connection.connectToRelay() } onRejected: Qt.quit() GridLayout { anchors.fill: parent columns: 2 Label { text: "Host:" } TextField { id: connectHost Layout.fillWidth: true text: connection.host || "localhost" focus: true selectByMouse: true onAccepted: connectDialog.accept() } Label { text: "Port:" } TextField { id: connectPort Layout.fillWidth: true text: connection.port || "" selectByMouse: true validator: IntValidator { bottom: 0; top: 65535 } onAccepted: connectDialog.accept() } } footer: DialogButtonBox { Button { text: qsTr("Connect") DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole highlighted: true } Button { text: qsTr("Exit") DialogButtonBox.buttonRole: DialogButtonBox.RejectRole } } } // Error dialog MessageDialog { id: errorDialog title: "Error" buttons: MessageDialog.Ok } Component.onCompleted: { if (!connection.connected) { connectDialog.open() } else { inputText.forceActiveFocus() } } }