diff --git a/examples/debug-ui/app.asd b/examples/debug-ui/app.asd new file mode 100644 index 0000000..332f8f2 --- /dev/null +++ b/examples/debug-ui/app.asd @@ -0,0 +1,10 @@ +(defsystem :app + :serial t + :depends-on () + :components ((:file "lisp/package") + (:file "lisp/ui-vars") + (:file "lisp/d-dialogs") ; for debug-ui + (:file "lisp/d-input-hook") ; for debug-ui + (:file "lisp/d-debug-ui") ; for debug-ui + (:file "lisp/main"))) + diff --git a/examples/debug-ui/lisp/d-debug-ui.lisp b/examples/debug-ui/lisp/d-debug-ui.lisp new file mode 100644 index 0000000..84791b2 --- /dev/null +++ b/examples/debug-ui/lisp/d-debug-ui.lisp @@ -0,0 +1,59 @@ +(defpackage :debug-ui + (:use :cl :qml) + (:export + #:*debug-dialog*)) + +(in-package :debug-ui) + +(defvar *error-output-buffer* (make-string-output-stream)) +(defvar *terminal-out-buffer* (make-string-output-stream)) +(defvar *gui-debug-io* nil) +(defvar *gui-debug-dialog* nil) + +(defun ini () + (setf *gui-debug-dialog* 'dialogs:debug-dialog) + (ini-streams) + (setf *debug-io* *gui-debug-io*)) + +(defun ini-streams () + (setf *error-output* (make-broadcast-stream *error-output* + *error-output-buffer*)) + (setf *terminal-io* (make-two-way-stream (two-way-stream-input-stream *terminal-io*) + (make-broadcast-stream (two-way-stream-output-stream *terminal-io*) + *terminal-out-buffer*)) + *gui-debug-io* (make-two-way-stream (input-hook:add 'handle-debug-io) + (two-way-stream-output-stream *terminal-io*)))) + +(defun clear-buffers () + (dolist (s (list *error-output-buffer* + *terminal-out-buffer*)) + (get-output-stream-string s))) + +(defun find-quit-restart () + ;; find best restart for ':q' (default) to exit the debugger + (let ((restarts (compute-restarts))) + (if (= 1 (length restarts)) + ":r1" + (let ((restart-names (mapcar (lambda (r) + (symbol-name (restart-name r))) + restarts))) + ;; precedence role + (dolist (name '("RESTART-TOPLEVEL" + "ABORT" + "RESTART-QT-EVENTS")) + (x:when-it (position name restart-names :test 'string=) + (return-from find-quit-restart (format nil ":r~D" x:it))))))) + ":q") + +(defun handle-debug-io () + (let ((cmd (funcall *gui-debug-dialog* + (list (cons (get-output-stream-string *error-output-buffer*) + "#d00000") + (cons (get-output-stream-string *terminal-out-buffer*) + "black"))))) + (when (string-equal ":q" cmd) + (setf cmd (find-quit-restart))) + (format nil "~A~%" (if (x:empty-string cmd) ":q" cmd)))) + +(ini) + diff --git a/examples/debug-ui/lisp/d-dialogs.lisp b/examples/debug-ui/lisp/d-dialogs.lisp new file mode 100644 index 0000000..e87708f --- /dev/null +++ b/examples/debug-ui/lisp/d-dialogs.lisp @@ -0,0 +1,61 @@ +(defpackage :dialogs + (:use :cl :qml) + (:export + #:debug-dialog + #:exited + #:push-dialog + #:pop-dialog)) + +(in-package :dialogs) + +(defvar *callback* nil) + +(defun push-dialog (name) + "Pushes dialog NAME onto the StackView." + (qjs |pushDialog| ui:*main* (string-downcase name))) + +(defun pop-dialog () + "Pops the currently shown dialog, returning T if there was a dialog to pop." + (prog1 + (> (q< |depth| ui:*main*) 1) + (qjs |popDialog| ui:*main*))) + +(defun wait-while-transition () + ;; needed for evtl. recursive calls + (x:while (q< |busy| ui:*main*) + (qsleep 0.1))) + +(defun append-debug-output (text color bold) + (qjs |appendOutput| ui:*d-debug-model* + (list :text text + :color color + :bold bold))) + +(defun debug-dialog (messages) + (qrun* + (q! |clear| ui:*d-debug-model*) + (q> |text| ui:*d-debug-input* ":q") + (dolist (text/color messages) + (let* ((text (string-trim '(#\Newline) (car text/color))) + (color (cdr text/color)) + (bold (not (string= "black" color)))) ; boolean + (append-debug-output text color bold))) + (wait-while-transition) + (push-dialog :debug) + (q! |forceActiveFocus| ui:*d-debug-input*) + (qsingle-shot 500 (lambda () (q! |positionViewAtEnd| ui:*d-debug-text*))) + (wait-for-closed) + (q< |text| ui:*d-debug-input*))) + +(let (waiting) + (defun wait-for-closed () + (setf waiting t) + ;; busy waiting is safer than suspending a thread, especially on mobile + (x:while waiting + (qsleep 0.1)) + (pop-dialog)) + (defun exited () ; called from QML + (unless waiting + (pop-dialog)) + (setf waiting nil))) + diff --git a/examples/debug-ui/lisp/d-input-hook.lisp b/examples/debug-ui/lisp/d-input-hook.lisp new file mode 100644 index 0000000..040317e --- /dev/null +++ b/examples/debug-ui/lisp/d-input-hook.lisp @@ -0,0 +1,53 @@ +;;; idea & most code from "ecl-readline.lisp" + +(defpackage input-hook + (:use :cl) + (:export + #:add)) + +(provide :input-hook) + +(in-package :input-hook) + +(defvar *functions* nil) + +(defun add (function) + (let ((stream (make-instance 'input-hook-stream))) + (push (cons stream function) *functions*) + stream)) + +(defclass input-hook-stream (gray:fundamental-character-input-stream) + ((buffer :initform (make-string 0)) + (index :initform 0))) + +(defmethod gray:stream-read-char ((stream input-hook-stream)) + (if (ensure-stream-data stream) + (with-slots (buffer index) stream + (let ((ch (char buffer index))) + (incf index) + ch)) + :eof)) + +(defmethod gray:stream-unread-char ((stream input-hook-stream) character) + (with-slots (index) stream + (when (> index 0) + (decf index)))) + +(defmethod gray:stream-listen ((stream input-hook-stream)) + nil) + +(defmethod gray:stream-clear-input ((stream input-hook-stream)) + nil) + +(defmethod gray:stream-peek-char ((stream input-hook-stream)) + (if (ensure-stream-data stream) + (with-slots (buffer index) stream + (char buffer index)) + :eof)) + +(defun ensure-stream-data (stream) + (with-slots (buffer index) stream + (when (= index (length buffer)) + (setf buffer (funcall (cdr (assoc stream *functions*))) + index 0)) + buffer)) diff --git a/examples/debug-ui/lisp/main.lisp b/examples/debug-ui/lisp/main.lisp new file mode 100644 index 0000000..d37504c --- /dev/null +++ b/examples/debug-ui/lisp/main.lisp @@ -0,0 +1,4 @@ +(in-package :app) + +;; intentional division by zero after 5 seconds +(qsingle-shot 5000 (lambda () (dotimes (i 1) (/ 1 i)))) diff --git a/examples/debug-ui/lisp/package.lisp b/examples/debug-ui/lisp/package.lisp new file mode 100644 index 0000000..6689a82 --- /dev/null +++ b/examples/debug-ui/lisp/package.lisp @@ -0,0 +1,4 @@ +(defpackage :app + (:use :cl :qml) + (:export)) + diff --git a/examples/debug-ui/lisp/ui-vars.lisp b/examples/debug-ui/lisp/ui-vars.lisp new file mode 100644 index 0000000..dbcd0c4 --- /dev/null +++ b/examples/debug-ui/lisp/ui-vars.lisp @@ -0,0 +1,17 @@ +;;; keep sorted to recognize eventual name clashes + +(defpackage ui + (:use :cl) + (:export + #:*d-debug-input* + #:*d-debug-model* + #:*d-debug-text* + #:*main*)) + +(in-package :ui) + +(defparameter *d-debug-input* "debug_input") +(defparameter *d-debug-model* "debug_model") +(defparameter *d-debug-text* "debug_text") + +(defparameter *main* "main") diff --git a/examples/debug-ui/qml/debug/DebugDialog.qml b/examples/debug-ui/qml/debug/DebugDialog.qml new file mode 100644 index 0000000..2c579d9 --- /dev/null +++ b/examples/debug-ui/qml/debug/DebugDialog.qml @@ -0,0 +1,84 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +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/debug-ui/qml/debug/Flickable.qml b/examples/debug-ui/qml/debug/Flickable.qml new file mode 100644 index 0000000..bb8f0c7 --- /dev/null +++ b/examples/debug-ui/qml/debug/Flickable.qml @@ -0,0 +1,22 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +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/debug-ui/qml/debug/MenuBack.qml b/examples/debug-ui/qml/debug/MenuBack.qml new file mode 100644 index 0000000..69dbd83 --- /dev/null +++ b/examples/debug-ui/qml/debug/MenuBack.qml @@ -0,0 +1,53 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +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/debug-ui/qml/debug/ScrollBar.qml b/examples/debug-ui/qml/debug/ScrollBar.qml new file mode 100644 index 0000000..d23cbc4 --- /dev/null +++ b/examples/debug-ui/qml/debug/ScrollBar.qml @@ -0,0 +1,32 @@ +// This is a modified version taken from the QML sources + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +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/debug-ui/qml/debug/TextField.qml b/examples/debug-ui/qml/debug/TextField.qml new file mode 100644 index 0000000..45f70d5 --- /dev/null +++ b/examples/debug-ui/qml/debug/TextField.qml @@ -0,0 +1,10 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TextField { + font.pixelSize: 18 + palette { + highlight: "#007aff" + highlightedText: "white" + } +} diff --git a/examples/debug-ui/qml/fonts/Hack-Bold.ttf b/examples/debug-ui/qml/fonts/Hack-Bold.ttf new file mode 100644 index 0000000..7ff4975 Binary files /dev/null and b/examples/debug-ui/qml/fonts/Hack-Bold.ttf differ diff --git a/examples/debug-ui/qml/fonts/Hack-Regular.ttf b/examples/debug-ui/qml/fonts/Hack-Regular.ttf new file mode 100644 index 0000000..92a90cb Binary files /dev/null and b/examples/debug-ui/qml/fonts/Hack-Regular.ttf differ diff --git a/examples/debug-ui/qml/main.qml b/examples/debug-ui/qml/main.qml new file mode 100644 index 0000000..6f3fcf7 --- /dev/null +++ b/examples/debug-ui/qml/main.qml @@ -0,0 +1,40 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Window 2.15 +import 'debug/' as Dbg + +StackView { + id: main + objectName: "main" + width: 800 // alternatively: Screen.desktopAvailableWidth + height: 600 // alternatively: Screen.desktopAvailableHeight + initialItem: mainRect + Screen.orientationUpdateMask: Qt.LandscapeOrientation | Qt.PortraitOrientation | Qt.InvertedLandscapeOrientation + + // show/hide dialogs + + function pushDialog(name) { + switch (name) { + case "debug": main.push(dialogDebug); break + } + } + + function popDialog() { main.pop() } + + // 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 + color: "lavender" + } + + // dialogs + + Dbg.DebugDialog { id: dialogDebug } +} diff --git a/examples/debug-ui/readme.md b/examples/debug-ui/readme.md new file mode 100644 index 0000000..945bd04 --- /dev/null +++ b/examples/debug-ui/readme.md @@ -0,0 +1,12 @@ + +Info +---- + +This is a debug dialog (taken from example cl-repl) to be integrated in your +app. + +So, if you merge this example with your app, the mobile app will not crash on +an eventual runtime error: an interactive debug dialog will be shown instead. + +This is especially helpful on android, where Lisp issues are hard to debug once +the app is installed.