diff --git a/examples/Qt6/cl-repl/.gitignore b/examples/Qt6/cl-repl/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/examples/Qt6/cl-repl/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/examples/Qt6/cl-repl/platforms/android/AndroidManifest.xml b/examples/Qt6/cl-repl/platforms/android/AndroidManifest.xml
new file mode 100644
index 0000000..2fab306
--- /dev/null
+++ b/examples/Qt6/cl-repl/platforms/android/AndroidManifest.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/Qt6/cl-repl/qml/ext/ArrowButton.qml b/examples/Qt6/cl-repl/qml/ext/ArrowButton.qml
new file mode 100644
index 0000000..8adab24
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/ArrowButton.qml
@@ -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")
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/Button.qml b/examples/Qt6/cl-repl/qml/ext/Button.qml
new file mode 100644
index 0000000..29ed116
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/Button.qml
@@ -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")
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/ClipboardMenu.qml b/examples/Qt6/cl-repl/qml/ext/ClipboardMenu.qml
new file mode 100644
index 0000000..d5a20ea
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/ClipboardMenu.qml
@@ -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
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/DebugDialog.qml b/examples/Qt6/cl-repl/qml/ext/DebugDialog.qml
new file mode 100644
index 0000000..20da257
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/DebugDialog.qml
@@ -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()
+ }
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/Dynamic.qml b/examples/Qt6/cl-repl/qml/ext/Dynamic.qml
new file mode 100644
index 0000000..eda1df2
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/Dynamic.qml
@@ -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
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/FileBrowser.qml b/examples/Qt6/cl-repl/qml/ext/FileBrowser.qml
new file mode 100644
index 0000000..f435820
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/FileBrowser.qml
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/FileButton.qml b/examples/Qt6/cl-repl/qml/ext/FileButton.qml
new file mode 100644
index 0000000..69592cb
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/FileButton.qml
@@ -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
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/FileDelegate.qml b/examples/Qt6/cl-repl/qml/ext/FileDelegate.qml
new file mode 100644
index 0000000..6f74a07
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/FileDelegate.qml
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/Flickable.qml b/examples/Qt6/cl-repl/qml/ext/Flickable.qml
new file mode 100644
index 0000000..5fd3ae4
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/Flickable.qml
@@ -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;
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/Help.qml b/examples/Qt6/cl-repl/qml/ext/Help.qml
new file mode 100644
index 0000000..d87c339
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/Help.qml
@@ -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:
+"
+
Eval line commands
+
+
+ | :? | find regular expression, e.g. :? prin[c1] hit RET for next match |
+
+
+ | * | copy eval value to clipboard |
+
+
+ | :c | clear all output |
+
+
+ | :k | kill eval thread (long running task) |
+
+
+ | :s | start Swank server |
+
+
+ | :q | load Quicklisp |
+
+
+ | :w | start local web-server for file upload/download, see http://192.168.1.x:1701/
+ (not encrypted) |
+
+
+ | :ws | stop local web-server |
+
+
+
+Special keys/taps
+
+
+ | double SPC | auto completion, e.g. m-v-b |
+
+
+ | tap and hold | in editor to select/copy/paste/eval s-expression, e.g. on defun |
+
+
+ | tap and hold | cursor buttons to move to beginning/end of line/file |
+
+
+ | hold ')' | (paren buttons) to close all open parens |
+
+
+
+Special functions
+
+
+ | print |
+
+ (ed:pr \"greetings\" :color \"red\" :bold t :line t)
+ pass :rich-text t if you use (a subset of) html
+ |
+
+%2
+%3
+
+
+External keyboard
+
+
+ | [Up] | move back in eval line history |
+
+
+ | [Down] | move forward in eval line history |
+
+
+ | [Tab] | switch focus between editor / eval line |
+
+
+ | [%1+E] | Expression: select s-exp |
+
+
+ | [%1+L] | Lambda: eval selected s-exp |
+
+
+".arg((Qt.platform.os === "ios")
+ ? "Alt" : ((Qt.platform.os === "osx")
+ ? "Cmd" : "Ctrl"))
+ .arg((Qt.platform.os === "android")
+ ? "| shell | (shell \"df -h\") |
"
+ : "")
+ .arg(((Qt.platform.os === "android") || (Qt.platform.os === "ios"))
+ ? "| zip | (zip \"all.zip\" \"doc\") |
| unzip | (unzip \"uploads/all.zip\" \"doc\") |
"
+ : "")
+ }
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/MenuBack.qml b/examples/Qt6/cl-repl/qml/ext/MenuBack.qml
new file mode 100644
index 0000000..6d0376c
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/MenuBack.qml
@@ -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
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/MenuButton.qml b/examples/Qt6/cl-repl/qml/ext/MenuButton.qml
new file mode 100644
index 0000000..a0da358
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/MenuButton.qml
@@ -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")
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/ParenButton.qml b/examples/Qt6/cl-repl/qml/ext/ParenButton.qml
new file mode 100644
index 0000000..8cd2d00
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/ParenButton.qml
@@ -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")
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/QueryDialog.qml b/examples/Qt6/cl-repl/qml/ext/QueryDialog.qml
new file mode 100644
index 0000000..f1f894e
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/QueryDialog.qml
@@ -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
+ }
+ }
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/ScrollBar.qml b/examples/Qt6/cl-repl/qml/ext/ScrollBar.qml
new file mode 100644
index 0000000..2a5ca6b
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/ScrollBar.qml
@@ -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 }
+ }
+ }
+ }
+}
+
diff --git a/examples/Qt6/cl-repl/qml/ext/TextField.qml b/examples/Qt6/cl-repl/qml/ext/TextField.qml
new file mode 100644
index 0000000..a36d373
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/TextField.qml
@@ -0,0 +1,11 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Basic
+
+TextField {
+ font.pixelSize: 18
+ palette {
+ highlight: "#007aff"
+ highlightedText: "white"
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/dialogs/Confirm.qml b/examples/Qt6/cl-repl/qml/ext/dialogs/Confirm.qml
new file mode 100644
index 0000000..701c2aa
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/dialogs/Confirm.qml
@@ -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)
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/dialogs/ConfirmMobile.qml b/examples/Qt6/cl-repl/qml/ext/dialogs/ConfirmMobile.qml
new file mode 100644
index 0000000..be867f2
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/dialogs/ConfirmMobile.qml
@@ -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)
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/dialogs/Dialogs.qml b/examples/Qt6/cl-repl/qml/ext/dialogs/Dialogs.qml
new file mode 100644
index 0000000..2e31871
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/dialogs/Dialogs.qml
@@ -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()
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/dialogs/Message.qml b/examples/Qt6/cl-repl/qml/ext/dialogs/Message.qml
new file mode 100644
index 0000000..67168d4
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/dialogs/Message.qml
@@ -0,0 +1,7 @@
+import QtQuick
+import QtQuick.Dialogs
+
+MessageDialog {
+ title: "Info"
+ buttons: MessageDialog.Ok
+}
diff --git a/examples/Qt6/cl-repl/qml/ext/dialogs/MessageMobile.qml b/examples/Qt6/cl-repl/qml/ext/dialogs/MessageMobile.qml
new file mode 100644
index 0000000..cbaf148
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/ext/dialogs/MessageMobile.qml
@@ -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
+ }
+}
diff --git a/examples/Qt6/cl-repl/qml/fonts/Hack-Bold.ttf b/examples/Qt6/cl-repl/qml/fonts/Hack-Bold.ttf
new file mode 100644
index 0000000..7ff4975
Binary files /dev/null and b/examples/Qt6/cl-repl/qml/fonts/Hack-Bold.ttf differ
diff --git a/examples/Qt6/cl-repl/qml/fonts/Hack-Regular.ttf b/examples/Qt6/cl-repl/qml/fonts/Hack-Regular.ttf
new file mode 100644
index 0000000..92a90cb
Binary files /dev/null and b/examples/Qt6/cl-repl/qml/fonts/Hack-Regular.ttf differ
diff --git a/examples/Qt6/cl-repl/qml/fonts/fontawesome-webfont.ttf b/examples/Qt6/cl-repl/qml/fonts/fontawesome-webfont.ttf
new file mode 100644
index 0000000..35acda2
Binary files /dev/null and b/examples/Qt6/cl-repl/qml/fonts/fontawesome-webfont.ttf differ
diff --git a/examples/Qt6/cl-repl/qml/img/paren-close.png b/examples/Qt6/cl-repl/qml/img/paren-close.png
new file mode 100644
index 0000000..41dadd8
Binary files /dev/null and b/examples/Qt6/cl-repl/qml/img/paren-close.png differ
diff --git a/examples/Qt6/cl-repl/qml/img/paren-open.png b/examples/Qt6/cl-repl/qml/img/paren-open.png
new file mode 100644
index 0000000..01f150a
Binary files /dev/null and b/examples/Qt6/cl-repl/qml/img/paren-open.png differ
diff --git a/examples/Qt6/cl-repl/qml/main.qml b/examples/Qt6/cl-repl/qml/main.qml
new file mode 100644
index 0000000..6599e68
--- /dev/null
+++ b/examples/Qt6/cl-repl/qml/main.qml
@@ -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 {}
+}