mirror of
https://gitlab.com/eql/lqml.git
synced 2025-12-06 02:30:38 -08:00
655 lines
18 KiB
QML
655 lines
18 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Window 2.15
|
|
import 'ext/' as Ext
|
|
import 'ext/dialogs' as Dlg
|
|
|
|
StackView {
|
|
id: main
|
|
objectName: "main"
|
|
width: 800 // alternatively: Screen.desktopAvailableWidth
|
|
height: 600 // alternatively: Screen.desktopAvailableHeight
|
|
initialItem: mainRect
|
|
Screen.orientationUpdateMask: Qt.LandscapeOrientation | Qt.PortraitOrientation | Qt.InvertedLandscapeOrientation
|
|
|
|
property bool small: (Math.max(width, height) < 1000)
|
|
property bool skipEnsureVisible: false
|
|
property double editorHeight: 0.5 // preferred initial height (50%)
|
|
property string cursorColor: "blue"
|
|
|
|
function availableHeight() {
|
|
var h = Math.round(Qt.inputMethod.keyboardRectangle.y /
|
|
((Qt.platform.os === "android") ? Screen.devicePixelRatio : 1))
|
|
return (h === 0) ? main.height : h
|
|
}
|
|
|
|
function divideHeight(factor) { return (availableHeight() - rectCommand.height) * factor }
|
|
function isLandscape() { return (Screen.primaryOrientation === Qt.LandscapeOrientation) }
|
|
function keyboardVisible() { return Qt.inputMethod.visible }
|
|
function showKeyboard(show) { show ? Qt.inputMethod.show() : Qt.inputMethod.hide() }
|
|
|
|
// show/hide dialogs
|
|
|
|
function pushDialog(name) {
|
|
switch (name) {
|
|
case "query": dialogQuery.open(); break
|
|
case "file": main.push(dialogFile); break
|
|
case "debug": main.push(dialogDebug); break
|
|
case "help": main.push(dialogHelp); break
|
|
}
|
|
}
|
|
|
|
function popDialog() { main.pop() }
|
|
|
|
Screen.onOrientationChanged: {
|
|
Lisp.call("editor:orientation-changed", Screen.orientation)
|
|
}
|
|
|
|
Keys.onPressed: (event) => {
|
|
if (event.key === Qt.Key_Back) {
|
|
event.accepted = true
|
|
Lisp.call("editor:back-pressed")
|
|
}
|
|
}
|
|
|
|
// custom transition animations
|
|
|
|
pushEnter: Transition {
|
|
ParallelAnimation {
|
|
OpacityAnimator {
|
|
from: 0
|
|
to: 1
|
|
easing.type: Easing.OutQuart
|
|
duration: 300
|
|
}
|
|
XAnimator {
|
|
from: width / 3
|
|
to: 0
|
|
easing.type: Easing.OutQuart
|
|
duration: 300
|
|
}
|
|
}
|
|
}
|
|
|
|
pushExit: Transition {
|
|
OpacityAnimator {
|
|
from: 1
|
|
to: 0
|
|
duration: 300
|
|
}
|
|
}
|
|
|
|
popEnter: Transition {
|
|
OpacityAnimator {
|
|
from: 0
|
|
to: 1
|
|
duration: 300
|
|
}
|
|
}
|
|
|
|
popExit: Transition {
|
|
ParallelAnimation {
|
|
OpacityAnimator {
|
|
from: 1
|
|
to: 0
|
|
easing.type: Easing.InQuart
|
|
duration: 300
|
|
}
|
|
XAnimator {
|
|
from: 0
|
|
to: width / 3
|
|
easing.type: Easing.InQuart
|
|
duration: 300
|
|
}
|
|
}
|
|
}
|
|
|
|
// delay timer
|
|
|
|
Timer {
|
|
id: timer
|
|
}
|
|
|
|
function delay(milliseconds, callback) {
|
|
timer.interval = milliseconds
|
|
timer.triggered.connect(callback)
|
|
timer.start()
|
|
}
|
|
|
|
function later(callback) {
|
|
delay(50, callback)
|
|
}
|
|
|
|
// fonts (must stay here, before using them below)
|
|
|
|
FontLoader { id: fontHack; source: "fonts/Hack-Regular.ttf" } // code
|
|
FontLoader { id: fontHackBold; source: "fonts/Hack-Bold.ttf" }
|
|
FontLoader { id: fontAwesome; source: "fonts/fontawesome-webfont.ttf" } // icons
|
|
|
|
// items
|
|
|
|
Rectangle {
|
|
id: mainRect
|
|
|
|
SplitView {
|
|
id: splitView
|
|
anchors.fill: parent
|
|
orientation: Qt.Vertical
|
|
|
|
property double handleHeight: 10
|
|
|
|
handle: Rectangle {
|
|
implicitHeight: splitView.handleHeight
|
|
color: SplitHandle.pressed ? Qt.darker(rectOutput.color) : rectOutput.color
|
|
}
|
|
|
|
Rectangle {
|
|
id: rectEdit
|
|
objectName: "rect_edit"
|
|
width: main.width
|
|
SplitView.preferredHeight: divideHeight(editorHeight)
|
|
|
|
Ext.Flickable {
|
|
id: flickEdit
|
|
objectName: "flick_edit"
|
|
anchors.fill: parent
|
|
contentWidth: edit.paintedWidth
|
|
contentHeight: edit.paintedHeight
|
|
|
|
TextEdit {
|
|
id: edit
|
|
objectName: "edit"
|
|
width: flickEdit.width
|
|
height: flickEdit.height
|
|
leftPadding: 2
|
|
font.family: "Hack"
|
|
font.pixelSize: 18
|
|
selectionColor: "firebrick"
|
|
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhNoTextHandles | Qt.ImhNoEditMenu
|
|
cursorDelegate: cursor
|
|
|
|
Keys.onTabPressed: command.forceActiveFocus()
|
|
|
|
onCursorRectangleChanged: flickEdit.ensureVisible(cursorRectangle)
|
|
|
|
Component.onCompleted: later(function() {
|
|
Lisp.call("editor:set-text-document", objectName, textDocument)
|
|
})
|
|
|
|
// for external keyboard
|
|
Shortcut {
|
|
sequence: "Ctrl+E" // E for Expression
|
|
onActivated: Lisp.call("editor:select-expression")
|
|
}
|
|
Shortcut {
|
|
sequence: "Ctrl+L" // L for Lambda
|
|
onActivated: Lisp.call("editor:eval-single-expression")
|
|
}
|
|
|
|
MouseArea {
|
|
width: Math.max(rectEdit.width, edit.paintedWidth)
|
|
height: Math.max(rectEdit.height, edit.paintedHeight)
|
|
|
|
onPressed: {
|
|
// seems necessary to consistently move cursor by tapping
|
|
edit.forceActiveFocus()
|
|
edit.cursorPosition = edit.positionAt(mouse.x, mouse.y)
|
|
Qt.inputMethod.show() // needed for edge case (since we have 2 input fields)
|
|
Lisp.call("editor:set-focus-editor", edit.objectName)
|
|
}
|
|
|
|
onPressAndHold: Lisp.call("editor:copy-paste", edit.cursorPosition)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
height: rectCommand.height + rectOutput.height
|
|
SplitView.fillHeight: false // see comment in rectOutput
|
|
|
|
Rectangle {
|
|
id: rectCommand
|
|
objectName: "rect_command"
|
|
width: parent.width
|
|
height: command.font.pixelSize + 11
|
|
border.width: 2
|
|
border.color: command.focus ? "#0066ff" : "lightgray"
|
|
|
|
Ext.Flickable {
|
|
id: flickCommand
|
|
objectName: "flick_command"
|
|
anchors.fill: parent
|
|
contentWidth: command.paintedWidth
|
|
contentHeight: command.paintedHeight
|
|
|
|
TextEdit {
|
|
id: command
|
|
objectName: "command"
|
|
width: flickCommand.width
|
|
height: flickCommand.height
|
|
padding: 4
|
|
font.family: "Hack"
|
|
font.pixelSize: 18
|
|
selectionColor: "firebrick"
|
|
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhNoTextHandles | Qt.ImhNoEditMenu
|
|
cursorDelegate: cursor
|
|
focus: true
|
|
|
|
Keys.onUpPressed: Lisp.call("editor:history-move", "back")
|
|
Keys.onDownPressed: Lisp.call("editor:history-move", "forward")
|
|
Keys.onTabPressed: edit.forceActiveFocus()
|
|
|
|
onCursorRectangleChanged: flickCommand.ensureVisible(cursorRectangle)
|
|
|
|
Component.onCompleted: later(function() {
|
|
Lisp.call("editor:set-text-document", objectName, textDocument)
|
|
})
|
|
|
|
MouseArea {
|
|
width: Math.max(rectCommand.width, command.paintedWidth)
|
|
height: Math.max(rectCommand.height, command.paintedHeight)
|
|
|
|
onPressed: {
|
|
// seems necessary to consistently move cursor by tapping
|
|
command.forceActiveFocus()
|
|
command.cursorPosition = command.positionAt(mouse.x, mouse.y)
|
|
Qt.inputMethod.show() // needed for edge case (since we have 2 input fields)
|
|
Lisp.call("editor:set-focus-editor", command.objectName)
|
|
Lisp.call("editor:ensure-output-visible")
|
|
}
|
|
|
|
onPressAndHold: Lisp.call("editor:copy-paste", command.cursorPosition)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: rectOutput
|
|
objectName: "rect_output"
|
|
width: main.width
|
|
// calculate manually (for virtual keyboard)
|
|
height: main.availableHeight() - rectEdit.height - rectCommand.height - splitView.handleHeight
|
|
|
|
ListView {
|
|
id: output
|
|
objectName: "output"
|
|
anchors.fill: parent
|
|
contentWidth: parent.width * 5
|
|
clip: true
|
|
model: outputModel
|
|
flickableDirection: Flickable.HorizontalAndVerticalFlick
|
|
|
|
property string fontFamily: "Hack"
|
|
property int fontSize: 18
|
|
|
|
delegate: Column {
|
|
Rectangle {
|
|
width: output.contentWidth
|
|
height: model.line ? 2 : 0
|
|
color: "#c0c0ff"
|
|
}
|
|
|
|
Text {
|
|
x: 2
|
|
padding: 2
|
|
textFormat: Text.PlainText
|
|
font.family: output.fontFamily
|
|
font.pixelSize: output.fontSize
|
|
text: model.richText ? "" : model.text
|
|
color: model.color
|
|
font.bold: model.bold
|
|
visible: !model.richText
|
|
}
|
|
|
|
Text {
|
|
x: 2
|
|
padding: 2
|
|
textFormat: Text.RichText
|
|
font.family: output.fontFamily
|
|
font.pixelSize: output.fontSize
|
|
text: model.richText ? model.text : ""
|
|
color: model.color
|
|
font.bold: model.bold
|
|
visible: model.richText
|
|
|
|
MouseArea {
|
|
width: parent.paintedWidth
|
|
height: parent.paintedHeight
|
|
|
|
onPressed: {
|
|
// custom link handling, since 'onLinkActivated' does not work within a Flickable
|
|
var link = parent.linkAt(mouse.x, mouse.y)
|
|
if (link.length) {
|
|
Qt.openUrlExternally(link)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onFlickStarted: forceActiveFocus()
|
|
|
|
Component.onCompleted: later(function () {
|
|
Lisp.call("editor:delayed-ini")
|
|
})
|
|
}
|
|
|
|
ListModel {
|
|
id: outputModel
|
|
objectName: "output_model"
|
|
|
|
function appendOutput(text) {
|
|
append(text)
|
|
output.contentX = 0
|
|
output.positionViewAtEnd()
|
|
}
|
|
}
|
|
|
|
ProgressBar {
|
|
objectName: "progress"
|
|
width: main.width
|
|
z: 1
|
|
indeterminate: true
|
|
enabled: visible
|
|
visible: false
|
|
}
|
|
|
|
// move history buttons
|
|
|
|
Rectangle {
|
|
id: buttonsBottom
|
|
width: rowButtonsBottom.width
|
|
height: rowButtonsBottom.height
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
opacity: 0.7
|
|
visible: command.activeFocus
|
|
|
|
Row {
|
|
id: rowButtonsBottom
|
|
padding: 4
|
|
spacing: 6
|
|
|
|
Ext.MenuButton {
|
|
objectName: "history_back"
|
|
text: "\uf100"
|
|
}
|
|
Ext.MenuButton {
|
|
objectName: "history_forward"
|
|
text: "\uf101"
|
|
}
|
|
}
|
|
}
|
|
|
|
// paren buttons (above keyboard)
|
|
|
|
Rectangle {
|
|
objectName: "rect_paren_buttons"
|
|
width: rowParens.width
|
|
height: rowParens.height
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
color: "transparent"
|
|
visible: Qt.inputMethod.visible
|
|
|
|
Row {
|
|
id: rowParens
|
|
padding: -parenOpen.width / 30
|
|
spacing: -parenOpen.width / 6
|
|
|
|
Ext.ParenButton {
|
|
id: parenOpen
|
|
objectName: "paren_open"
|
|
icon.source: "img/paren-open.png"
|
|
onClicked: Lisp.call("editor:insert", "(")
|
|
}
|
|
Ext.ParenButton {
|
|
objectName: "paren_close"
|
|
icon.source: "img/paren-close.png"
|
|
onClicked: Lisp.call("editor:insert", ")")
|
|
onPressAndHold: Lisp.call("editor:close-all-parens")
|
|
}
|
|
}
|
|
}
|
|
|
|
// arrow buttons (cursor movement)
|
|
|
|
Rectangle {
|
|
id: rectArrows
|
|
objectName: "rect_arrows"
|
|
width: arrows.width + 20
|
|
height: width
|
|
anchors.right: rectOutput.right
|
|
anchors.bottom: rectOutput.bottom
|
|
color: "transparent"
|
|
visible: Qt.inputMethod.visible
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onPressed: Lisp.call("editor:ensure-focus")
|
|
}
|
|
|
|
Item {
|
|
id: arrows
|
|
width: up.width * 3
|
|
height: width
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
Ext.ArrowButton {
|
|
id: up
|
|
objectName: "up"
|
|
text: "\uf139"
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
Ext.ArrowButton {
|
|
objectName: "left"
|
|
text: "\uf137"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Ext.ArrowButton {
|
|
objectName: "right"
|
|
text: "\uf138"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
}
|
|
|
|
Ext.ArrowButton {
|
|
objectName: "down"
|
|
text: "\uf13a"
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: cursor
|
|
|
|
Rectangle {
|
|
width: 2
|
|
color: main.cursorColor
|
|
visible: parent.activeFocus
|
|
|
|
SequentialAnimation on opacity {
|
|
running: true
|
|
loops: Animation.Infinite
|
|
|
|
NumberAnimation { to: 0; duration: 500; easing.type: "OutQuad" }
|
|
NumberAnimation { to: 1; duration: 500; easing.type: "InQuad" }
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: buttonsTop
|
|
objectName: "buttons_top"
|
|
y: -height // hidden
|
|
width: rowButtonsTop.width
|
|
height: rowButtonsTop.height
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
opacity: 0.7
|
|
|
|
Row {
|
|
id: rowButtonsTop
|
|
padding: 4
|
|
spacing: 6
|
|
|
|
Ext.MenuButton {
|
|
objectName: "undo"
|
|
text: "\uf0e2"
|
|
enabled: edit.canUndo
|
|
}
|
|
Ext.MenuButton {
|
|
objectName: "redo"
|
|
text: "\uf01e"
|
|
enabled: edit.canRedo
|
|
}
|
|
Ext.MenuButton {
|
|
objectName: "font_smaller"
|
|
text: "\uf010"
|
|
font.pixelSize: main.small ? 10 : 15
|
|
}
|
|
Ext.MenuButton {
|
|
objectName: "font_bigger"
|
|
text: "\uf00e"
|
|
font.pixelSize: main.small ? 16 : 25
|
|
}
|
|
}
|
|
}
|
|
|
|
Ext.MenuButton {
|
|
id: showMenu
|
|
objectName: "show_menu"
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 4
|
|
y: 4
|
|
opacity: 0.7
|
|
text: "\uf142"
|
|
|
|
onClicked: {
|
|
showButtonsTop.start()
|
|
showButtonsRight.start()
|
|
menuTimer.start()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: menuTimer
|
|
objectName: "menu_timer"
|
|
interval: 3000
|
|
onTriggered: {
|
|
if (buttonsTop.y === 0) {
|
|
hideButtonsTop.start()
|
|
hideButtonsRight.start()
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: buttonsRight
|
|
objectName: "buttons_right"
|
|
x: -width // hidden
|
|
width: colButtonsRight.width
|
|
height: colButtonsRight.height
|
|
|
|
Column {
|
|
id: colButtonsRight
|
|
padding: 4
|
|
spacing: 6
|
|
|
|
Ext.Button {
|
|
objectName: "clear"
|
|
text: "\uf014"
|
|
}
|
|
Ext.Button {
|
|
objectName: "open_file"
|
|
text: "\uf115"
|
|
}
|
|
Ext.Button {
|
|
objectName: "save_file"
|
|
text: "\uf0c7"
|
|
}
|
|
Ext.Button {
|
|
objectName: "eval"
|
|
text: "\u03bb" // lambda
|
|
}
|
|
}
|
|
}
|
|
|
|
// animations for showing/hiding editor menu buttons
|
|
|
|
NumberAnimation {
|
|
id: showButtonsTop
|
|
objectName: "show_buttons_top"
|
|
target: buttonsTop
|
|
property: "y"
|
|
from: -buttonsTop.height
|
|
to: 0
|
|
duration: 500
|
|
easing.type: Easing.OutExpo
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: showButtonsRight
|
|
objectName: "show_buttons_right"
|
|
target: buttonsRight
|
|
property: "x"
|
|
from: buttonsRight.parent.width
|
|
to: buttonsRight.parent.width - buttonsRight.width
|
|
duration: 500
|
|
easing.type: Easing.OutExpo
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: hideButtonsTop
|
|
target: buttonsTop
|
|
property: "y"
|
|
from: 0
|
|
to: -buttonsTop.height
|
|
duration: 500
|
|
easing.type: Easing.InExpo
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: hideButtonsRight
|
|
target: buttonsRight
|
|
property: "x"
|
|
from: buttonsRight.parent.width - buttonsRight.width
|
|
to: buttonsRight.parent.width
|
|
duration: 500
|
|
easing.type: Easing.InExpo
|
|
}
|
|
}
|
|
|
|
// custom font loader
|
|
|
|
function loadFont(file) {
|
|
var font = Qt.createQmlObject("import QtQuick 2.15; FontLoader { source: '" + file + "' }", main)
|
|
return font.name
|
|
}
|
|
|
|
// not visible dialog / menu instances
|
|
|
|
Ext.QueryDialog { id: dialogQuery }
|
|
Ext.FileBrowser { id: dialogFile; opacity: 0 }
|
|
Ext.DebugDialog { id: dialogDebug; opacity: 0 }
|
|
Ext.Help { id: dialogHelp; opacity: 0 }
|
|
|
|
Ext.ClipboardMenu {}
|
|
|
|
// modal dialogs
|
|
|
|
Dlg.Dialogs {}
|
|
|
|
// dynamic QML items
|
|
|
|
Ext.Dynamic {}
|
|
}
|