add new example 'advanced-qml-auto-reload' (single file reload); revisions

This commit is contained in:
pls.153 2022-03-09 17:28:37 +01:00
parent 5f3b251a69
commit 1473ec8271
28 changed files with 1144 additions and 2 deletions

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,10 @@
(defsystem :app
:serial t
:depends-on ()
:components ((:file "lisp/package")
(: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,69 @@
LISP_FILES = $$files(lisp/*) app.asd make.lisp
android {
lisp.commands = $$(ECL_ANDROID)/../ecl-android-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:ios {
lisp.commands = $$(ECL_IOS)/../ecl-ios-host/bin/ecl \
-norc -shell $$PWD/make.lisp
} else:unix {
lisp.commands = /usr/local/bin/ecl -shell $$PWD/make.lisp
}
lisp.input = LISP_FILES
lisp.output = tmp/libapp.a
QMAKE_EXTRA_COMPILERS += lisp
PRE_TARGETDEPS += tmp/libapp.a
QT += quick qml
TEMPLATE = app
CONFIG += no_keywords release
DEFINES += INI_LISP
INCLUDEPATH = /usr/local/include
LIBS = -L/usr/local/lib -lecl
DESTDIR = .
TARGET = app
OBJECTS_DIR = tmp
MOC_DIR = tmp
linux: LIBS += -L../../../platforms/linux/lib
macx: LIBS += -L../../../platforms/macos/lib
android {
QT += androidextras
INCLUDEPATH = $$(ECL_ANDROID)/include
LIBS = -L$$(ECL_ANDROID)/lib -lecl
LIBS += -L../../../platforms/android/lib
ANDROID_ABIS = "arm64-v8a"
ANDROID_EXTRA_LIBS += $$(ECL_ANDROID)/lib/libecl.so
ANDROID_PACKAGE_SOURCE_DIR = ../platforms/android
}
ios {
DEFINES += INI_ECL_CONTRIB
INCLUDEPATH = $$(ECL_IOS)/include
ECL_VERSION = $$lower($$system($ECL_IOS/../ecl-ios-host/bin/ecl -v))
ECL_VERSION = $$replace(ECL_VERSION, " ", "-")
LIBS = -L$$(ECL_IOS)/lib -lecl
LIBS += -leclatomic -leclffi -leclgc -leclgmp
LIBS += -L$$(ECL_IOS)/lib/$$ECL_VERSION
LIBS += -lasdf -lecl-help -ldeflate -lecl-cdb -lecl-curl -lql-minitar -lsockets
LIBS += -L../../../platforms/ios/lib
assets.files = $$files($$PWD/platforms/ios/assets)
QMAKE_BUNDLE_DATA += assets
}
LIBS += -llqml -llisp -Ltmp -lapp
HEADERS += ../../src/cpp/main.h
SOURCES += ../../src/cpp/main.cpp
system(ecl -shell qml/.create-qml-loaders.lisp)
RESOURCES += $$files(qml/*)
RESOURCES += $$files(qml/.ext/*)
QMAKE_CXXFLAGS += -std=c++17

View file

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import os
total = 0
max_secs = 0
edited_file = ''
for root, dirs, files in os.walk('qml'):
for name in files:
if name.endswith('.qml'):
current_file = os.path.join(root, name)
secs = int(os.path.getctime(os.path.abspath(current_file)))
total += secs
if (secs > max_secs):
max_secs = secs
edited_file = current_file
print('Content-type:text/plain\r\n\r\n')
print(str(total) + '\r\n' + edited_file[4:])

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

@ -0,0 +1,181 @@
(defpackage :eval
(:use :cl :qml)
(:export
#:*eval-thread*
#:append-output
#:eval-in-thread))
(in-package :eval)
(defvar *output-buffer* (make-string-output-stream))
(defvar *prompt* t)
(defvar *eval-thread* nil)
(defvar * nil)
(defvar ** nil)
(defvar *** nil)
(defun ini-streams ()
(setf *standard-output* (make-broadcast-stream *standard-output*
*output-buffer*))
(setf *trace-output* *standard-output*
*error-output* *standard-output*))
(defun current-package-name ()
(if (eql (find-package :cl-user) *package*)
"CL-USER"
(car (sort (list* (package-name *package*) (package-nicknames *package*))
(lambda (x y) (< (length x) (length y)))))))
(let ((n -1))
(defun eval-in-thread (text &optional (progress t)) ; called from QML
(let ((str (string-trim " " text)))
(unless (x:empty-string str)
(if *prompt*
(let ((pkg (if (zerop n) "QML-USER" (current-package-name)))
(counter (princ-to-string (incf n))))
(format t "~A [~A]~%~A"
pkg
counter
str))
(format t "~%~%~A" str))
;; run eval in its own thread, so UI will remain responsive
(update-output t)
(when progress
(show-progress-bar))
(qsingle-shot 50 (lambda ()
(setf *eval-thread*
(mp:process-run-function "LQML REPL top-level"
(lambda () (do-eval str))))))))))
(defvar *color-text* "#c0c0c0")
(defvar *color-values* "#80b0ff")
(defvar *color-read-error* "orange")
(defvar *color-error* "#ff8080")
#+ios
(defun escape-smart-quotation (string)
(dotimes (i (length string))
(case (char-code (char string i))
((8216 8217 8218)
(setf (char string i) #\'))
((171 187 8220 8221 8222)
(setf (char string i) #\"))))
string)
(defun do-eval (string)
(let ((str #+ios (escape-smart-quotation string)
#-ios string)
(color *color-read-error*))
(handler-case
(let ((exp (read-from-string str)))
(setf color *color-error*)
(let ((vals (multiple-value-list (eval exp))))
(setf *** ** ** * * (first vals))
(update-output)
(append-output (format nil "~{~S~^~%~}" vals) *color-values* t))
(q! |clear| ui:*repl-input*)
(history-add str))
(condition (c)
(show-error c color))))
(qsingle-shot 50 'eval-exited))
(defun eval-exited ()
(update-output)
(show-progress-bar nil))
(defun show-error (error color)
(let ((e1 (prin1-to-string error))
(e2 (princ-to-string error)))
(append-output e1 color)
(unless (string= e1 e2)
(append-output e2 color))))
(defun show-progress-bar (&optional (show t))
(q> |visible| ui:*progress* show))
;;; output
(defun update-output (&optional line)
(let ((text (get-output-stream-string *output-buffer*)))
(unless (x:empty-string text)
(let ((err (search "[LQML:err]" text)))
(qjs |appendText| ui:*repl-model*
(list :m-text (if err (subseq text err) text)
:m-color (if err *color-error* *color-text*)
:m-bold nil
:m-line line))))))
(defun append-output (text &optional (color *color-text*) bold)
(qjs |appendText| ui:*repl-model*
(list :m-text text
:m-color color
:m-bold bold
:m-line nil)))
;;; command history
(defvar *history* (make-array 0 :adjustable t :fill-pointer t))
(defvar *history-index* nil)
(defvar *history-file* ".lqml-repl-history")
(defvar *max-history* 100)
(defun read-saved-history ()
(when (probe-file *history-file*)
(let ((i -1))
(labels ((index ()
(mod i *max-history*))
(next-index ()
(incf i)
(index)))
(let ((tmp (make-array *max-history*))) ; ring buffer
(with-open-file (s *history-file*)
(x:while-it (read-line s nil nil)
(setf (svref tmp (next-index)) x:it)))
(let ((max (min (1+ i) *max-history*)))
(when (< max *max-history*)
(setf i -1))
(dotimes (n max)
(vector-push-extend (svref tmp (next-index))
*history*))
(setf *history-index* (length *history*)))))))) ; 1 after last
(let (out)
(defun history-ini ()
(read-saved-history)
(setf out (open *history-file* :direction :output
:if-exists :append :if-does-not-exist :create)))
(defun history-add (line)
(unless out
(history-ini))
(let ((len (length *history*)))
(when (or (zerop len)
(string/= line (aref *history* (1- len))))
(vector-push-extend line *history*)
(write-line line out)
(finish-output out)))
(setf *history-index* (length *history*))) ; 1 after last
(defun history-move (direction)
(unless out
(history-ini))
(when (and *history-index*
(plusp (length *history*)))
(setf *history-index* (if (string= "back" direction)
(max (1- *history-index*) 0)
(min (1+ *history-index*) (1- (length *history*)))))
(let ((text (aref *history* *history-index*)))
(q> |text| ui:*repl-input* text)
(q> |cursorPosition| ui:*repl-input*
(- (length text) (if (x:ends-with ")" text) 1 0)))))))
(defun qml::help ()
(format t "~%~
~% :s (start-swank)~
~% :q (quicklisp)")
(values))
(progn
(ini-streams)
(qlater (lambda ()
(in-package :qml-user)
(eval-in-thread "(qml::help)" nil))))

View file

@ -0,0 +1,5 @@
(in-package :app)
#+(or android ios)
(when qml::*remote-ip*
(qsingle-shot 1000 'auto-reload-qml))

View file

@ -0,0 +1,4 @@
(defpackage :app
(:use :cl :qml)
(:export))

View file

@ -0,0 +1,66 @@
;;; trivial QML auto reload during development for mobile
(in-package :qml)
#+(or android ios)
(defvar *reload-all* nil)
(defvar *edited-file* nil)
#+(or android ios)
(defun remote-ip ()
(terpri *query-io*)
(princ "Please enter WiFi IP of desktop computer (hit RET to skip): "
*query-io*)
(let ((ip (read-line *query-io*)))
(unless (x:empty-string ip)
(format nil "http://~A:8080/" ip))))
#+(or android ios)
(defvar *remote-ip* #+interpreter nil
#-interpreter #.(remote-ip))
#+(or android ios)
(defun qml:view-status-changed (status)
(when (and (= 1 status)
(reload-main-p))
(load (make-string-input-stream
(funcall (%sym 'curl :qml)
(x:cc *remote-ip* "lisp/qml-reload/on-reloaded.lisp"))))))
#+(or android ios)
(defun reload-main-p ()
(or *reload-all*
(string= "main.qml" *edited-file*)))
#+(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/file (x:split (funcall (%sym 'curl :qml)
(x:cc *remote-ip* "cgi-bin/qml-last-modified.py"))
#.(coerce (list #\Return #\Newline) 'string))))
(when (= 2 (length curr/file))
(destructuring-bind (curr file)
curr/file
(setf curr (parse-integer curr)
*edited-file* (string-right-trim '(#\Return #\Newline) file))
(when (/= secs curr)
(if ini
(progn
(setf ini nil)
(qset *engine* |baseUrl| *remote-ip*)
(let ((src (qget *quick-view* |source|)))
(qset *quick-view* |source| (subseq src #.(length "qrc:///")))))
(if (reload-main-p)
(qml:reload)
(qjs |reload| *edited-file*)))
(setf secs curr)))))
(qsingle-shot 250 'auto-reload-qml)))
#+(or android ios)
(export 'auto-reload-qml)

View file

@ -0,0 +1,49 @@
;;; trivial QML auto reload during development (desktop only), see:
;;;
;;; lqml run.lisp -auto
(in-package :qml-user)
(defvar *reload-all* nil)
(defvar *edited-file* nil)
(defparameter *dir* *load-truename*)
(defun reload-main-p ()
(or *reload-all*
(string= "main.qml" *edited-file*)))
(defun qml:view-status-changed (status)
(when (and (= 1 status)
(reload-main-p))
(load (merge-pathnames "on-reloaded" *dir*))))
(let ((secs 0))
(defun watch-files ()
(let ((sum 0)
(max 0)
files)
;; don't cache, files might be added/removed during development
(dolist (file (directory (merge-pathnames "../../qml/**/*.qml" *dir*)))
(unless (search "/." (namestring file))
(push file files)))
(dolist (file files)
(let ((date (file-write-date file)))
(when (> date max)
(setf max date
*edited-file* file))
(incf sum date)))
(let ((edited (namestring *edited-file*)))
(setf *edited-file*
(subseq edited (+ 5 (search "/qml/" edited)))))
(when (/= secs sum)
(unless (zerop secs)
(if (reload-main-p)
(qml:reload)
(qjs |reload| *edited-file*)))
(setf secs sum)))
(qsingle-shot 250 'watch-files)))
(progn
(load "qml/.create-qml-loaders")
(watch-files))

View file

@ -0,0 +1,6 @@
;;; this file will be loaded every time QML has been reloaded
(in-package :qml-user)
;; delay needed here
(qsingle-shot 500 (lambda () (eval:eval-in-thread "(qml::help)"))) ; show help in REPL

View file

@ -0,0 +1,190 @@
;;; enable Swank and Quicklisp on mobile
(in-package :qml)
;; for mobile app updates:
;; to be incremented on every ECL upgrade in order to replace all asset files
#+(or android ios)
(defconstant +app-version+ 1)
#+(and ios (not interpreter))
(ffi:clines "extern void init_lib_ASDF(cl_object);")
#+(or android ios)
(defvar *assets* #+android "assets:/lib/"
#+ios "assets/")
#+ios
(defvar *bundle-root* (namestring *default-pathname-defaults*))
#+(or android ios)
(defun copy-asset-files (&optional (dir-name *assets*) origin)
"Copy asset files to home directory."
(flet ((directory-p (path)
(x:ends-with "/" path))
(translate (name)
#+android
(if (x:starts-with *assets* name)
(subseq name (length *assets*))
name)
#+ios
(namestring
(merge-pathnames (x:cc "../" (subseq name (length origin)))))))
(ensure-directories-exist (translate dir-name))
;; note: both QDIRECTORY and QCOPY-FILE are prepared for accessing
;; APK asset files, which can't be accessed directly from Lisp
(dolist (from (qdirectory dir-name))
(if (directory-p from)
(copy-asset-files from origin)
(let ((to (translate from)))
(when (probe-file to)
(delete-file to))
(unless (qcopy-file from to)
(qlog "Error copying asset file: ~S" from)
(return-from copy-asset-files))))))
t)
#+(or android ios)
(let ((file ".app-version"))
(defun app-version ()
(if (probe-file file)
(with-open-file (s file)
(let ((str (make-string (file-length s))))
(read-sequence str s)
(values (parse-integer str))))
0))
(defun save-app-version ()
(with-open-file (s file :direction :output :if-exists :supersede)
(princ +app-version+ s))
(values)))
(defun %sym (symbol package)
(intern (symbol-name symbol) package))
;;; Quicklisp setup
#+ios
(defun load-asdf ()
(unless (find-package :asdf)
;; needed for ASDF and Quicklisp
(setf (logical-pathname-translations "SYS")
(list (list "sys:**;*.*"
(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))
:asdf)
#+(or android ios)
(defun ensure-asdf ()
(unless (find-package :asdf)
#+android
(require :asdf)
#+ios
(load-asdf)))
#+(or android ios)
(defun quicklisp ()
(ensure-asdf)
(unless (find-package :quicklisp)
#+android
(progn
(require :ecl-quicklisp)
(require :deflate)
(require :ql-minitar))
#+ios
(load "quicklisp/setup")
;; replace interpreted function with precompiled one from DEFLATE
(setf (symbol-function (%sym 'gunzip :ql-gunzipper))
(symbol-function (%sym 'gunzip :deflate)))
(in-package :qml-user))
:quicklisp)
;;; Swank setup
#+(or android ios)
(defun swank/create-server (interface port dont-close style)
(funcall (%sym 'create-server :swank)
:interface interface
:port port
:dont-close dont-close
:style style))
#+(or android ios)
(defun start-swank (&key (port 4005) (interface "0.0.0.0") (style :spawn)
(load-contribs t) (setup t) (delete t) (quiet t)
(dont-close t) log-events)
(unless (find-package :swank)
(ensure-asdf)
(funcall (%sym 'load-system :asdf) :swank))
(funcall (%sym 'init :swank-loader)
:load-contribs load-contribs
:setup setup
:delete delete
:quiet quiet)
(setf (symbol-value (%sym '*log-events* :swank)) log-events)
(eval (read-from-string "(swank/backend:defimplementation swank/backend:lisp-implementation-program () \"org.lisp.ecl\")"))
(if (eql :spawn style)
(swank/create-server interface port dont-close style)
(mp:process-run-function
"SLIME-listener"
(lambda () (swank/create-server interface port dont-close style)))))
#+(or android ios)
(defun stop-swank (&optional (port 4005))
(when (find-package :swank)
(funcall (%sym 'stop-server :swank) port)
:stopped))
#+(or android ios)
(progn
;; be careful not to use :s, :q in your mobile app code
;; ios simulator note: wrap :s and :q in qrun* (would crash otherwise)
(define-symbol-macro :s (start-swank))
(define-symbol-macro :q (quicklisp)))
#+(or android ios)
(export (list #+ios
'load-asdf
'start-swank
'stop-swank
'quicklisp))
#+ios
(progn
;; adapt paths to iOS specific values
(defvar *user-homedir-pathname-orig* (symbol-function 'user-homedir-pathname))
(ext:package-lock :common-lisp nil)
(defun cl:user-homedir-pathname (&optional host)
(merge-pathnames "Library/" (funcall *user-homedir-pathname-orig* host)))
(ext:package-lock :common-lisp t)
(dolist (el '(("XDG_DATA_HOME" . "")
("XDG_CONFIG_HOME" . "")
("XDG_DATA_DIRS" . "")
("XDG_CONFIG_DIRS" . "")
("XDG_CACHE_HOME" . ".cache")))
(ext:setenv (car el) (namestring (merge-pathnames (cdr el)
(user-homedir-pathname))))))
;;; ini
#+(or android ios)
(defun startup-ini ()
#+ios
(setf *default-pathname-defaults* (user-homedir-pathname))
(ext:install-bytecodes-compiler)
(and (/= +app-version+ (app-version))
#+ios
(let ((dir (namestring (merge-pathnames *assets* *bundle-root*))))
(copy-asset-files dir dir))
#+android
(copy-asset-files)
(save-app-version)))
#+(or android ios)
(qlater 'startup-ini)

View file

@ -0,0 +1,20 @@
(defpackage ui
(:use :cl :qml)
(:export
#:*flick-output*
#:*history-back*
#:*history-forward*
#:*progress*
#:*repl-input*
#:*repl-output*
#:*repl-model*))
(in-package :ui)
(defparameter *flick-output* "flick_output")
(defparameter *history-back* "history_back")
(defparameter *history-forward* "history_forward")
(defparameter *progress* "progress")
(defparameter *repl-input* "repl_input")
(defparameter *repl-output* "repl_output")
(defparameter *repl-model* "repl_model")

View file

@ -0,0 +1,104 @@
;;; check target
(let ((arg (first (ext:command-args))))
(mapc (lambda (name feature)
(when (search name arg)
(pushnew feature *features*)))
(list "/ecl-android/" "/ecl-ios/")
(list :android :ios)))
;;; copy Swank and ECL contrib files (mobile only)
(defun cc (&rest args)
(apply 'concatenate 'string args))
#+(or android ios)
(defvar *assets* #+android "../platforms/android/assets/lib/"
#+ios "../platforms/ios/assets/Library/")
#+(or android ios)
(defun find-swank ()
(probe-file (cc *assets* "quicklisp/local-projects/slime/swank.lisp")))
#+(or android ios)
(defun shell (command)
(ext:run-program "sh" (list "-c" command)))
#+(or android ios)
(progn
(unless (find-swank)
(let ((to (cc *assets* "quicklisp/local-projects/slime/")))
(ensure-directories-exist to)
(shell (cc "cp -r ../../../slime/src/* " to))))
(unless (probe-file (cc *assets* "encodings"))
#+android
(let ((lib (cc (ext:getenv "ECL_ANDROID") "/lib/ecl-*/")))
(shell (cc "cp " lib "*.asd " *assets*))
(shell (cc "cp " lib "*.fas " *assets*))
(shell (cc "cp " lib "*.doc " *assets*))
(shell (cc "cp -r " lib "encodings " *assets*)))
#+ios
(let ((lib (cc (ext:getenv "ECL_IOS") "/lib/ecl-*/")))
(shell (cc "cp " lib "*.doc " *assets*))
(shell (cc "cp -r " lib "encodings " *assets*)))))
#+(or android ios)
(unless (find-swank)
(error "Swank files missing, please see <LQML root>/slime/src/readme-sources.md"))
;;; compile ASDF system
(require :asdf)
(push (merge-pathnames "../")
asdf:*central-registry*)
(setf *default-pathname-defaults*
(truename (merge-pathnames "../../../"))) ; LQML root
(defvar *current*
(let ((name (namestring *load-truename*)))
(subseq name
(length (namestring *default-pathname-defaults*))
(1+ (position #\/ name :from-end t)))))
;; load all LQML symbols
(dolist (file (list "package" "x" "ecl-ext" "ini" "qml"))
(load (merge-pathnames file "src/lisp/")))
#-(or android ios)
(progn
(asdf:make-build "app"
:monolithic t
:type :static-library
:move-here (cc *current* "build/tmp/")
:init-name "ini_app")
(let* ((from (cc *current* "build/tmp/app--all-systems.a"))
(to "libapp.a")
(to* (cc *current* "build/tmp/" to)))
(when (probe-file to*)
(delete-file to*))
(rename-file from to)))
#+(or android ios)
(progn
(pushnew :interpreter *features*)
(defvar *asdf-system* "app")
(defvar *ql-libs* (cc *current* "ql-libs.lisp"))
(defvar *init-name* "ini_app")
(defvar *library-name* (format nil "~Abuild-~A/tmp/app"
*current*
#+android "android"
#+ios "ios"))
(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

@ -0,0 +1,22 @@
(in-package :cl-user)
(defparameter *dir* *load-truename*)
(defvar *template* (with-open-file (s (merge-pathnames ".template.qml" *dir*))
(let ((str (make-string (file-length s))))
(read-sequence str s)
str)))
(defun create-qml-loaders ()
(dolist (file (directory (merge-pathnames "ext/**/*.qml" *dir*)))
(let* ((name (namestring file))
(p (1+ (search "/ext/" name)))
(loader (concatenate 'string (subseq name 0 p) "." (subseq name p))))
(unless (probe-file loader)
(ensure-directories-exist loader)
(with-open-file (s loader :direction :output)
(let ((new (subseq name p)))
(format t "~&creating .~A~%" new)
(format s *template* (subseq name p))))))))
(create-qml-loaders)

View file

@ -0,0 +1,15 @@
import QtQuick 2.15
Loader {
objectName: "~A"
source: "../" + objectName // TODO: set 'baseUrl' to Engine, so we don't need to fiddle here
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,13 @@
import QtQuick 2.15
Rectangle {
radius: 50
color: Qt.lighter("red", 1.5)
border.width: 10
border.color: "red"
Text {
anchors.centerIn: parent
text: "<h2>page 1</h2>"
}
}

View file

@ -0,0 +1,13 @@
import QtQuick 2.15
Rectangle {
radius: 50
color: Qt.lighter("green", 3.0)
border.width: 10
border.color: "green"
Text {
anchors.centerIn: parent
text: "<h2>page 2</h2>"
}
}

View file

@ -0,0 +1,13 @@
import QtQuick 2.15
Rectangle {
radius: 50
color: Qt.lighter("blue", 1.7)
border.width: 10
border.color: "blue"
Text {
anchors.centerIn: parent
text: "<h2>page 3</h2>"
}
}

View file

@ -0,0 +1,162 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
Item {
id: repl
z: 2
anchors.fill: parent
Row {
z: 1
anchors.right: parent.right
Text {
text: "REPL"
anchors.verticalCenter: show.verticalCenter
visible: !show.checked
}
Switch {
id: show
onCheckedChanged: container.enabled = checked
}
}
Column {
id: container
opacity: 0
Rectangle {
width: repl.parent.width
height: repl.parent.height / 4
color: "#101010"
ListView {
id: replOutput
objectName: "repl_output"
anchors.fill: parent
contentWidth: parent.width * 4
clip: true
model: replModel
flickableDirection: Flickable.HorizontalAndVerticalFlick
delegate: Column {
Rectangle {
width: replOutput.contentWidth
height: 1
color: "#707070"
visible: mLine
}
Text {
x: 2
padding: 2
textFormat: Text.PlainText
font.family: fontHack.name
font.pixelSize: 16
font.bold: mBold
text: mText
color: mColor
}
}
}
ListModel {
id: replModel
objectName: "repl_model"
function appendText(data) {
append(data)
replOutput.contentX = 0
replOutput.positionViewAtEnd()
}
}
}
Row {
width: repl.parent.width
TextField {
id: input
objectName: "repl_input"
width: repl.parent.width - 2 * back.width
font.family: fontHack.name
font.pixelSize: 16
font.bold: true
color: "#c0c0c0"
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
focus: show.checked
palette {
highlight: "#e0e0e0"
highlightedText: "#101010"
}
background: Rectangle {
color: "#101010"
border.width: 2
border.color: "gray"
}
onAccepted: Lisp.call("eval:eval-in-thread", text)
}
Button {
id: back
objectName: "history_back"
width: 40
height: input.height
focusPolicy: Qt.NoFocus
font.family: fontIcons.name
font.pixelSize: 26
text: "\uf100"
onClicked: Lisp.call("eval:history-move", "back")
}
Rectangle {
width: 1
height: input.height
color: "#101010"
}
Button {
id: forward
objectName: "history_forward"
width: back.width
height: input.height
focusPolicy: Qt.NoFocus
font.family: fontIcons.name
font.pixelSize: 26
text: "\uf101"
onClicked: Lisp.call("eval:history-move", "forward")
}
}
Rectangle {
width: repl.parent.width
height: 1
color: "#101010"
}
}
ProgressBar {
objectName: "progress"
anchors.top: container.bottom
width: repl.width
z: 1
indeterminate: true
enabled: visible
visible: false
}
states: [
State { when: show.checked; PropertyChanges { target: container; opacity: 0.9; y: 0 }},
State { when: !show.checked; PropertyChanges { target: container; opacity: 0.0; y: -height }}
]
transitions: [
Transition { NumberAnimation { properties: "opacity,y"; duration: 250; easing.type: Easing.InCubic }}
]
}

View file

@ -0,0 +1,46 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import ".ext/" as Ext // for single file auto reload (development)
//import "ext/" as Ext // release version
Rectangle {
width: 300
height: 500
color: "black"
SwipeView {
id: view
objectName: "view"
anchors.fill: parent
Rectangle {
color: "white"
Ext.Repl {}
Text {
anchors.centerIn: parent
text: "swipe for next page"
}
}
// N.B. don't use Loader inside a Repeater here, won't work with single
// file auto reload (which already uses a Loader)
Ext.Page1 {}
Ext.Page2 {}
Ext.Page3 {}
}
PageIndicator {
anchors.bottom: view.bottom
anchors.bottomMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
count: view.count
currentIndex: view.currentIndex
}
FontLoader { id: fontIcons; source: "fonts/fontawesome-webfont.ttf" }
FontLoader { id: fontHack; source: "fonts/Hack-Regular.ttf" }
FontLoader { id: fontHackBold; source: "fonts/Hack-Bold.ttf" }
}

View file

@ -0,0 +1,80 @@
Prepare
-------
Please copy the app template files first:
```
$ cd ..
$ ./copy.sh advanced-qml-auto-reload
```
See also [../../slime/src/readme-sources](../../slime/src/readme-sources.md) for
installing the Slime sources where this example can find them.
Info
----
This is simply an extended version of example **swank-sevrer** of the parent
directory.
It adds a QML `SwipeView` with 3 pages, to demonstrate how to reload single
QML pages, without reloading the whole UI. This is important for nested UI
pages, in order to not lose your current view in the UI.
Without the aboce feature, you would always land on the main view after
reloading QML.
QML single file auto reload on mobile
-------------------------------------
If you compile for mobile, it will ask for the **Wifi IP** of your desktop
computer (currently hard coded into the app). If you just hit RET, auto
reloading will be disabled.
N.B: **Before** 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, only
the saved file will be reloaded.
Only if you edit and save `qml/main.qml` 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.
If you don't want auto reload enabled (would block the app if the web server
isn't reachable), you need to rebuild the app and skip entering an IP during
the build process, see above.
Both desktop and mobile auto reload can also be run **simultaneously**, since
they share the QML source files. Any number of mobile devices may be connected,
if they are in the same WiFi and point to the same desktop IP.
Important notes for mobile
--------------------------
Please remember that installing a new version of your app on mobile will
**keep all app data** present on the device.
So, if you changed e.g. `lisp/curl.lisp`, a simple update will not replace
that file, because it has been copied from the assets directory, and is only
replaced if you increment `+app-version+` in `lisp/swank-quicklisp.lisp`.
A simple way to guarantee a clean install is simply uninstalling the app first,
both on the device and on the emulator (android) or simulator (iOS).
If your local web server doesn't seem to work, please check your firewall
settings.

View file

@ -0,0 +1,3 @@
ecl -shell qml/.create-qml-loaders.lisp # for single file reload
python3 -m http.server 8080 --cgi

View file

@ -55,7 +55,8 @@ QML auto reload on mobile
------------------------- -------------------------
If you compile for mobile, it will ask for the **Wifi IP** of your desktop If you compile for mobile, it will ask for the **Wifi IP** of your desktop
computer (currently hard coded into the app). computer (currently hard coded into the app). If you just hit RET, auto
reloading will be disabled.
After installing and launching the app, just run this script from your example After installing and launching the app, just run this script from your example
directory: directory:
@ -78,7 +79,9 @@ If you don't want auto reload enabled (would block the app if the web server
isn't reachable), comment out `auto-reload-qml` in `lisp/main.lisp`. isn't reachable), comment out `auto-reload-qml` in `lisp/main.lisp`.
Both desktop and mobile auto reload can also be run **simultaneously**, since Both desktop and mobile auto reload can also be run **simultaneously**, since
they share the QML source files. they share the QML source files. Any number of mobile devices may be connected,
if they are in the same WiFi and point to the same desktop IP.
Important notes for mobile Important notes for mobile

View file

@ -9,6 +9,24 @@ cross-platform apps. The same sources can be used to build executables for both
desktop (Linux/macOS) and mobile (android/iOS). desktop (Linux/macOS) and mobile (android/iOS).
QML auto reload
---------------
A new feature is auto reloading of QML files after saving any changes. This
works both on the desktop and on mobile.
As a concrete example, you may have running your app on the desktop, and have
both an android mobile device plus an iOS mobile device pointing to the IP of
the desktop. Now you will see any change to QML on all 3 screens
simultaneoulsy.
This even works (with some limitations, and only in this
[advanced example](examples/advanced-qml-auto-reload/)) at QML file level,
which means: only the QML file currently edited is reloaded, preserving the
state of all other QML files, and more importantly, the current view in case
of nested page structures.
License License
------- -------