add QML auto reload for mobile (example 'swank-server' only); several revisions

This commit is contained in:
pls.153 2022-03-05 22:13:21 +01:00
parent 79a5e5cc30
commit 24c2a57fa0
25 changed files with 213 additions and 86 deletions

View file

@ -4,17 +4,17 @@
(in-package :qml-user)
(defvar *dir* *load-truename*)
(defun qml:view-status-changed (status)
(when (= 1 status)
;; any ini code goes here
;;(app:populate-item-model))
))
(load (merge-pathnames "on-reloaded" *dir*))))
(let ((secs 0)
files)
(defun watch-files ()
(unless files
(dolist (file (directory "qml/**/*.qml"))
(dolist (file (directory (merge-pathnames "../../qml/**/*.qml" *dir*)))
(push file files)))
(let ((curr 0))
(dolist (file files)
@ -26,3 +26,4 @@
(qsingle-shot 250 'watch-files)))
(watch-files)

View file

@ -0,0 +1,5 @@
;;; this file will be loaded every time QML has been reloaded
(in-package :qml-user)
;;(eval:eval-in-thread "(qml::help)")

View file

@ -8,7 +8,8 @@ Optionally pass `-slime` to start a Swank server, and connect from Emacs with
`M-x slime-connect`.
During development you can pass `-auto`, which will releoad all QML files after
you made a change to any of them and saved it, see `auto-reload-qml.lisp`.
you made a change to any of them and saved it. For re-initialization after
reloading, file `lisp/qml-reload/on-reloaded` will be loaded.
Closing the window quits the app. If you try to kill it with `ctrl-c`, you need
an additional `ctrl-d` to exit from ECL. To quit from Slime, do `(qq)` which is

View file

@ -17,7 +17,7 @@
;;; trivial auto reload of all QML files after saving any change
(when (option "-auto")
(load "auto-reload-qml"))
(load "lisp/qml-reload/auto-reload"))
;;; for Slime after copying 'lqml-start-swank.lisp' from LQML sources
;;; to your Slime directory, which is assumed to be '~/slime/'

View file

@ -49,7 +49,7 @@
(unless (copy-file from to)
(error "File ~A could not be copied." to))
#+unix
(when (string= "sh" (pathname-type to))
(when (find (pathname-type to) '("sh" "py") :test 'string=)
(ext:run-program "chmod" (list "+x" to)))))))
(terpri)

View file

@ -1,27 +0,0 @@
;;; trivial QML auto reload during development (desktop only), see:
;;;
;;; lqml run.lisp -auto
(in-package :qml-user)
(defun qml:view-status-changed (status)
(when (= 1 status)
;; any ini code goes here
(app:populate-item-model)))
(let ((secs 0)
files)
(defun watch-files ()
(unless files
(dolist (file (directory "qml/**/*.qml"))
(push file files)))
(let ((curr 0))
(dolist (file files)
(incf curr (file-write-date file)))
(when (/= secs curr)
(unless (zerop secs)
(qml:reload))
(setf secs curr)))
(qsingle-shot 250 'watch-files)))
(watch-files)

View file

@ -19,6 +19,7 @@
;;;
;;; (q! |remove| *planets* 0)
;;; (q! |clear| *planets*)
;;; (q! |setProperty| 0 "name" "First")
(defun populate-item-model ()
(q! |clear| *planets*)

View file

@ -0,0 +1,5 @@
;;; this file will be loaded every time QML has been reloaded
(in-package :qml-user)
(app:populate-item-model)

View file

@ -5,5 +5,6 @@
(:file "lisp/ui-vars")
(:file "lisp/swank-quicklisp")
(:file "lisp/eval")
(:file "lisp/qml-reload/auto-reload-mobile")
(:file "lisp/main")))

View file

@ -0,0 +1,28 @@
(in-package :qml)
(defun curl (url)
"args: (url)
Trivial download of UTF-8 encoded files, or binary files."
(multiple-value-bind (response headers stream)
(loop
(multiple-value-bind (response headers stream)
(ecl-curl::url-connection url)
(unless (member response '(301 302))
(return (values response headers stream)))
(close stream)
(setf url (header-value :location headers))))
(if (>= response 400)
(qlog "curl download error:" :url url :response response)
(let ((byte-array (make-array 0 :adjustable t :fill-pointer t
:element-type '(unsigned-byte 8)))
(type (pathname-type url)))
(x:while-it (read-byte stream nil nil)
(vector-push-extend x:it byte-array))
(close stream)
(if (or (search type "txt html lisp")
(search "/cgi-bin/" (namestring url)))
(qfrom-utf8 byte-array)
byte-array)))))
(export 'curl)

View file

@ -1,2 +1,4 @@
(in-package :app)
#+(or android ios)
(qsingle-shot 1000 'auto-reload-qml)

View file

@ -0,0 +1,44 @@
;;; trivial QML auto reload during development for mobile
(in-package :qml)
#+(or android ios)
(defvar *remote-ip* #+(and (not interpreter (or ios android)))
(format nil "http://~A:8080/"
#.(progn
(format *query-io* "~%Please enter WiFi IP of desktop computer: " *query-io*)
(read-line *query-io*)))
#-(or ios android) "http://localhost:8080/")
#+(or android ios)
(defun qml:view-status-changed (status)
(when (= 1 status)
(load (make-string-input-stream
(funcall (%sym 'curl :qml)
(x:cc *remote-ip* "lisp/qml-reload/on-reloaded.lisp"))))))
#+(or android ios)
(let ((load t)
(secs 0)
(ini t))
(defun auto-reload-qml ()
(when load
(setf load nil)
(require :ecl-curl)
(load "curl"))
(let ((curr (ignore-errors
(parse-integer
(funcall (%sym 'curl :qml)
(x:cc *remote-ip* "cgi-bin/qml-last-modified.py"))))))
(when (and curr (/= secs curr))
(when (plusp secs)
(if ini
(progn
(setf ini nil)
(qset *quick-view* |source| (x:cc *remote-ip* "qml/main.qml")))
(qml:reload)))
(setf secs curr)))
(qsingle-shot 250 'auto-reload-qml)))
#+(or android ios)
(export 'auto-reload-qml)

View file

@ -0,0 +1,5 @@
;;; this file will be loaded every time QML has been reloaded
(in-package :qml-user)
(eval:eval-in-thread "(qml::help)") ; show help in REPL

View file

@ -58,7 +58,6 @@
(princ +app-version+ s))
(values)))
#+(or android ios)
(defun %sym (symbol package)
(intern (symbol-name symbol) package))
@ -67,13 +66,11 @@
#+ios
(defun load-asdf ()
(unless (find-package :asdf)
;; needed for ASDF
;; needed for ASDF and Quicklisp
(setf (logical-pathname-translations "SYS")
(list (list "sys:**;*.*"
(merge-pathnames "**/*.*" (user-homedir-pathname)))))
;; needed for Quicklisp
(setf (logical-pathname-translations "HOME")
(list (list "home:**;*.*"
(merge-pathnames "**/*.*" (user-homedir-pathname)))
(list "home:**;*.*"
(merge-pathnames "**/*.*" (user-homedir-pathname)))))
(ffi:c-inline nil nil :void "ecl_init_module(NULL, init_lib_ASDF)" :one-liner t)
(in-package :qml-user))

View file

@ -93,3 +93,12 @@
(defvar *epilogue-code* nil)
(load "platforms/shared/make"))
;;; byte compile curl (delayed load)
#+(or android ios)
(progn
(require :ecl-curl)
(ext:install-bytecodes-compiler)
(compile-file (cc *current* "/lisp/curl.lisp")
:output-file (cc *current* "/lisp/" *assets* "curl.fasc")))

View file

@ -1,13 +1,13 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import "ext/"
import "ext/" as Ext
import Lisp 1.0
Item {
width: 300
height: 500
Repl {}
Ext.Repl {}
FontLoader { id: fontIcons; source: "fonts/fontawesome-webfont.ttf" }
FontLoader { id: fontHack; source: "fonts/Hack-Regular.ttf" }

View file

@ -29,7 +29,7 @@ mobile device after `M-x slime-connect`. You may need to detach your device
from USB for this to work.
**Quicklisp** note: it's always preferable to install Quicklisp and any library
from Slime on the desktop connected to the mobile device. Otherwise you don't
from Slime on the desktop connected to the mobile device. Otherwise you won't
see the progress or any eventual problem during the process.
@ -51,9 +51,25 @@ means:
TODO
----
QML auto reload on mobile
-------------------------
* add QML auto reload on mobile (easy to implement even without Swank, a local
trivial web server like `python3 -m http.server 8080` and periodic polling
from the mobile device is sufficient to implement it)
If you compile for mobile, it will ask for the **Wifi IP** of your desktop
computer (currently hard coded into the app).
After installing and launching the app, just run this script from your example
directory:
```
./web-server.sh
```
It requires Python 3 and the cgi module, which are probably already installed
on your computer.
You may now edit any QML file on the desktop computer, and upon saving, all of
QML will be reloaded automatically. After reloading, the following file will be
loaded for eventual re-initialization on Lisp side:
```
lisp/qml-reload/on-reloaded.lisp
```
For **android**, in order to see the debug output of eventual QML errors, you
need to run `./log.sh` in your `build-android/` directory.

View file

@ -1,7 +1,12 @@
*Please note: the below may be convenient for sketching; during development,
the simple `-auto` option of `lqml run.lisp` may be more appropriate, because
it allows for re-initialization (calling Lisp code) after reloading.*
*Please note: the below may be convenient for sketching -- but during
development, the simple `-auto` option of `lqml run.lisp` may be more
appropriate, because it allows for re-initialization (calling Lisp code) after
reloading QML.*
*The above method also works on mobile, please see example `swank-server`. It
doesn't depend on Swank, and should therefore also be stable (it uses a local
trivial web server).*
QML Preview and Slime

View file

@ -30,9 +30,20 @@ port still lacks significant parts of mobile (as of Qt6.2).
TODO
----
* add QML auto reload on mobile
* add sokoban example
* add CL REPL example
* add Windows platform
* port to CMake
macOS note
----------
Qt is (obviously) working perfectly well on Linux (and on Linux only, I dare to
say).
On macOS/iOS you'll always find some subtle bug, like a QQuickView not updating
after a property change through e.g. Slime.
In the above case you need to click on the QQuickView in order to update the
view; after that, it seems to work for subsequent property changes.

View file

@ -6,6 +6,9 @@
#ifndef ECL_FUN_PLUGIN
#define ECL_FUN_PLUGIN
#undef SLOT
#include <QUrl>
#include <QVariant>
#include <QObject>
#include <ecl/ecl.h>
@ -169,6 +172,18 @@ QByteArray toCString(cl_object l_str) {
return ba;
}
QByteArray toQByteArray(cl_object l_vec) {
QByteArray ba;
if (ECL_VECTORP(l_vec)) {
int len = LEN(l_vec);
ba.resize(len);
for (int i = 0; i < len; i++) {
ba[i] = toInt(ecl_aref(l_vec, i));
}
}
return ba;
}
QString toQString(cl_object l_str) {
QString s;
if (ECL_STRINGP(l_str)) {
@ -198,9 +213,11 @@ QVariantList toQVariantList(cl_object);
QVariant toQVariant(cl_object l_arg, int type) {
QVariant var;
switch (type) {
case QMetaType::QByteArray: var = toQByteArray(l_arg); break;
case QMetaType::QPointF: var = toQPointF(l_arg); break;
case QMetaType::QRectF: var = toQRectF(l_arg); break;
case QMetaType::QSizeF: var = toQSizeF(l_arg); break;
case QMetaType::QUrl: var = QUrl(toQString(l_arg)); break;
default:
if (cl_integerp(l_arg) == ECL_T) { // int
var = QVariant(toInt(l_arg));
@ -216,10 +233,11 @@ QVariant toQVariant(cl_object l_arg, int type) {
var = (cl_keywordp(cl_first(l_arg)) == ECL_T)
? toQVariantMap(l_arg)
: toQVariantList(l_arg);
} else if (cl_vectorp(l_arg) == ECL_T) { // vector (of octets)
var = QVariant(toQByteArray(l_arg));
} else { // default: undefined
var = QVariant();
}
break;
}
return var;
}
@ -298,7 +316,8 @@ cl_object from_qvariant(const QVariant& var) {
case QMetaType::QPointF: l_obj = from_qpointf(var.toPointF()); break;
case QMetaType::QRectF: l_obj = from_qrectf(var.toRectF()); break;
case QMetaType::QSizeF: l_obj = from_qsizef(var.toSizeF()); break;
case QMetaType::QString: l_obj = from_qstring(var.toString()); break;
case QMetaType::QString:
case QMetaType::QUrl: l_obj = from_qstring(var.toString()); break;
// special case (can be nested)
case QMetaType::QVariantList:
QVariantList list(var.value<QVariantList>());

View file

@ -7,7 +7,7 @@
#include <QStringList>
#include <QDebug>
const char LQML::version[] = "22.3.1"; // Mar 2022
const char LQML::version[] = "22.3.2"; // Mar 2022
extern "C" void ini_LQML(cl_object);
@ -101,7 +101,6 @@ void LQML::ignoreIOStreams() {
}
void LQML::exec(lisp_ini ini, const QByteArray& expression, const QByteArray& package) {
// see my_app example
ecl_init_module(NULL, ini);
eval(QString("(in-package :%1)").arg(QString(package)));
eval(expression);

View file

@ -32,13 +32,13 @@ public:
static LQML* me;
static QQuickView* quickView;
void exec(lisp_ini, const QByteArray& = "nil", const QByteArray& = "qml-user"); // see my_app example
void exec(lisp_ini, const QByteArray& = "nil", const QByteArray& = "qml-user");
void ignoreIOStreams();
void printVersion() {
eval("(multiple-value-bind (lqml qt)"
" (qml:qversion)"
" (format t \"LQML ~A (ECL ~A, Qt ~A)~%\" lqml (lisp-implementation-version) qt))");
" (format t \"LQML ~A (ECL ~A, Qt ~A)~%~%\" lqml (lisp-implementation-version) qt))");
}
Q_INVOKABLE void runOnUiThread(void*);

View file

@ -65,10 +65,13 @@ int main(int argc, char* argv[]) {
LQML lqml(argc, argv, &view);
if (arguments.contains("-v") || arguments.contains("--version")) {
lqml.printVersion();
std::cout << std::endl;
exit(0);
}
#ifdef Q_OS_WIN
lqml.ignoreIOStreams();
#endif
new QQmlFileSelector(view.engine(), &view);
QString qml("qml/main.qml");
QUrl url;
@ -116,15 +119,15 @@ int main(int argc, char* argv[]) {
#endif
#ifdef NO_QT_RESTART
bool slime = false;
bool qtRestart = false;
#else
bool slime = true;
bool qtRestart = true;
#endif
if (arguments.contains("-slime")
|| (arguments.indexOf(QRegularExpression(".*start-swank.*")) != -1)) {
arguments.removeAll("-slime");
slime = true;
qtRestart = true;
}
// load Lisp file
@ -134,7 +137,7 @@ int main(int argc, char* argv[]) {
LQML::eval(QString("(load \"%1\")").arg(file), true);
}
if (slime) {
if (qtRestart) {
LQML::eval("(qml::exec-with-qt-restart)", true);
return 0;
}

View file

@ -1,4 +1,5 @@
#include "marshal.h"
#include <QUrl>
#include <QVariant>
#include <QObject>
@ -142,6 +143,7 @@ QVariant toQVariant(cl_object l_arg, int type) {
case QMetaType::QPointF: var = toQPointF(l_arg); break;
case QMetaType::QRectF: var = toQRectF(l_arg); break;
case QMetaType::QSizeF: var = toQSizeF(l_arg); break;
case QMetaType::QUrl: var = QUrl(toQString(l_arg)); break;
default:
if (cl_integerp(l_arg) == ECL_T) { // int
var = QVariant(toInt(l_arg));
@ -162,7 +164,6 @@ QVariant toQVariant(cl_object l_arg, int type) {
} else { // default: undefined
var = QVariant();
}
break;
}
return var;
}
@ -276,7 +277,8 @@ cl_object from_qvariant(const QVariant& var) {
case QMetaType::QPointF: l_obj = from_qpointf(var.toPointF()); break;
case QMetaType::QRectF: l_obj = from_qrectf(var.toRectF()); break;
case QMetaType::QSizeF: l_obj = from_qsizef(var.toSizeF()); break;
case QMetaType::QString: l_obj = from_qstring(var.toString()); break;
case QMetaType::QString:
case QMetaType::QUrl: l_obj = from_qstring(var.toString()); break;
// special case (can be nested)
case QMetaType::QVariantList:
QVariantList list(var.value<QVariantList>());

View file

@ -49,12 +49,12 @@
(format nil "%~A%" (gensym)))
(defun qexec (&optional ms)
(%qexec ms))
(qrun* (%qexec ms)))
(defun qsleep (seconds)
"args: (seconds)
Similar to SLEEP, but continuing to process Qt events."
(%qexec (floor (* 1000 seconds)))
(qrun* (%qexec (floor (* 1000 seconds))))
nil)
(defmacro qsingle-shot (milliseconds function)
@ -191,19 +191,19 @@
(%qinvoke-method object function-name arguments))
(defmacro qget (object name)
`(%qget ,object ,(if (symbolp name)
`(qrun* (%qget ,object ,(if (symbolp name)
(symbol-name name)
name)))
name))))
(defmacro qset (object &rest arguments)
(assert (evenp (length arguments)))
`(%qset ,object ',(let (name)
`(qrun* (%qset ,object (list ,@(let (name)
(mapcar (lambda (x)
(setf name (not name))
(if (and name (symbolp x))
(symbol-name x)
x))
arguments))))
arguments))))))
(defun exec-with-qt-restart ()
;; for internal use; for conditions in Slime during Qt event loop processing
@ -218,7 +218,7 @@
normal program exit."
(declare (ignore kill-all-threads)) ; only here to be equivalent to EXT:QUIT
(assert (typep exit-status 'fixnum))
(%qquit exit-status))
(qrun* (%qquit exit-status)))
;;; android