mirror of
https://gitlab.com/eql/lqml.git
synced 2025-12-05 18:20:33 -08:00
add new example 'debug-ui' (to be integrated in your mobile app)
This commit is contained in:
parent
ee72fc8847
commit
3b1934f3bf
16 changed files with 461 additions and 0 deletions
10
examples/debug-ui/app.asd
Normal file
10
examples/debug-ui/app.asd
Normal file
|
|
@ -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")))
|
||||||
|
|
||||||
59
examples/debug-ui/lisp/d-debug-ui.lisp
Normal file
59
examples/debug-ui/lisp/d-debug-ui.lisp
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
61
examples/debug-ui/lisp/d-dialogs.lisp
Normal file
61
examples/debug-ui/lisp/d-dialogs.lisp
Normal file
|
|
@ -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)))
|
||||||
|
|
||||||
53
examples/debug-ui/lisp/d-input-hook.lisp
Normal file
53
examples/debug-ui/lisp/d-input-hook.lisp
Normal file
|
|
@ -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))
|
||||||
4
examples/debug-ui/lisp/main.lisp
Normal file
4
examples/debug-ui/lisp/main.lisp
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
(in-package :app)
|
||||||
|
|
||||||
|
;; intentional division by zero after 5 seconds
|
||||||
|
(qsingle-shot 5000 (lambda () (dotimes (i 1) (/ 1 i))))
|
||||||
4
examples/debug-ui/lisp/package.lisp
Normal file
4
examples/debug-ui/lisp/package.lisp
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
(defpackage :app
|
||||||
|
(:use :cl :qml)
|
||||||
|
(:export))
|
||||||
|
|
||||||
17
examples/debug-ui/lisp/ui-vars.lisp
Normal file
17
examples/debug-ui/lisp/ui-vars.lisp
Normal file
|
|
@ -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")
|
||||||
84
examples/debug-ui/qml/debug/DebugDialog.qml
Normal file
84
examples/debug-ui/qml/debug/DebugDialog.qml
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
examples/debug-ui/qml/debug/Flickable.qml
Normal file
22
examples/debug-ui/qml/debug/Flickable.qml
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
examples/debug-ui/qml/debug/MenuBack.qml
Normal file
53
examples/debug-ui/qml/debug/MenuBack.qml
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
32
examples/debug-ui/qml/debug/ScrollBar.qml
Normal file
32
examples/debug-ui/qml/debug/ScrollBar.qml
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
10
examples/debug-ui/qml/debug/TextField.qml
Normal file
10
examples/debug-ui/qml/debug/TextField.qml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
font.pixelSize: 18
|
||||||
|
palette {
|
||||||
|
highlight: "#007aff"
|
||||||
|
highlightedText: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
examples/debug-ui/qml/fonts/Hack-Bold.ttf
Normal file
BIN
examples/debug-ui/qml/fonts/Hack-Bold.ttf
Normal file
Binary file not shown.
BIN
examples/debug-ui/qml/fonts/Hack-Regular.ttf
Normal file
BIN
examples/debug-ui/qml/fonts/Hack-Regular.ttf
Normal file
Binary file not shown.
40
examples/debug-ui/qml/main.qml
Normal file
40
examples/debug-ui/qml/main.qml
Normal file
|
|
@ -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 }
|
||||||
|
}
|
||||||
12
examples/debug-ui/readme.md
Normal file
12
examples/debug-ui/readme.md
Normal file
|
|
@ -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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue