From d8431223d34476a17835a05d9508e92447f22479 Mon Sep 17 00:00:00 2001 From: Adrian Kummerlaender Date: Sat, 15 Aug 2015 16:06:00 +0200 Subject: Restructure QML and JS sources `list` holds the components of the central list UI element. `command` holds the UI and implementation parts of the command mode. `widget` holds more or less general purpose elements that may also be of use in other circumstances. --- src/StateHandler.qml | 158 ++++++++++++++++++++++++++++++++++++ src/command/CommandInput.qml | 146 +++++++++++++++++++++++++++++++++ src/command/commands.js | 55 +++++++++++++ src/list/TerminalItem.qml | 176 ++++++++++++++++++++++++++++++++++++++++ src/list/TerminalList.qml | 125 ++++++++++++++++++++++++++++ src/main.qml | 53 ++++++++++++ src/ui.qrc | 12 +++ src/widget/EmbeddedTerminal.qml | 153 ++++++++++++++++++++++++++++++++++ src/widget/Highlighter.qml | 33 ++++++++ 9 files changed, 911 insertions(+) create mode 100644 src/StateHandler.qml create mode 100644 src/command/CommandInput.qml create mode 100644 src/command/commands.js create mode 100644 src/list/TerminalItem.qml create mode 100644 src/list/TerminalList.qml create mode 100644 src/main.qml create mode 100644 src/ui.qrc create mode 100644 src/widget/EmbeddedTerminal.qml create mode 100644 src/widget/Highlighter.qml (limited to 'src') diff --git a/src/StateHandler.qml b/src/StateHandler.qml new file mode 100644 index 0000000..d9f5f5a --- /dev/null +++ b/src/StateHandler.qml @@ -0,0 +1,158 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 +import Qt.labs.settings 1.0 + +Item { + id: item + + property Item terminalList : null + property CommandInput commandInput : null + + Settings { + id: settings + category: "keybinding" + + property string insertMode : "i" + property string normalMode : "Shift+ESC" + property string commandMode : ":" + property string nextItem : "j" + property string prevItem : "k" + property string firstItem : "g" + property string resetItem : "d" + property string lastItem : "Shift+G" + property string heightenItem : "Shift+J" + property string shortenItem : "Shift+K" + } + + state: "INSERT" + + function enterInsertMode() { + enterInsertAction.trigger(); + } + + function enterNormalMode() { + enterNormalAction.trigger(); + } + + states: [ + State { + name: "NORMAL" + + PropertyChanges { target: enterNormalAction; enabled: false } + PropertyChanges { target: enterInsertAction; enabled: true } + PropertyChanges { target: enterCommandAction; enabled: true } + PropertyChanges { target: nextTerminalAction; enabled: true } + PropertyChanges { target: heightenTerminalAction; enabled: true } + PropertyChanges { target: shortenTerminalAction; enabled: true } + PropertyChanges { target: prevTerminalAction; enabled: true } + PropertyChanges { target: lastTerminalAction; enabled: true } + PropertyChanges { target: firstTerminalAction; enabled: true } + PropertyChanges { target: resetTerminalAction; enabled: true } + }, + State { + name: "INSERT" + + PropertyChanges { target: enterNormalAction; enabled: true } + PropertyChanges { target: enterInsertAction; enabled: false } + PropertyChanges { target: enterCommandAction; enabled: false } + PropertyChanges { target: nextTerminalAction; enabled: false } + PropertyChanges { target: heightenTerminalAction; enabled: false } + PropertyChanges { target: shortenTerminalAction; enabled: false } + PropertyChanges { target: prevTerminalAction; enabled: false } + PropertyChanges { target: lastTerminalAction; enabled: false } + PropertyChanges { target: firstTerminalAction; enabled: false } + PropertyChanges { target: resetTerminalAction; enabled: false } + }, + State { + name: "COMMAND" + + PropertyChanges { target: enterNormalAction; enabled: true } + PropertyChanges { target: enterInsertAction; enabled: false } + PropertyChanges { target: enterCommandAction; enabled: false } + PropertyChanges { target: nextTerminalAction; enabled: false } + PropertyChanges { target: heightenTerminalAction; enabled: false } + PropertyChanges { target: shortenTerminalAction; enabled: false } + PropertyChanges { target: prevTerminalAction; enabled: false } + PropertyChanges { target: lastTerminalAction; enabled: false } + PropertyChanges { target: firstTerminalAction; enabled: false } + PropertyChanges { target: resetTerminalAction; enabled: false } + } + ] + + Action { + id: enterNormalAction + shortcut: settings.normalMode + onTriggered: { + item.state = "NORMAL"; + + terminalList.forceActiveFocus(); + terminalList.unfocusCurrent(); + commandInput.unfocus(); + } + } + + Action { + id: enterInsertAction + shortcut: settings.insertMode + onTriggered: { + item.state = "INSERT"; + + terminalList.focusCurrent(); + } + } + + Action { + id: enterCommandAction + shortcut: settings.commandMode + onTriggered: { + item.state = "COMMAND"; + + commandInput.focus(shortcut); + } + } + + Action { + id: nextTerminalAction + shortcut: settings.nextItem + onTriggered: terminalList.selectNext() + } + + Action { + id: heightenTerminalAction + shortcut: settings.heightenItem + onTriggered: terminalList.getCurrent().heighten() + } + + Action { + id: shortenTerminalAction + shortcut: settings.shortenItem + onTriggered: terminalList.getCurrent().shorten() + } + + Action { + id: prevTerminalAction + shortcut: settings.prevItem + onTriggered: terminalList.selectPrev() + } + + Action { + id: lastTerminalAction + shortcut: settings.lastItem + onTriggered: terminalList.selectItem(terminalList.children.length - 1) + } + + Action { + id: firstTerminalAction + shortcut: settings.firstItem + onTriggered: terminalList.selectItem(0) + } + + Action { + id: resetTerminalAction + shortcut: settings.resetItem + onTriggered: { + terminalList.getCurrent().reset(); + terminalList.getCurrent().select(); + } + } +} diff --git a/src/command/CommandInput.qml b/src/command/CommandInput.qml new file mode 100644 index 0000000..99f5d0e --- /dev/null +++ b/src/command/CommandInput.qml @@ -0,0 +1,146 @@ +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import Qt.labs.settings 1.0 + +import "commands.js" as Commands + +Item { + id: item + + signal executed + + visible: false + + Layout.preferredHeight: container.height + + Settings { + id: settings + category: "command" + + property string background : "black" + property int fontSize : 12 + property string fontFamily : "Monospace" + property string fontColor : "white" + property string errorColor : "red" + } + + onVisibleChanged: container.reset() + + function focus(prefix) { + visible = true; + command.text = prefix; + command.forceActiveFocus(); + } + + function unfocus() { + visible = false; + } + + Rectangle { + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + height: container.height + + color: settings.background + + ColumnLayout { + id: container + + function reset() { + command.initialize(); + output.initialize(); + } + + TextInput { + id: command + + Layout.fillWidth: true + + font { + family: settings.fontFamily + pointSize: settings.fontSize + } + + color: settings.fontColor + selectionColor: settings.fontColor + selectedTextColor: settings.background + selectByMouse: true + + function initialize() { + text = ''; + } + + onAccepted: { + output.initialize(); + + const prefix = String(text).charAt(0); + const cmd = String(text).substring(1, String(text).length); + + switch ( prefix ) { + case ':': { + Commands.execute(output, cmd); + break; + } + default: { + output.error('"' + prefix + '"' + ' is not a command prefix.'); + } + } + + if ( output.isInitial() ) { + item.executed(); + } + } + } + + Text { + id: output + + Layout.fillWidth: true + Layout.preferredHeight: 0 + + font { + family: settings.fontFamily + pointSize: settings.fontSize + } + + color: settings.fontColor + + function isInitial() { + return text === ''; + } + + function initialize() { + text = ''; + } + + function log(msg) { + if ( isInitial() ) { + text = msg; + } else { + text += '
' + msg; + } + } + + function error(msg) { + text = '' + + msg + + ''; + } + + onTextChanged: { + if ( isInitial() ) { + Layout.preferredHeight = 0; + } else { + Layout.preferredHeight = contentHeight; + } + } + } + } + } +} diff --git a/src/command/commands.js b/src/command/commands.js new file mode 100644 index 0000000..f76af01 --- /dev/null +++ b/src/command/commands.js @@ -0,0 +1,55 @@ +function execute(output, command) { + var notImplemented = function(name) { + output.error('"' + name + '"' + ' is not implemented.'); + }; + var args = command.split(' '); + + try { + var closure = eval(args[0]); + + if ( typeof closure === 'function' ) { + args.shift(); + closure(output, args); + } else { + notImplemented(args[0]); + } + } catch (exception) { + notImplemented(args[0]); + } +} + +function exec(output, args) { + try { + var result = eval(args.join(' ')); + + if ( typeof result !== 'undefined' ) { + output.log(result); + } + } catch (exception) { + output.error(exception); + } +} + +function jump(output, index) { + terminalList.selectItem(index); +} + +function kill(output, index) { + terminalList.get(index).reset(); +} + +function next() { + terminalList.selectNext(); +} + +function prev() { + terminalList.selectPrev(); +} + +function ls(output) { + terminalList.iterate(function(item) { + if ( item.terminal !== null ) { + output.log(item.index + ': ' + item.terminal.program); + } + }); +} diff --git a/src/list/TerminalItem.qml b/src/list/TerminalItem.qml new file mode 100644 index 0000000..57197bd --- /dev/null +++ b/src/list/TerminalItem.qml @@ -0,0 +1,176 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import Qt.labs.settings 1.0 + +Item { + id: item + + property int index : 0 + property EmbeddedTerminal terminal : null + + signal executed (int index) + + Settings { + id: settings + category: "item" + + property int fontSize : 18 + property string fontFamily : "Monospace" + property string fontColor : "white" + } + + anchors { + left: parent.left + right: parent.right + } + + height: elementList.height + + function select() { + if ( terminal === null ) { + highlighter.select(); + } else { + terminal.select(); + } + } + + function deselect() { + if ( terminal === null ) { + highlighter.deselect(); + } else { + terminal.deselect(); + } + } + + function forceActiveFocus() { + scope.forceActiveFocus(); + + if ( terminal === null ) { + scope.forceActiveFocus(); + highlighter.select(); + highlighter.focus(); + } + } + + function unfocus() { + if ( terminal === null ) { + highlighter.unfocus(); + } + } + + function heighten() { + if ( terminal !== null ) { + terminal.lines += 1; + } + } + + function shorten() { + if ( terminal !== null ) { + if ( terminal.lines > 10 ) { + terminal.lines -= 1; + } else { + terminal.displayOverlay(); + } + } + } + + function reset() { + if ( terminal !== null ) { + terminal.destroy(); + + terminal = null; + command.readOnly = false; + command.focus = true; + + unfocus(); + } + } + + FocusScope { + id: scope + + anchors { + left: parent.left + right: parent.right + } + + Column { + id: elementList + + anchors { + left: parent.left + right: parent.right + } + + function createTerminal(program) { + var terminalComponent = Qt.createComponent("qrc:/EmbeddedTerminal.qml"); + var instantiateTerminal = function() { + item.terminal = terminalComponent.createObject(elementList, { + "program" : program, + "workingDirectory" : "$HOME", + "focus" : true + }); + } + + if ( terminalComponent.status === Component.Ready ) { + instantiateTerminal(); + } else { + terminalComponent.statusChanged.connect(instantiateTerminal); + } + } + + RowLayout { + anchors { + left: parent.left + right: parent.right + } + + Highlighter { + id: highlighter + + width: 10 + height: command.height + } + + TextInput { + id: command + + font { + family: settings.fontFamily + pointSize: settings.fontSize + } + + color: settings.fontColor + selectionColor: settings.fontColor + selectedTextColor: "#161616" + + selectByMouse: true + focus: true + Layout.fillWidth: true + + onAccepted: { + if ( item.terminal === null ) { + readOnly = true; + focus = false; + + elementList.createTerminal(text); + item.executed(item.index); + highlighter.deselect(); + } + } + } + + Text { + font { + family: settings.fontFamily + pointSize: settings.fontSize / 1.5 + } + color: settings.fontColor + + text: item.index + } + } + } + } +} diff --git a/src/list/TerminalList.qml b/src/list/TerminalList.qml new file mode 100644 index 0000000..6c6465b --- /dev/null +++ b/src/list/TerminalList.qml @@ -0,0 +1,125 @@ +import QtQuick 2.0 +import QtQuick.Layouts 1.1 + +Item { + id: item + + property StateHandler state : null + property int activeItem : 0 + property int itemIndex : 0 + + property alias children : column.children + + function onItemExecuted(index) { + if ( index === (children.length - 1) ) { + createItem(); + } + } + + function createItem() { + var terminalItem = Qt.createComponent("qrc:/TerminalItem.qml"); + var instantiateTerminal = function() { + var instance = terminalItem.createObject(column, { + "index": itemIndex, + "width": flickable.width + }); + instance.onExecuted.connect(onItemExecuted); + + ++itemIndex; + } + + if ( terminalItem.status === Component.Ready ) { + instantiateTerminal(); + } else { + terminalItem.statusChanged.connect(instantiateTerminal); + } + } + + function scrollTo(index) { + if ( column.height >= flickable.height ) { + var offset = children[index].y + + (children[index].height / 2) + - (flickable.height / 2); + + var bound = column.height + - flickable.height; + + if ( offset < 0 ) { + flickable.contentY = 0; + } else if ( offset >= bound ) { + flickable.contentY = bound; + } else { + flickable.contentY = offset; + } + } + } + + function selectItem(index) { + children[activeItem].deselect(); + children[index ].select(); + + activeItem = typeof index === "number" ? index : parseInt(index); + + scrollTo(index); + } + + function selectNext() { + if ( activeItem < (children.length - 1) ) { + selectItem(activeItem + 1); + } else { + state.enterInsertMode(); + } + } + + function selectPrev() { + if ( activeItem > 0 ) { + selectItem(activeItem - 1); + } + } + + function focusCurrent() { + children[activeItem].forceActiveFocus(); + } + + function unfocusCurrent() { + children[activeItem].unfocus(); + } + + function getCurrent() { + return children[activeItem]; + } + + function get(index) { + return children[index]; + } + + function iterate(func) { + for ( var i = 0; i < children.length; i++ ) { + func(children[i]); + } + } + + Flickable { + id: flickable + + anchors.fill: parent + + boundsBehavior: Flickable.StopAtBounds + contentHeight: column.height + contentWidth: parent.width + pixelAligned: true + + Column { + id: column + + anchors { + left: parent.left + right: parent.right + } + + spacing: 10 + + onHeightChanged: scrollTo(activeItem) + } + } +} diff --git a/src/main.qml b/src/main.qml new file mode 100644 index 0000000..f7673dc --- /dev/null +++ b/src/main.qml @@ -0,0 +1,53 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import Qt.labs.settings 1.0 + +ApplicationWindow { + id: root + + visible: true + + Settings { + id: settings + category: "window" + + property string background : "#161616" + } + + color: settings.background + + Component.onCompleted: { + terminalList.createItem(); + terminalList.focusCurrent(); + } + + ColumnLayout { + anchors.fill: parent + + TerminalList { + id: terminalList + + state: state + + Layout.fillHeight: true + Layout.fillWidth: true + } + + CommandInput { + id: command + + Layout.fillWidth: true + + onExecuted: state.enterNormalMode() + } + } + + StateHandler { + id: state + + terminalList: terminalList + commandInput: command + } +} diff --git a/src/ui.qrc b/src/ui.qrc new file mode 100644 index 0000000..48554f7 --- /dev/null +++ b/src/ui.qrc @@ -0,0 +1,12 @@ + + + main.qml + StateHandler.qml + list/TerminalItem.qml + list/TerminalList.qml + command/CommandInput.qml + command/commands.js + widget/EmbeddedTerminal.qml + widget/Highlighter.qml + + diff --git a/src/widget/EmbeddedTerminal.qml b/src/widget/EmbeddedTerminal.qml new file mode 100644 index 0000000..6d0dc6e --- /dev/null +++ b/src/widget/EmbeddedTerminal.qml @@ -0,0 +1,153 @@ +import QtQuick 2.0 +import QMLTermWidget 1.0 +import QtQuick.Layouts 1.1 +import Qt.labs.settings 1.0 + +Item { + id: item + + property string program + property string workingDirectory + + Settings { + id: settings + category: "terminal" + + property int initialLines : 20 + property int frameWidth : 10 + property int fontSize : 8 + property string fontFamily : "Monospace" + property string colorScheme : "cool-retro-term" + property string overlayBackground : "black" + property string overlayFontColor : "white" + } + + property int lines : settings.initialLines + + height: terminal.height + width: parent.width - settings.frameWidth + + function select() { highlighter.select() } + function deselect() { highlighter.deselect() } + function displayOverlay() { overlay.displayBriefly() } + + RowLayout { + id: container + + anchors { + left: parent.left + right: parent.right + } + + spacing: 0 + + Highlighter { + id: highlighter + + width: settings.frameWidth + Layout.fillHeight: true + } + + QMLTermWidget { + id: terminal + + font { + family: settings.fontFamily + pointSize: settings.fontSize + } + + Layout.fillWidth: true + Layout.preferredHeight: fontMetrics.height * item.lines + + colorScheme: settings.colorScheme + + session: QMLTermSession { + initialWorkingDirectory: item.workingDirectory + + shellProgram: { + return (item.program).split(" ")[0]; + } + + shellProgramArgs: { + const elements = (item.program).split(" "); + elements.shift(); + + return elements; + } + } + + Component.onCompleted: { + forceActiveFocus(); + highlighter.select(); + session.startShellProgram(); + overlay.enabled = true; + } + + onTermGetFocus: highlighter.focus() + onTermLostFocus: highlighter.unfocus() + onHeightChanged: overlay.displayBriefly(); + onWidthChanged: overlay.displayBriefly(); + + Rectangle { + id: overlay + + property bool enabled : false + + function displayBriefly() { + if ( enabled ) { animation.restart() } + } + + anchors.fill: parent + opacity: 0 + color: settings.overlayBackground + + SequentialAnimation { + id: animation + + ScriptAction { + script: overlay.opacity = 0.8 + } + + PauseAnimation { + duration: 500 + } + + NumberAnimation { + target: overlay + property: "opacity" + + easing.type: Easing.InSine + duration: 300 + from: 0.8 + to: 0 + } + } + + Text { + anchors { + horizontalCenter: overlay.horizontalCenter + verticalCenter: overlay.verticalCenter + } + + font { + family: settings.fontFamily + pointSize: settings.fontSize * 2 + } + color: settings.overlayFontColor + + text: { + return item.lines + + 'x' + + Math.floor(terminal.width / terminal.fontMetrics.width); + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + onWheel: { } + } + } + } +} diff --git a/src/widget/Highlighter.qml b/src/widget/Highlighter.qml new file mode 100644 index 0000000..e42aeb1 --- /dev/null +++ b/src/widget/Highlighter.qml @@ -0,0 +1,33 @@ +import QtQuick 2.0 +import Qt.labs.settings 1.0 + +Item { + Settings { + id: settings + category: "highlighter" + + property string defaultColor : "#909636" + property string focusColor : "#352F6A" + } + + function select() { bar.opacity = 1 } + function deselect() { bar.opacity = 0 } + function focus() { bar.color = settings.focusColor } + function unfocus() { bar.color = settings.defaultColor } + + Rectangle { + id: bar + + anchors.fill: parent + + opacity: 0 + color: settings.defaultColor + + Behavior on opacity { + NumberAnimation { + duration: 300 + easing.type: Easing.OutCubic + } + } + } +} -- cgit v1.2.3