add Qt6 version of 'cl-repl'

This commit is contained in:
pls.153 2024-10-26 12:58:33 +02:00
parent e8866954c0
commit 07f65d6a20
29 changed files with 1553 additions and 0 deletions

2
examples/Qt6/cl-repl/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,64 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.eql5.android.repl"
android:versionName="1.0.67"
android:versionCode="130"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<application
android:name="org.qtproject.qt.android.bindings.QtApplication"
android:label="cl-repl"
android:icon="@drawable/icon"
android:hardwareAccelerated="true"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:fullBackupOnly="false">
<activity
android:name="org.qtproject.qt.android.bindings.QtActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:label="cl-repl"
android:theme="@style/splashScreenTheme"
android:launchMode="singleTop"
android:screenOrientation="unspecified"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="app" />
<meta-data
android:name="android.app.arguments"
android:value="" />
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
<meta-data
android:name="android.app.splash_screen_drawable"
android:resource="@drawable/splashscreen"/>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.qtprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/qtprovider_paths"/>
</provider>
</application>
</manifest>

View file

@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 33 : 45
height: width
flat: true
focusPolicy: Qt.NoFocus
font.family: fontAwesome.name
font.pixelSize: 1.2 * width
opacity: 0.12
scale: 1.2
onPressed: Lisp.call(this, "editor:button-pressed")
onPressAndHold: Lisp.call(this, "editor:button-pressed-and-helt")
}

View file

@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 40 : 60
height: main.small ? 37 : 55
font.family: fontAwesome.name
font.pixelSize: main.small ? 25 : 36
focusPolicy: Qt.NoFocus
onPressed: Lisp.call(this, "editor:button-pressed")
}

View file

@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Ext
Popup {
objectName: "clipboard_menu"
x: (main.width - width) / 2
y: 4
Row {
id: menuButtonRow
spacing: 6
Ext.MenuButton {
objectName: "select_all"
text: "\uf07d"
}
Ext.MenuButton {
objectName: "cut"
text: "\uf0c4"
}
Ext.MenuButton {
objectName: "copy"
text: "\uf0c5"
}
Ext.MenuButton {
objectName: "paste"
text: "\uf0ea"
}
Ext.MenuButton {
objectName: "eval_exp"
text: "\u03bb" // lambda
}
}
}

View file

@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
import "." as Ext
Rectangle {
id: debugDialog
objectName: "debug_dialog"
color: "#f0f0f0"
visible: false
ColumnLayout {
anchors.fill: parent
spacing: 0
Ext.MenuBack {
id: menuBack
Layout.fillWidth: true
label: "Debug Dialog"
}
TextField {
id: debugInput
objectName: "debug_input"
Layout.fillWidth: true
font.family: "Hack"
font.pixelSize: 18
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
text: ":q"
onAccepted: Lisp.call("dialogs:exited")
}
Text {
id: label
Layout.fillWidth: true
leftPadding: 8
rightPadding: 8
topPadding: 8
bottomPadding: 8
font.family: "Hack"
font.pixelSize: 14
text: ":r1 etc. restart / :h help / :q quit"
}
Rectangle {
id: line
Layout.fillWidth: true
height: 1
color: "#d0d0d0"
}
ListView {
id: debugText
objectName: "debug_text"
Layout.fillWidth: true
Layout.fillHeight: true
contentWidth: parent.width * 5
clip: true
model: debugModel
flickableDirection: Flickable.HorizontalAndVerticalFlick
delegate: Text {
padding: 8
textFormat: Text.PlainText
font.pixelSize: 16
font.family: "Hack"
font.bold: model.bold
text: model.text
color: model.color
}
}
ListModel {
id: debugModel
objectName: "debug_model"
function appendOutput(data) {
append(data)
debugText.positionViewAtEnd()
}
}
}
}

View file

@ -0,0 +1,23 @@
import QtQuick
Item {
objectName: "dynamic"
property Component component
property Item item
function createItem(file) {
// for custom QML items to be loaded on top of REPL app
if (item != null) {
item.destroy()
}
Engine.clearCache()
var pre = (Qt.platform.os === "windows") ? "file:/" : "file://"
component = Qt.createComponent(pre + file)
if (component.status === Component.Ready) {
item = component.createObject()
return item
}
return null
}
}

View file

@ -0,0 +1,165 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import Qt.labs.folderlistmodel
import "." as Ext
Rectangle {
id: fileBrowser
objectName: "file_browser"
visible: false
property bool editMode: false
property string editFrom
function urlToString(url) {
var cut = (Qt.platform.os === "windows") ? "file:///" : "file://"
return url.toString().substring(cut.length)
}
Rectangle {
id: header
width: fileBrowser.width
height: headerColumn.height
z: 2
color: "#f0f0f0"
Column {
id: headerColumn
Ext.MenuBack {
id: menuBack
Row {
id: buttonRow
spacing: 4
anchors.horizontalCenter: parent.horizontalCenter
// one directory up
Ext.FileButton {
text: "\uf062"
onClicked: Lisp.call("dialogs:set-file-browser-path",
urlToString(folderModel.parentFolder))
}
// documents
Ext.FileButton {
text: "\uf0f6"
onClicked: Lisp.call("dialogs:set-file-browser-path", ":data")
}
// home
Ext.FileButton {
text: "\uf015"
onClicked: Lisp.call("dialogs:set-file-browser-path", ":home")
}
}
}
Ext.TextField {
id: path
objectName: "path"
width: fileBrowser.width
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
onFocusChanged: if (focus) { cursorPosition = length }
onAccepted: {
if (fileBrowser.editMode) {
Lisp.call("dialogs:rename-file*", fileBrowser.editFrom, path.text)
fileBrowser.editMode = false
} else {
Lisp.call("dialogs:set-file-name", text)
}
}
}
}
// edit mode
Ext.FileButton {
id: fileEdit
objectName: "file_edit"
anchors.right: parent.right
contentItem: Text {
id: editButton
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "\uf044"
color: fileBrowser.editMode ? "red" : "#007aff"
font.family: fontAwesome.name
font.pixelSize: 24
}
onClicked: fileBrowser.editMode = !fileBrowser.editMode
}
}
ListView {
id: folderView
objectName: "folder_view"
y: header.height
width: parent.width
height: parent.height - y
delegate: Ext.FileDelegate {}
currentIndex: -1 // no initial highlight
footerPositioning: ListView.OverlayHeader
property var colors: ["white", "#f0f0f0"]
model: FolderListModel {
id: folderModel
objectName: "folder_model"
showDirsFirst: true
showHidden: true
nameFilters: ["*.lisp", "*.lsp", "*.qml", "*.asd", "*.exp", "*.sexp",
"*.fas", "*.fasb", "*.fasc", ".eclrc", ".repl-history"]
onFolderChanged: path.text = urlToString(folder)
}
Row {
y: main.small ? 7 : 10
anchors.horizontalCenter: parent.horizontalCenter
spacing: 20
visible: Lisp.call("qml:mobile-p") ? path.focus : false
// cursor back
Ext.ArrowButton {
opacity: 0.15
text: "\uf137"
onPressed: path.cursorPosition--
onPressAndHold: path.cursorPosition = 0
}
// cursor forward
Ext.ArrowButton {
opacity: 0.15
text: "\uf138"
onPressed: path.cursorPosition++
onPressAndHold: path.cursorPosition = path.length
}
}
footer: Rectangle {
width: fileBrowser.width
height: itemCount.height + 4
z: 2
color: "lightgray"
border.width: 1
border.color: "gray"
Row {
anchors.fill: parent
Text {
id: itemCount
anchors.verticalCenter: parent.verticalCenter
text: Lisp.call("cl:format", null, " ~D item~P", folderModel.count, folderModel.count)
font.pixelSize: 18
}
}
}
}
}

View file

@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 42 : 48
height: width
font.family: fontAwesome.name
font.pixelSize: 24
flat: true
}

View file

@ -0,0 +1,64 @@
import QtQuick
Rectangle {
width: folderView.width
height: 48
color: (index === folderView.currentIndex) ? "lightskyblue" : folderView.colors[index & 1]
Row {
anchors.fill: parent
Text {
id: icon
width: 38
anchors.verticalCenter: parent.verticalCenter
font.family: fontAwesome.name
font.pixelSize: 24
text: fileIsDir ? " \uf115" : " \uf016"
}
Text {
width: 3/4 * folderView.width - icon.width
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 18
text: fileName
}
Text {
width: 1/4 * folderView.width - 4
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
font.pixelSize: 18
text: fileIsDir ? "" : Lisp.call("cl:format", null, "~:D", fileSize)
}
}
MouseArea {
anchors.fill: parent
onClicked: {
// highlight selected
folderView.currentIndex = index
Lisp.call("qml:qsleep", 0.1)
folderView.currentIndex = -1
if (fileBrowser.editMode) {
path.text = filePath
fileBrowser.editFrom = filePath
path.forceActiveFocus()
var start = filePath.lastIndexOf("/") + 1
var end = filePath.lastIndexOf(".")
if (end > start) {
path.cursorPosition = start
path.moveCursorSelection(end, TextInput.SelectCharacters)
}
} else {
if (fileIsDir) {
Lisp.call("dialogs:set-file-browser-path", filePath)
}
else {
fileBrowser.visible = false
Lisp.call("dialogs:set-file-name", filePath)
}
}
}
}
}

View file

@ -0,0 +1,23 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Ext
Flickable {
clip: true
ScrollBar.vertical: Ext.ScrollBar {}
function ensureVisible(r) {
if (main.skipEnsureVisible)
return;
if (contentX >= r.x)
contentX = r.x;
else if (contentX + width <= r.x + r.width)
contentX = r.x + r.width - width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY + height <= r.y + r.height)
contentY = r.y + r.height - height;
}
}

View file

@ -0,0 +1,119 @@
import QtQuick
import "." as Ext
Rectangle {
color: "lightyellow"
visible: false
Column {
anchors.fill: parent
Ext.MenuBack {
label: "Help"
}
Ext.Flickable {
id: flick
width: parent.width
height: parent.height
contentWidth: help.paintedWidth
contentHeight: help.paintedHeight + 100
Text {
id: help
width: flick.width
padding: 10
wrapMode: Text.WordWrap
font.pixelSize: 18
textFormat: Text.RichText
text:
"
<h3>Eval line commands</h3>
<table cellpadding=5>
<tr>
<td align=right><b>:?</b></td><td>find regular expression, e.g.<br><font face=\"Hack\">:?&nbsp;prin[c1]</font><br>hit RET for next match</td>
</tr>
<tr>
<td align=right><b>*</b></td><td>copy eval value to clipboard</td>
</tr>
<tr>
<td align=right><b>:c</b></td><td>clear all output</td>
</tr>
<tr>
<td align=right><b>:k</b></td><td>kill eval thread (long running task)</td>
</tr>
<tr>
<td align=right><b>:s</b></td><td>start Swank server</td>
</tr>
<tr>
<td align=right><b>:q</b></td><td>load Quicklisp</td>
</tr>
<tr>
<td align=right><b>:w</b></td><td>start local web-server for file upload/download, see<br><font face=\"Hack\">http://192.168.1.x:1701/</font>
<br>(not encrypted)</td>
</tr>
<tr>
<td align=right><b>:ws</b></td><td>stop local web-server</td>
</tr>
</table>
<br>
<h3>Special keys/taps</h3>
<table cellpadding=5>
<tr>
<td align=right><b>double SPC</b></td><td>auto completion, e.g.<b> m-v-b</b></td>
</tr>
<tr>
<td align=right><b>tap and hold</b></td><td>in editor to select/copy/paste/eval s-expression, e.g. on <b>defun</b></td>
</tr>
<tr>
<td align=right><b>tap and hold</b></td><td>cursor buttons to move to beginning/end of line/file</td>
</tr>
<tr>
<td align=right><b>hold ')'</b></td><td>(paren buttons) to close all open parens</td>
</tr>
</table>
<br>
<h3>Special functions</h3>
<table cellpadding=5>
<tr>
<td align=right><b>print</b></td>
<td>
<font face=\"Hack\">(ed:pr \"greetings\" :color \"red\" :bold t :line t)</font>
<br>pass <font face=\"Hack\"> :rich-text t </font> if you use (a subset of) <b>html</b>
</td>
</tr>
%2
%3
</table>
<br>
<h3>External keyboard</h3>
<table cellpadding=5>
<tr>
<td align=right><b>[Up]</b></td><td>move back in eval line history</td>
</tr>
<tr>
<td align=right><b>[Down]</b></td><td>move forward in eval line history</td>
</tr>
<tr>
<td align=right><b>[Tab]</b></td><td>switch focus between editor / eval line</td>
</tr>
<tr>
<td align=right><b>[%1+E]</b></td><td><b>E</b>xpression: select s-exp</td>
</tr>
<tr>
<td align=right><b>[%1+L]</b></td><td><b>L</b>ambda: eval selected s-exp</td>
</tr>
</table>
".arg((Qt.platform.os === "ios")
? "Alt" : ((Qt.platform.os === "osx")
? "Cmd" : "Ctrl"))
.arg((Qt.platform.os === "android")
? "<tr><td align=right><b>shell</b></td><td><font face=\"Hack\">(shell \"df -h\")</font></td></tr>"
: "")
.arg(((Qt.platform.os === "android") || (Qt.platform.os === "ios"))
? "<tr><td align=right><b>zip</b></td><td><font face=\"Hack\">(zip \"all.zip\" \"doc\")</font></td></tr><tr><td align=right><b>unzip</b></td><td><font face=\"Hack\">(unzip \"uploads/all.zip\" \"doc\")</font></td></tr>"
: "")
}
}
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Rectangle {
id: menuBack
width: main.width
height: backButton.height
color: "#f0f0f0"
property alias label: label.text
Button {
id: backButton
height: main.small ? 40 : 46
width: 80
background: Rectangle {
Text {
id: iconBack
x: 10
height: backButton.height
verticalAlignment: Text.AlignVCenter
font.family: fontAwesome.name
font.pixelSize: 32
color: "#007aff"
text: "\uf104"
}
Text {
x: 30
height: backButton.height * 1.1 // align correction (different font from above)
verticalAlignment: Text.AlignVCenter
font.pixelSize: 20
font.weight: Font.DemiBold
color: iconBack.color
text: "Repl"
visible: (Qt.platform.os === "ios")
}
implicitWidth: 90
color: menuBack.color
}
onPressed: Lisp.call("dialogs:exited")
}
Text {
id: label
anchors.centerIn: parent
font.pixelSize: 20
font.weight: Font.DemiBold
}
}

View file

@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 30 : 42
height: width
font.family: fontAwesome.name
font.pixelSize: main.small ? 20 : 28
focusPolicy: Qt.NoFocus
onPressed: Lisp.call(this, "editor:button-pressed")
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
id: parenButton
width: 1.4 * (main.small ? 35 : 55)
icon.width: width / 1.4
icon.height: height / 1.4
height: width
focusPolicy: Qt.NoFocus
flat: true
opacity: 0.12
onPressed: Lisp.call(this, "editor:button-pressed")
}

View file

@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Ext
Popup {
id: popup
x: 4
y: x
width: parent.width - 2 * x
height: queryInput.height + text.height + 24
closePolicy: Popup.NoAutoClose
onVisibleChanged: main.enabled = !visible
Rectangle {
id: queryDialog
objectName: "query_dialog"
anchors.fill: parent
color: "#f0f0f0"
Column {
id: column
width: parent.width
TextField {
id: queryInput
objectName: "query_input"
width: parent.width
font.family: "Hack"
font.pixelSize: 18
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
onAccepted: {
popup.close()
Lisp.call("dialogs:exited")
Lisp.call("editor:ensure-output-visible")
}
}
Text {
id: text
objectName: "query_text"
width: parent.width
padding: 8
font.pixelSize: 18
}
}
}
}

View file

@ -0,0 +1,33 @@
// This is a modified version taken from the QML sources
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
ScrollBar {
id: control
orientation: Qt.Vertical
contentItem: Rectangle {
implicitWidth: 12
implicitHeight: 100
radius: width / 2
color: control.pressed ? "#202020" : "#909090"
opacity: 0.0
states: State {
name: "active"
when: (control.active && control.size < 1.0)
PropertyChanges { target: control.contentItem; opacity: 0.75 }
}
transitions: Transition {
from: "active"
SequentialAnimation {
PauseAnimation { duration: 450 }
NumberAnimation { target: control.contentItem; duration: 200; property: "opacity"; to: 0.0 }
}
}
}
}

View file

@ -0,0 +1,11 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
TextField {
font.pixelSize: 18
palette {
highlight: "#007aff"
highlightedText: "white"
}
}

View file

@ -0,0 +1,12 @@
import QtQuick
import QtQuick.Dialogs
MessageDialog {
title: "LQML"
buttons: MessageDialog.Save | MessageDialog.Cancel
property string callback
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Dialog {
anchors.centerIn: parent
title: "Confirm"
font.pixelSize: 18
modal: true
standardButtons: Dialog.Save | Dialog.Cancel
property alias text: message.text
property string callback
Text {
id: message
width: parent.width // without width word wrap won't work
wrapMode: Text.Wrap
font.pixelSize: 18
}
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,36 @@
import QtQuick
Item {
id: dialogs
objectName: "dialogs"
anchors.fill: parent
Loader {
id: loader
anchors.centerIn: parent
}
function message(text) {
if (Lisp.call("qml:mobile-p")) {
loader.source = "MessageMobile.qml"
} else {
loader.source = "Message.qml"
}
loader.item.text = text
main.showKeyboard(false)
loader.item.open()
}
function confirm(title, text, callback) {
if (Lisp.call("qml:mobile-p")) {
loader.source = "ConfirmMobile.qml"
} else {
loader.source = "Confirm.qml"
}
loader.item.title = title
loader.item.text = text
loader.item.callback = callback
main.showKeyboard(false)
loader.item.open()
}
}

View file

@ -0,0 +1,7 @@
import QtQuick
import QtQuick.Dialogs
MessageDialog {
title: "Info"
buttons: MessageDialog.Ok
}

View file

@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Dialog {
anchors.centerIn: parent
title: "Info"
font.pixelSize: 18
modal: true
standardButtons: Dialog.Ok
property alias text: message.text
Text {
id: message
width: parent.width // without width word wrap won't work
wrapMode: Text.Wrap
font.pixelSize: 18
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,655 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Window
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
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 mainHeight() {
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 (mainHeight() - 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: (mouse) => {
// 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.mainHeight() - 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 {}
}