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
:cclear all output
:kkill eval thread (long running task)
:sstart Swank server
:qload Quicklisp
:wstart local web-server for file upload/download, see
http://192.168.1.x:1701/ +
(not encrypted)
:wsstop local web-server
+
+

Special keys/taps

+ + + + + + + + + + + + + +
double SPCauto completion, e.g. m-v-b
tap and holdin editor to select/copy/paste/eval s-expression, e.g. on defun
tap and holdcursor buttons to move to beginning/end of line/file
hold ')'(paren buttons) to close all open parens
+
+

Special functions

+ + + + + +%2 +%3 +
print + (ed:pr \"greetings\" :color \"red\" :bold t :line t) +
pass :rich-text t if you use (a subset of) html +
+
+

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 {} +}