example 'meshtastic', add iOS version, background ini, startup animation

This commit is contained in:
pls.153 2023-06-06 11:37:12 +02:00
parent 1f72af5440
commit 0446628830
25 changed files with 181 additions and 111 deletions

2
examples/meshtastic/.gitignore vendored Normal file
View file

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

View file

@ -30,7 +30,7 @@ win32: PRE_TARGETDEPS = tmp/app.lib
QT += quick qml bluetooth QT += quick qml bluetooth
TEMPLATE = app TEMPLATE = app
CONFIG += c++17 no_keywords release CONFIG += c++17 no_keywords release
DEFINES += DESKTOP_APP INI_LISP INI_ECL_CONTRIB QT_EXTENSION DEFINES += DESKTOP_APP BACKGROUND_INI_LISP INI_ECL_CONTRIB QT_EXTENSION
INCLUDEPATH = /usr/local/include INCLUDEPATH = /usr/local/include
ECL_VERSION = $$lower($$system(ecl -v)) ECL_VERSION = $$lower($$system(ecl -v))
ECL_VERSION = $$replace(ECL_VERSION, " ", "-") ECL_VERSION = $$replace(ECL_VERSION, " ", "-")
@ -87,10 +87,10 @@ ios {
LIBS += -L../../../platforms/ios/lib LIBS += -L../../../platforms/ios/lib
QMAKE_INFO_PLIST = platforms/ios/Info.plist QMAKE_INFO_PLIST = platforms/ios/Info.plist
QMAKE_ASSET_CATALOGS += platforms/ios/Assets.xcassets #QMAKE_ASSET_CATALOGS += platforms/ios/Assets.xcassets
launch.files = platforms/ios/designable.storyboard platforms/img/logo.png #launch.files = platforms/ios/designable.storyboard platforms/img/logo.png
QMAKE_BUNDLE_DATA += launch #QMAKE_BUNDLE_DATA += launch
} }
32bit { 32bit {

View file

@ -17,8 +17,6 @@ BLE::BLE(const QBluetoothUuid& uuid) : mainServiceUuid(uuid) {
connect(discoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error), connect(discoveryAgent, QOverload<QBluetoothDeviceDiscoveryAgent::Error>::of(&QBluetoothDeviceDiscoveryAgent::error),
this, &BLE::deviceScanError); this, &BLE::deviceScanError);
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BLE::deviceScanFinished); connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BLE::deviceScanFinished);
QTimer::singleShot(0, this, &BLE::startDeviceDiscovery);
} }
void BLE::startDeviceDiscovery() { void BLE::startDeviceDiscovery() {
@ -60,8 +58,7 @@ void BLE::scanServices() {
if (controller && (previousAddress != currentDevice.address())) { if (controller && (previousAddress != currentDevice.address())) {
Q_EMIT deviceDisconnecting(); Q_EMIT deviceDisconnecting();
controller->disconnectFromDevice(); controller->disconnectFromDevice();
delete controller; delete controller; controller = nullptr;
controller = nullptr;
} }
if (!controller) { if (!controller) {

View file

@ -35,10 +35,11 @@ Q_SIGNALS:
void mainServiceReady(); void mainServiceReady();
void deviceDisconnecting(); void deviceDisconnecting();
/*** </INTERFACE> *********************************************************/
public Q_SLOTS: public Q_SLOTS:
void startDeviceDiscovery(); void startDeviceDiscovery();
/*** </INTERFACE> *********************************************************/
void scanServices(); void scanServices();
void connectToService(const QString&); void connectToService(const QString&);
void disconnectFromDevice(); void disconnectFromDevice();

View file

@ -16,6 +16,11 @@ QT::QT() : QObject() {
ble = new BLE_ME; ble = new BLE_ME;
} }
QVariant QT::startDeviceDiscovery() {
ble->startDeviceDiscovery();
return QVariant();
}
QVariant QT::read2() { QVariant QT::read2() {
ble->read(); ble->read();
return QVariant(); return QVariant();

View file

@ -19,6 +19,7 @@ class QT : public QObject {
public: public:
// BLE_ME // BLE_ME
Q_INVOKABLE QVariant startDeviceDiscovery();
Q_INVOKABLE QVariant read2(); Q_INVOKABLE QVariant read2();
Q_INVOKABLE QVariant write2(const QVariant&); Q_INVOKABLE QVariant write2(const QVariant&);

View file

@ -2,8 +2,9 @@
(defun ini () (defun ini ()
(qt:ini) (qt:ini)
(qt:start-device-discovery qt:*ble*)
(msg:load-messages) (msg:load-messages)
(q> |visible| ui:*hour-glass* nil) ; shown during Lisp startup (q> |playing| ui:*loading* nil) ; shown during Lisp startup
(q> |playing| ui:*busy* t)) ; shown during BLE setup (q> |playing| ui:*busy* t)) ; shown during BLE setup
(qlater 'ini) (qlater 'ini)

View file

@ -7,19 +7,19 @@
(defun add-message (message &optional loading) (defun add-message (message &optional loading)
"Adds passed MESSAGE (a PLIST) to both the QML item model and *MESSAGES*. "Adds passed MESSAGE (a PLIST) to both the QML item model and *MESSAGES*.
The model keys are: The model keys are:
:m-text :m-sender :m-timestamp :m-id :m-ack-state" :text :sender :me :timestamp :mid :ack-state"
(qjs |addMessage| ui:*messages* message) (qjs |addMessage| ui:*messages* message)
(unless loading (unless loading
(push message *messages*) (push message *messages*)
(qlater 'save-messages))) (qlater 'save-messages)))
(defun change-state (state id) (defun change-state (state mid)
(let ((i-state (position state *states*))) (let ((i-state (position state *states*)))
(qjs |changeState| ui:*messages* (qjs |changeState| ui:*messages*
i-state id) i-state mid)
(dolist (msg *messages*) (dolist (msg *messages*)
(when (eql (getf msg :m-id) id) ; EQL: might be NIL (when (eql (getf msg :mid) mid) ; EQL: might be NIL
(setf (getf msg :m-ack-state) i-state) (setf (getf msg :ack-state) i-state)
(return)))) (return))))
(qlater 'save-messages)) (qlater 'save-messages))
@ -31,7 +31,7 @@
(with-open-file (s *file*) (with-open-file (s *file*)
(setf *messages* (read s))) (setf *messages* (read s)))
(dolist (msg (reverse *messages*)) (dolist (msg (reverse *messages*))
(setf *message-id* (max (or (getf msg :m-id) 0) (setf *message-id* (max (or (getf msg :mid) 0)
*message-id*)) *message-id*))
(add-message msg t)))) (add-message msg t))))

View file

@ -3,6 +3,7 @@
(:export (:export
#:*ble* #:*ble*
#:ini #:ini
#:start-device-discovery
#:read* #:read*
#:write*)) #:write*))

View file

@ -39,8 +39,6 @@
(values)) (values))
(defun start-config () (defun start-config ()
#+android
(ensure-permissions :access-coarse-location) ; needed for BLE
(when *ready* (when *ready*
(incf *config-id*) (incf *config-id*)
(send-to-radio (send-to-radio
@ -60,11 +58,12 @@
:portnum :text-message-app :portnum :text-message-app
:payload (babel:string-to-octets text))))) :payload (babel:string-to-octets text)))))
(msg:add-message (msg:add-message
(list :m-text text (list :text text
:m-sender (me:short-name (me:user *my-node-info*)) :sender (me:short-name (me:user *my-node-info*))
:m-timestamp (timestamp-to-string) :me t
:m-id msg:*message-id* :timestamp (timestamp-to-string)
:m-ack-state (position :not-received msg:*states*)))) :mid msg:*message-id*
:ack-state (position :not-received msg:*states*))))
(defun read-radio () (defun read-radio ()
"Triggers a read on the radio. Will call RECEIVED-FROM-RADIO on success." "Triggers a read on the radio. Will call RECEIVED-FROM-RADIO on success."
@ -89,9 +88,10 @@
(push from-radio *received*))) (push from-radio *received*)))
(values)) (values))
(defun receiving-done () (defun receiving-done () ; called from Qt
(setf *reading* nil) (setf *reading* nil)
(process-received)) (process-received)
(values))
(defun node-to-name (num) (defun node-to-name (num)
(dolist (info *node-infos*) (dolist (info *node-infos*)
@ -115,10 +115,10 @@
;; text-message ;; text-message
(:text-message-app (:text-message-app
(msg:add-message (msg:add-message
(list :m-text (babel:octets-to-string payload) (list :text (babel:octets-to-string payload)
:m-sender (node-to-name (me:from packet)) :sender (node-to-name (me:from packet))
:m-timestamp (timestamp-to-string)))) :timestamp (timestamp-to-string))))
;; for :m-ack-state (acknowledgement state) ;; for :ack-state (acknowledgement state)
(:routing-app (:routing-app
(msg:change-state (case (me:routing.error-reason (msg:change-state (case (me:routing.error-reason
(pr:deserialize-from-bytes 'me:routing payload)) (pr:deserialize-from-bytes 'me:routing payload))
@ -148,8 +148,6 @@
((me:from-radio.has-config-complete-id struct) ((me:from-radio.has-config-complete-id struct)
(when (= *config-id* (me:config-complete-id struct)) (when (= *config-id* (me:config-complete-id struct))
(qlater 'config-device) (qlater 'config-device)
(q> |myName| ui:*view*
(me:short-name (me:user *my-node-info*)))
(q> |playing| ui:*busy* nil) (q> |playing| ui:*busy* nil)
(qlog :config-complete *config-id*))))) (qlog :config-complete *config-id*)))))
(setf *received* nil)) (setf *received* nil))

View file

@ -4,14 +4,14 @@
(:use :cl) (:use :cl)
(:export (:export
#:*busy* #:*busy*
#:*hour-glass* #:*loading*
#:*messages* #:*messages*
#:*view*)) #:*view*))
(in-package :ui) (in-package :ui)
(defparameter *busy* "busy") (defparameter *busy* "busy")
(defparameter *hour-glass* "hour_glass") (defparameter *loading* "loading")
(defparameter *messages* "messages") (defparameter *messages* "messages")
(defparameter *view* "view") (defparameter *view* "view")

View file

@ -91,35 +91,3 @@
(delete-file to*)) (delete-file to*))
(rename-file from to)) (rename-file from to))
;;; build 'cl-protobufs.fas' (slow on mobile, will be loaded in background)
#|
#-mobile
(asdf:make-build "my-cl-protobufs"
:monolithic t
:type :fasl
:move-here (cc *current* "build/tmp/"))
#+mobile
(progn
(pushnew :interpreter *features*)
(defvar *asdf-system* "my-cl-protobufs")
(defvar *ql-libs* (cc *current* "ql-libs.lisp"))
(defvar *build-type* :fasl)
(defvar *library-path* (format nil "~Abuild-~A/tmp/"
*current*
#+android "android"
#+ios "ios"))
(load "platforms/shared/make"))
;;; rename lib
(let* ((from #-mobile (cc *current* "build/tmp/my-cl-protobufs--all-systems.fasb")
#+mobile (cc *library-path* "my-cl-protobufs--all-systems.fasb"))
(to "cl-protobufs.fas")
(to* #-mobile (cc *current* "build/tmp/" to)
#+mobile (cc *library-path* to)))
(when (probe-file to*)
(delete-file to*))
(rename-file from to))
|#

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${QMAKE_SHORT_VERSION}</string>
<key>CFBundleSignature</key>
<string>${QMAKE_PKGINFO_TYPEINFO}</string>
<key>CFBundleVersion</key>
<string>${QMAKE_FULL_VERSION}</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>${IPHONEOS_DEPLOYMENT_TARGET}</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>For connecting to meshtastic radio devices.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -16,7 +16,7 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: "#e5d8bd" color: loading.visible ? "#1974d3" : "#e5d8bd"
} }
ListView { ListView {
@ -28,27 +28,32 @@ Item {
spacing: 3 spacing: 3
delegate: messageDelegate delegate: messageDelegate
model: messages model: messages
property string myName
} }
ListModel { ListModel {
id: messages id: messages
objectName: "messages" objectName: "messages"
// hack to define all model key _types_
ListElement {
text: ""; sender: ""; me: true; timestamp: ""; mid: 0; ackState: 0
}
function addMessage(message) { function addMessage(message) {
append(message) append(message)
view.positionViewAtEnd() view.positionViewAtEnd()
} }
function changeState(state, id) { function changeState(state, mid) {
for (var i = count - 1; i >= 0; i--) { for (var i = count - 1; i >= 0; i--) {
if (get(i).mId === id) { if (get(i).mid === mid) {
setProperty(i, "mAckState", state) setProperty(i, "ackState", state)
break break
} }
} }
} }
Component.onCompleted: remove(0) // see hack above
} }
Component { Component {
@ -61,7 +66,7 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: (mSender === view.myName) ? "#f2f2f2" : "#ffffcc" color: model.me ? "#f2f2f2" : "#ffffcc"
radius: 12 radius: 12
border.width: 0 border.width: 0
border.color: "#dc1128" border.color: "#dc1128"
@ -78,8 +83,8 @@ Item {
width: 8 width: 8
height: width height: width
source: "img/semaphore.gif" source: "img/semaphore.gif"
currentFrame: mAckState currentFrame: model.ackState
visible: (sender.text === view.myName) visible: model.me
} }
Text { Text {
@ -88,7 +93,7 @@ Item {
font.bold: true font.bold: true
font.family: fontMono.name font.family: fontMono.name
color: "#8B0000" color: "#8B0000"
text: mSender text: model.sender
} }
} }
@ -99,7 +104,7 @@ Item {
font.pixelSize: 10 font.pixelSize: 10
font.family: fontText.name font.family: fontText.name
color: "#505050" color: "#505050"
text: mTimestamp text: model.timestamp
} }
Text { Text {
@ -111,7 +116,7 @@ Item {
font.pixelSize: 18 font.pixelSize: 18
font.family: fontText.name font.family: fontText.name
color: "#303030" color: "#303030"
text: mText text: model.text
} }
} }
} }
@ -159,28 +164,37 @@ Item {
} }
} }
// busy image / animation // shown while loading app (may take a while)
Item {
Item { // shown while loading app (slow...) visible: loading.visible
anchors.fill: parent anchors.fill: parent
objectName: "hour_glass"
Image { AnimatedImage {
id: loading
objectName: "loading"
anchors.centerIn: parent anchors.centerIn: parent
source: "img/busy.png" source: "img/busy.webp"
visible: playing
playing: true
} }
Text { Text {
width: parent.width id: iniCount
anchors.bottom: parent.bottom anchors.centerIn: parent
anchors.bottomMargin: main.height / 4 color: "white"
horizontalAlignment: Text.AlignHCenter font.family: fontText.name
font.pixelSize: 20 font.pixelSize: 22
text: qsTr("Loading app...\n(make take a while)") }
Timer {
running: loading.playing
interval: 4500
repeat: true
onTriggered: iniCount.text = Number(iniCount.text) + 1
} }
} }
AnimatedImage { // shown during config AnimatedImage {
objectName: "busy" objectName: "busy"
anchors.centerIn: parent anchors.centerIn: parent
width: 42 width: 42

View file

@ -2,7 +2,7 @@
Info Info
---- ----
Please note: this is **WIP!**. It's only a 'proof-of-concept' version. Please note: this is **WIP!** It's only a 'proof-of-concept' version.
Eventually it will (hopefully) catch up with the official app versions. Eventually it will (hopefully) catch up with the official app versions.
@ -22,10 +22,15 @@ some small adaptions and included all generated proto Lisp files in order to be
independent. independent.
Unfortunately cl-protobufs loads very slowly on mobile (and conses hugely Unfortunately cl-protobufs loads very slowly on mobile (and conses hugely
during startup). On an older phone and a cold startup this may take up to 30 during startup). On an older phone and a cold startup this may take more than
seconds. On newer phones and warm startup it should 'only' take around 10 20 seconds. On newer phones and warm startup it should 'only' take around 10
seconds (which seems acceptable). seconds (which seems acceptable).
For the above reason, an animation is shown while loading the app, together
with a counter. For this to work, the app is loaded in the background (that is,
in a separate thread). You'll need to rebuild the lqml library for this to
work.
You will see a json output of all data sent/received. It simply uses the You will see a json output of all data sent/received. It simply uses the
`print-json` convenience function from cl-protobufs. `print-json` convenience function from cl-protobufs.
@ -34,24 +39,29 @@ You will see a json output of all data sent/received. It simply uses the
Tested Tested
------ ------
Currently tested on Linux, macOS, android. The macOS version shows an exception Tested on Linux, macOS, android, iOS. The macOS version shows an ECL exception
during BLE ini, but works nevertheless. during BLE ini, but works nevertheless.
The iOS version doesn't currently work yet (WIP).
How to use cl-meshtastic How to use cl-meshtastic
------------------------ ------------------------
You currently need 2 meshtastic radio devices, both should be running before You currently need exactly 2 meshtastic radio devices, both should be running
you start the app. Both bluetooth and location needs to be enabled (coarse before you start the app. Both bluetooth and location needs to be enabled
location permission is required on android for BLE to work). (coarse location permission is required on android for BLE to work).
Pairing might sometimes require some playing around. If it asks for a PIN and Pairing might sometimes require some playing around. If it asks for a PIN and
your device doesn't have a display (like the RAK starter kit), just use your device doesn't have a display (like the RAK starter kit), just use
`123456`. `123456`.
On iOS pairing is not needed beforehand, because it will ask for pairing and
PIN during BLE ini.
If your android phone says "no BLE devices found" (see logcat output), you
might need to uninstall an eventual app which used the LoRa radio before, and
restart the phone.
On Linux you might need to restart the bluetooth service if you want to pair On Linux you might need to restart the bluetooth service if you want to pair
a different device (after already pairing a first one). a different device (after already pairing a first one).

View file

@ -48,7 +48,6 @@ Qt Creator.
TODO TODO
---- ----
* add (very simple) [meshtastic](https://meshtastic.org) app example
* port to CMake (?) * port to CMake (?)

BIN
screenshots/meshtastic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View file

@ -23,11 +23,11 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
void iniCLFunctions() { void iniCLFunctions() {
cl_object qml(STRING("QML")); cl_object l_qml(STRING("QML"));
if (cl_find_package(qml) == ECL_NIL) { if (cl_find_package(l_qml) == ECL_NIL) {
cl_make_package(1, qml); cl_make_package(1, l_qml);
} }
si_select_package(qml); si_select_package(l_qml);
DEFUN ("clipboard-text", clipboard_text, 0) DEFUN ("clipboard-text", clipboard_text, 0)
DEFUN ("%disable-clipboard-menu", disable_clipboard_menu2, 1) DEFUN ("%disable-clipboard-menu", disable_clipboard_menu2, 1)
DEFUN ("%ensure-permissions", ensure_permissions2, 1) DEFUN ("%ensure-permissions", ensure_permissions2, 1)

View file

@ -7,7 +7,7 @@
#include <QQuickView> #include <QQuickView>
#include <QDebug> #include <QDebug>
const char LQML::version[] = "23.5.2"; // May 2023 const char LQML::version[] = "23.6.1"; // June 2023
extern "C" void ini_LQML(cl_object); extern "C" void ini_LQML(cl_object);

View file

@ -26,7 +26,7 @@
#define ADD_MACOS_BUNDLE_IMPORT_PATH #define ADD_MACOS_BUNDLE_IMPORT_PATH
#endif #endif
#ifdef INI_LISP #if (defined INI_LISP) || (defined BACKGROUND_INI_LISP)
extern "C" void ini_app(cl_object); extern "C" void ini_app(cl_object);
#endif #endif
@ -54,6 +54,13 @@ int catch_all_qexec() {
return ret; return ret;
} }
cl_object do_ini_app() {
#ifdef BACKGROUND_INI_LISP
ecl_init_module(NULL, ini_app);
#endif
return Cnil;
}
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
#if QT_VERSION < 0x060000 #if QT_VERSION < 0x060000
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
@ -93,6 +100,10 @@ int main(int argc, char* argv[]) {
exit(0); exit(0);
} }
cl_object l_qml(STRING("QML"));
si_select_package(l_qml);
DEFUN ("do-ini-app", do_ini_app, 0)
QTranslator translator; QTranslator translator;
if ((QFile::exists("i18n") && translator.load(QLocale(), QString(), QString(), "i18n")) if ((QFile::exists("i18n") && translator.load(QLocale(), QString(), QString(), "i18n"))
|| translator.load(QLocale(), QString(), QString(), ":/i18n")) { || translator.load(QLocale(), QString(), QString(), ":/i18n")) {
@ -165,6 +176,10 @@ int main(int argc, char* argv[]) {
ecl_init_module(NULL, ini_app); ecl_init_module(NULL, ini_app);
#endif #endif
#ifdef BACKGROUND_INI_LISP
LQML::eval("(qml::background-ini)", true); // see 'ini.liso'
#endif
#ifdef NO_QT_RESTART #ifdef NO_QT_RESTART
bool qtRestart = false; bool qtRestart = false;
#else #else

View file

@ -1,11 +1,21 @@
#pragma once #pragma once
#undef SLOT
#include <ecl/ecl.h>
#include <QQmlEngine> #include <QQmlEngine>
#include <QGuiApplication> #include <QGuiApplication>
#include <QInputMethodEvent> #include <QInputMethodEvent>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
#define STRING(s) ecl_make_constant_base_string(s, -1)
#define DEFUN(name, c_name, num_args) \
ecl_def_c_function(ecl_read_from_cstring(name), (cl_objectfn_fixed)c_name, num_args);
cl_object do_ini_app (); // for background ini
class Engine : public QQmlEngine { class Engine : public QQmlEngine {
Q_OBJECT Q_OBJECT
public: public:

View file

@ -338,6 +338,12 @@
#-mobile #-mobile
:mobile-only) :mobile-only)
;;; background ini for big apps, so we can show animation during ini
(defun background-ini ()
;; DO-INI-APP is defined in main.cpp
(mp:process-run-function :app-ini 'do-ini-app))
;;; alias ;;; alias
(defmacro alias (s1 s2) (defmacro alias (s1 s2)

View file

@ -6,6 +6,7 @@
#:*engine* #:*engine*
#:*root-item* #:*root-item*
#:*caller* #:*caller*
#:background-ini
#:clipboard-text #:clipboard-text
#:copy-all-asset-files #:copy-all-asset-files
#:define-qt-wrappers #:define-qt-wrappers