// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Layouts 1.12 import Clipboard 0.1 import "Base" import "ShortcutBundles" HDrawer { id: debugConsole property Item previouslyFocused: null property QtObject target: null property alias t: debugConsole.target property var history: window.history.console property int historyEntry: -1 property int maxHistoryLength: 4096 property var textBeforeHistoryNavigation: null // null or string property int selectedOutputDelegateIndex: -1 property string selectedOutputText: "" property string pythonDebugKeybind: window.settings.Keys.python_debugger[0] property string help: qsTr( `Interact with the QML code using JavaScript ES6 syntax. Useful variables: t Target item to debug for which this console was opened this The console itself py Python interpreter (${pythonDebugKeybind} to start debugger) window, mainUI, theme, settings, utils, mainPane, pageLoader Special commands: .j OBJECT, .json OBJECT Print OBJECT as human-readable JSON .t, .top Attach console to the parent window's top .b, .bottom Attach console to the parent window's bottom .l, .left Attach console to the parent window's left .r, .right Attach console to the parent window's right .h, .help Show this help`.replace(/^ {8}/gm, "") ) property bool doUselessThing: false property real baseGIFSpeed: 1.0 readonly property alias outputList: outputList function toggle(targetItem=null, js="", addToHistory=false) { if (debugConsole.visible) { debugConsole.visible = false return } debugConsole.visible = true debugConsole.target = ! targetItem && ! debugConsole.target ? mainUI : targetItem ? targetItem : debugConsole.target if (js) debugConsole.runJS(js, addToHistory) } function runJS(input, addToHistory=true) { if (addToHistory && history.slice(-1)[0] !== input) { history.push(input) while (history.length > maxHistoryLength) history.shift() window.saveHistory() } let output = "" let error = false try { if ([".h", ".help"].includes(input)) { output = debugConsole.help } else if ([".t", ".top"].includes(input)) { debugConsole.edge = Qt.TopEdge } else if ([".b", ".bottom"].includes(input)) { debugConsole.edge = Qt.BottomEdge } else if ([".l", ".left"].includes(input)) { debugConsole.edge = Qt.LeftEdge } else if ([".r", ".right"].includes(input)) { debugConsole.edge = Qt.RightEdge } else if (input.startsWith(".j ") || input.startsWith(".json ")) { output = JSON.stringify(eval(input.substring(2)), null, 4) } else { let result = eval(input) output = result instanceof Array ? "[" + String(result) + "]" : String(result) } } catch (err) { error = true output = err.toString() } outputList.model.insert(0, { input, output, error }) } objectName: "debugConsole" edge: Qt.TopEdge x: horizontal ? 0 : referenceSizeParent.width / 2 - width / 2 y: vertical ? 0 : referenceSizeParent.height / 2 - height / 2 width: horizontal ? calculatedSize : Math.min(window.width, 720) height: vertical ? calculatedSize : Math.min(window.height, 720) defaultSize: 400 z: 9999 position: 0 onTargetChanged: { outputList.model.insert(0, { input: "t = " + String(target), output: "", error: false, }) } onVisibleChanged: { if (visible) { previouslyFocused = window.activeFocusItem forceActiveFocus() } else if (previouslyFocused) { previouslyFocused.forceActiveFocus() } } onHistoryEntryChanged: { if (historyEntry === -1) { inputArea.clear() inputArea.append(textBeforeHistoryNavigation) textBeforeHistoryNavigation = null return } if (textBeforeHistoryNavigation === null) textBeforeHistoryNavigation = inputArea.text inputArea.clear() inputArea.append(history.slice(-historyEntry - 1)[0]) } HShortcut { sequences: settings.Keys.qml_console onActivated: debugConsole.toggle() } HColumnLayout { anchors.fill: parent // Fixes inputArea cursor invisible when at cursorPosition 0 anchors.leftMargin: 1 HListView { id: outputList spacing: theme.spacing topMargin: theme.spacing bottomMargin: topMargin leftMargin: theme.spacing rightMargin: leftMargin clip: true verticalLayoutDirection: ListView.BottomToTop Layout.fillWidth: true Layout.fillHeight: true model: ListModel {} delegate: HSelectableLabel { id: delegate width: outputList.width - outputList.leftMargin - outputList.rightMargin readonly property color inputColor: model.error ? theme.colors.errorText : model.output ? theme.colors.accentText : theme.colors.positiveText text: `
` + `` + utils.plain2Html(model.input) + "" + (model.input && model.output ? "
" : "") + (model.output ? utils.plain2Html(model.output) : "") + "
" leftPadding: theme.spacing textFormat: HSelectableLabel.RichText wrapMode: HLabel.Wrap font.family: theme.fontFamily.mono color: model.error ? Qt.darker(inputColor, 1.4) : theme.colors.halfDimText Layout.fillWidth: true onSelectedTextChanged: if (selectedPlainText) { selectedOutputDelegateIndex = model.index selectedOutputText = selectedPlainText } else if (selectedOutputDelegateIndex === model.index) { selectedOutputDelegateIndex = -1 selectedOutputText = "" } Connections { target: debugConsole onSelectedOutputDelegateIndexChanged: { if (selectedOutputDelegateIndex !== model.index) delegate.deselect() } } TapHandler { acceptedButtons: Qt.RightButton gesturePolicy: TapHandler.ReleaseWithinBounds acceptedPointerTypes: PointerDevice.GenericPointer | PointerDevice.Pen onTapped: menu.popup() } TapHandler { acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen onLongPressed: menu.popup() } HMenu { id: menu implicitWidth: Math.min(window.width, 150) z: 10000 HMenuItem { icon.name: "copy-text" text: qsTr("Copy") onTriggered: { if (delegate.selectedPlainText) { delegate.copy() return } delegate.selectAll() delegate.copy() delegate.deselect() } } } Rectangle { width: 1 height: parent.height color: model.error ? theme.colors.negativeBackground : model.output ? theme.colors.accentElement : theme.colors.positiveBackground } } FlickShortcuts { active: debugConsole.visible flickable: outputList } Rectangle { z: -10 anchors.fill: parent color: theme.colors.weakBackground } } HTextArea { id: inputArea function accept() { if (! text) return runJS(text) clear() historyEntry = -1 } focus: true backgroundColor: Qt.hsla(0, 0, 0, 0.85) bordered: false placeholderText: qsTr("QML/JavaScript debug console - Type .help") font.family: theme.fontFamily.mono Keys.onUpPressed: ev => { ev.accepted = cursorRectangle.top < topPadding + font.pixelSize && historyEntry + 1 < history.length if (ev.accepted) { historyEntry += 1 cursorPosition = length } } Keys.onDownPressed: ev => { ev.accepted = cursorRectangle.bottom >= height - bottomPadding - font.pixelSize && historyEntry - 1 >= -1 if (ev.accepted) historyEntry -= 1 } Keys.onTabPressed: inputArea.insertAtCursor(" ") Keys.onReturnPressed: ev => { ev.modifiers & Qt.ShiftModifier || ev.modifiers & Qt.ControlModifier || ev.modifiers & Qt.AltModifier ? inputArea.insertAtCursor("\n") : accept() } Keys.onEnterPressed: ev => Keys.returnPressed(ev) Keys.onEscapePressed: debugConsole.close() Keys.onPressed: ev => { if ( ev.matches(StandardKey.Copy) && ! inputArea.selectedPlainText && selectedOutputText ) { ev.accepted = true Clipboard.text = selectedOutputText } } Layout.fillWidth: true } } NumberAnimation { running: doUselessThing target: mainUI.mainPane.roomList property: "rotation" duration: 250 from: 360 to: 0 loops: Animation.Infinite onStopped: target.rotation = 0 } }