diff --git a/examples/advanced-qml-auto-reload/.gitignore b/examples/advanced-qml-auto-reload/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/advanced-qml-auto-reload/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/advanced-qml-auto-reload/app.asd b/examples/advanced-qml-auto-reload/app.asd new file mode 100644 index 0000000..beab393 --- /dev/null +++ b/examples/advanced-qml-auto-reload/app.asd @@ -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"))) + diff --git a/examples/advanced-qml-auto-reload/app.pro b/examples/advanced-qml-auto-reload/app.pro new file mode 100644 index 0000000..8f3e27d --- /dev/null +++ b/examples/advanced-qml-auto-reload/app.pro @@ -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 + diff --git a/examples/advanced-qml-auto-reload/cgi-bin/qml-last-modified.py b/examples/advanced-qml-auto-reload/cgi-bin/qml-last-modified.py new file mode 100755 index 0000000..721c6ad --- /dev/null +++ b/examples/advanced-qml-auto-reload/cgi-bin/qml-last-modified.py @@ -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:]) diff --git a/examples/advanced-qml-auto-reload/lisp/curl.lisp b/examples/advanced-qml-auto-reload/lisp/curl.lisp new file mode 100644 index 0000000..fda7670 --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/curl.lisp @@ -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) + diff --git a/examples/advanced-qml-auto-reload/lisp/eval.lisp b/examples/advanced-qml-auto-reload/lisp/eval.lisp new file mode 100644 index 0000000..dbaa731 --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/eval.lisp @@ -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)))) + diff --git a/examples/advanced-qml-auto-reload/lisp/main.lisp b/examples/advanced-qml-auto-reload/lisp/main.lisp new file mode 100644 index 0000000..a38aae8 --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/main.lisp @@ -0,0 +1,5 @@ +(in-package :app) + +#+(or android ios) +(when qml::*remote-ip* + (qsingle-shot 1000 'auto-reload-qml)) diff --git a/examples/advanced-qml-auto-reload/lisp/package.lisp b/examples/advanced-qml-auto-reload/lisp/package.lisp new file mode 100644 index 0000000..6689a82 --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/package.lisp @@ -0,0 +1,4 @@ +(defpackage :app + (:use :cl :qml) + (:export)) + diff --git a/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload-mobile.lisp b/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload-mobile.lisp new file mode 100644 index 0000000..a666eaf --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload-mobile.lisp @@ -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) diff --git a/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload.lisp b/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload.lisp new file mode 100644 index 0000000..8738789 --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/qml-reload/auto-reload.lisp @@ -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)) diff --git a/examples/advanced-qml-auto-reload/lisp/qml-reload/on-reloaded.lisp b/examples/advanced-qml-auto-reload/lisp/qml-reload/on-reloaded.lisp new file mode 100644 index 0000000..8be6b3e --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/qml-reload/on-reloaded.lisp @@ -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 diff --git a/examples/advanced-qml-auto-reload/lisp/swank-quicklisp.lisp b/examples/advanced-qml-auto-reload/lisp/swank-quicklisp.lisp new file mode 100644 index 0000000..ca8016c --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/swank-quicklisp.lisp @@ -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) diff --git a/examples/advanced-qml-auto-reload/lisp/ui-vars.lisp b/examples/advanced-qml-auto-reload/lisp/ui-vars.lisp new file mode 100644 index 0000000..20b81bf --- /dev/null +++ b/examples/advanced-qml-auto-reload/lisp/ui-vars.lisp @@ -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") diff --git a/examples/advanced-qml-auto-reload/make.lisp b/examples/advanced-qml-auto-reload/make.lisp new file mode 100644 index 0000000..624a8fc --- /dev/null +++ b/examples/advanced-qml-auto-reload/make.lisp @@ -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 /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"))) + diff --git a/examples/advanced-qml-auto-reload/qml/.create-qml-loaders.lisp b/examples/advanced-qml-auto-reload/qml/.create-qml-loaders.lisp new file mode 100644 index 0000000..dde001b --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/.create-qml-loaders.lisp @@ -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) diff --git a/examples/advanced-qml-auto-reload/qml/.template.qml b/examples/advanced-qml-auto-reload/qml/.template.qml new file mode 100644 index 0000000..5df39d0 --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/.template.qml @@ -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 + } +} diff --git a/examples/advanced-qml-auto-reload/qml/ext/Page1.qml b/examples/advanced-qml-auto-reload/qml/ext/Page1.qml new file mode 100644 index 0000000..a16b82b --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/ext/Page1.qml @@ -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: "

page 1

" + } +} diff --git a/examples/advanced-qml-auto-reload/qml/ext/Page2.qml b/examples/advanced-qml-auto-reload/qml/ext/Page2.qml new file mode 100644 index 0000000..9351061 --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/ext/Page2.qml @@ -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: "

page 2

" + } +} diff --git a/examples/advanced-qml-auto-reload/qml/ext/Page3.qml b/examples/advanced-qml-auto-reload/qml/ext/Page3.qml new file mode 100644 index 0000000..ae0fd76 --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/ext/Page3.qml @@ -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: "

page 3

" + } +} diff --git a/examples/advanced-qml-auto-reload/qml/ext/Repl.qml b/examples/advanced-qml-auto-reload/qml/ext/Repl.qml new file mode 100644 index 0000000..b4ed5e6 --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/ext/Repl.qml @@ -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 }} + ] +} diff --git a/examples/advanced-qml-auto-reload/qml/fonts/Hack-Bold.ttf b/examples/advanced-qml-auto-reload/qml/fonts/Hack-Bold.ttf new file mode 100644 index 0000000..7ff4975 Binary files /dev/null and b/examples/advanced-qml-auto-reload/qml/fonts/Hack-Bold.ttf differ diff --git a/examples/advanced-qml-auto-reload/qml/fonts/Hack-Regular.ttf b/examples/advanced-qml-auto-reload/qml/fonts/Hack-Regular.ttf new file mode 100644 index 0000000..92a90cb Binary files /dev/null and b/examples/advanced-qml-auto-reload/qml/fonts/Hack-Regular.ttf differ diff --git a/examples/advanced-qml-auto-reload/qml/fonts/fontawesome-webfont.ttf b/examples/advanced-qml-auto-reload/qml/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/examples/advanced-qml-auto-reload/qml/fonts/fontawesome-webfont.ttf differ diff --git a/examples/advanced-qml-auto-reload/qml/main.qml b/examples/advanced-qml-auto-reload/qml/main.qml new file mode 100644 index 0000000..cc5b94a --- /dev/null +++ b/examples/advanced-qml-auto-reload/qml/main.qml @@ -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" } +} diff --git a/examples/advanced-qml-auto-reload/readme.md b/examples/advanced-qml-auto-reload/readme.md new file mode 100644 index 0000000..0d2eefa --- /dev/null +++ b/examples/advanced-qml-auto-reload/readme.md @@ -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. diff --git a/examples/advanced-qml-auto-reload/web-server.sh b/examples/advanced-qml-auto-reload/web-server.sh new file mode 100755 index 0000000..2266a49 --- /dev/null +++ b/examples/advanced-qml-auto-reload/web-server.sh @@ -0,0 +1,3 @@ +ecl -shell qml/.create-qml-loaders.lisp # for single file reload + +python3 -m http.server 8080 --cgi diff --git a/examples/swank-server/readme.md b/examples/swank-server/readme.md index e357585..ae69b6a 100644 --- a/examples/swank-server/readme.md +++ b/examples/swank-server/readme.md @@ -55,7 +55,8 @@ QML 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). +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 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`. 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 diff --git a/readme.md b/readme.md index bd213dc..b01fc17 100644 --- a/readme.md +++ b/readme.md @@ -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). +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 -------