diff --git a/examples/qsqlite/app.asd b/examples/qsqlite/app.asd new file mode 100644 index 0000000..880a383 --- /dev/null +++ b/examples/qsqlite/app.asd @@ -0,0 +1,9 @@ +(defsystem :app + :serial t + :depends-on () + :components ((:file "lisp/package") + (:file "lisp/qt") + (:file "lisp/ui-vars") + (:file "lisp/db") + (:file "lisp/main"))) + diff --git a/examples/qsqlite/app.pro b/examples/qsqlite/app.pro new file mode 100644 index 0000000..e6ef2eb --- /dev/null +++ b/examples/qsqlite/app.pro @@ -0,0 +1,127 @@ +LISP_FILES = $$files(lisp/*) app.asd make.lisp + +exists(/etc/sailfish-release) { + CONFIG += sfos +} + +android { + 32bit { + ECL = $$(ECL_ANDROID_32) + } else { + ECL = $$(ECL_ANDROID) + } + lisp.commands = $$ECL/../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 +} else:win32 { + lisp.commands = ecl.exe -shell $$PWD/make.lisp +} + +lisp.input = LISP_FILES + +win32: lisp.output = tmp/app.lib +!win32: lisp.output = tmp/libapp.a + +QMAKE_EXTRA_COMPILERS += lisp + +win32: PRE_TARGETDEPS = tmp/app.lib +!win32: PRE_TARGETDEPS = tmp/libapp.a + +QT += quick qml quickcontrols2 sql +TEMPLATE = app +CONFIG += c++17 no_keywords release +DEFINES = DESKTOP_APP INI_LISP QT_EXTENSION +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 +win32: LIBS += -L../../../platforms/windows/lib + +win32 { + include(../../src/windows.pri) +} + +android { + DEFINES -= DESKTOP_APP + INCLUDEPATH = $$ECL/include + LIBS = -L$$ECL/lib -lecl + LIBS += -L../../../platforms/android/lib + + equals(QT_MAJOR_VERSION, 6) { + QT += core-private + } + lessThan(QT_MAJOR_VERSION, 6) { + QT += androidextras + } + + ANDROID_EXTRA_LIBS += $$ECL/lib/libecl.so + ANDROID_PACKAGE_SOURCE_DIR = ../platforms/android + + 32bit { + ANDROID_ABIS = "armeabi-v7a" + } else { + ANDROID_ABIS = "arm64-v8a" + } +} + +ios { + DEFINES -= DESKTOP_APP + INCLUDEPATH = $$(ECL_IOS)/include + LIBS = -L$$(ECL_IOS)/lib -lecl + LIBS += -leclatomic -leclffi -leclgc -leclgmp + LIBS += -L../../../platforms/ios/lib +} + +32bit { + android { + equals(QT_MAJOR_VERSION, 6) { + LIBS += -llqml32_armeabi-v7a + } + lessThan(QT_MAJOR_VERSION, 6) { + LIBS += -llqml32 + } + } + !android { + LIBS += -llqml32 + } + LIBS += -llisp32 +} else { + android { + equals(QT_MAJOR_VERSION, 6) { + LIBS += -llqml_arm64-v8a + } + lessThan(QT_MAJOR_VERSION, 6) { + LIBS += -llqml + } + } + !android { + LIBS += -llqml + } + LIBS += -llisp +} + +LIBS += -Ltmp -lapp + +HEADERS += \ + ../../src/cpp/main.h \ + cpp/qt.h + +SOURCES += \ + ../../src/cpp/main.cpp \ + cpp/qt.cpp + +RESOURCES += $$files(qml/*) +RESOURCES += $$files(i18n/*.qm) + +lupdate_only { + SOURCES += i18n/tr.h +} diff --git a/examples/qsqlite/blob/logo-064.png b/examples/qsqlite/blob/logo-064.png new file mode 100755 index 0000000..03a9009 Binary files /dev/null and b/examples/qsqlite/blob/logo-064.png differ diff --git a/examples/qsqlite/blob/logo-128.png b/examples/qsqlite/blob/logo-128.png new file mode 100755 index 0000000..7ceb761 Binary files /dev/null and b/examples/qsqlite/blob/logo-128.png differ diff --git a/examples/qsqlite/blob/logo-256.png b/examples/qsqlite/blob/logo-256.png new file mode 100755 index 0000000..a6a536a Binary files /dev/null and b/examples/qsqlite/blob/logo-256.png differ diff --git a/examples/qsqlite/cpp/qt.cpp b/examples/qsqlite/cpp/qt.cpp new file mode 100644 index 0000000..2bfdcfa --- /dev/null +++ b/examples/qsqlite/cpp/qt.cpp @@ -0,0 +1,94 @@ +#include "qt.h" +#include +#include +#include +#include + +QObject* ini() { + static QObject* qt = nullptr; + if (qt == nullptr) { + qt = new QT; + } + return qt; +} + +QT::QT() : QObject() { +} + +QVariant QT::dataPath(const QVariant& prefix) { + // for desktop + QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + path.truncate(path.lastIndexOf(QChar('/'))); + path.append(QStringLiteral("/lqml-qsqlite/") + prefix.toString()); + return path; +} + +// SQL + +QVariant QT::iniDb(const QVariant& vName, const QVariant& vQuickView) { + // add database image provider, in order to load images in QML directly from a SQL database + auto quickView = vQuickView.value(); + quickView->engine()->addImageProvider(QLatin1String("db"), new DatabaseImageProvider(this)); + // ini + db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName(vName.toString()); + return vName; +} + +QVariant QT::sqlQuery(const QVariant& vQuery, const QVariant& vValues, const QVariant& vCols) { + QVariantList results; + QSqlQuery sqlQuery(db); + if (db.open()) { + QString query = vQuery.toString(); + sqlQuery.prepare(vQuery.toString()); + const QVariantList values = vValues.value(); + for (auto value : values) { + sqlQuery.addBindValue(value); + } + if (sqlQuery.exec()) { + auto cols = vCols.toInt(); + while (sqlQuery.next()) { + if (cols > 1) { + QVariantList list; + for (auto r = 0; r < cols; r++) { + list << sqlQuery.value(r); + } + results << QVariant(list); + } else { + results << sqlQuery.value(0); + } + } + if (!cols && query.startsWith("insert", Qt::CaseInsensitive)) { + results << sqlQuery.lastInsertId(); + } + db.close(); + return results; + } + db.close(); + } + QString text; + if (sqlQuery.lastError().isValid()) { + text = sqlQuery.lastError().text(); + } else { + text = db.lastError().text(); + } + qDebug() << "SQL error:" << text; + return QVariant(); +} + +// database image provider + +QPixmap DatabaseImageProvider::requestPixmap(const QString& name, QSize* size, const QSize& requestedSize) { + auto result = qt->sqlQuery( + "select data from images where name = ?", + QVariantList() << name, + 1); // number of returned columns + QPixmap pixmap; + pixmap.loadFromData(result.value().first().toByteArray()); + *size = pixmap.size(); + if (requestedSize.isValid() && (pixmap.size() != requestedSize)) { + pixmap = pixmap.scaled(requestedSize); + } + return pixmap; +} + diff --git a/examples/qsqlite/cpp/qt.h b/examples/qsqlite/cpp/qt.h new file mode 100644 index 0000000..b96916f --- /dev/null +++ b/examples/qsqlite/cpp/qt.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#ifdef Q_CC_MSVC +#define LIB_EXPORT __declspec(dllexport) +#else +#define LIB_EXPORT +#endif + +extern "C" { LIB_EXPORT QObject* ini(); } + +class QT : public QObject { + Q_OBJECT + +public: + Q_INVOKABLE QVariant dataPath(const QVariant&); + Q_INVOKABLE QVariant iniDb(const QVariant&, const QVariant&); + Q_INVOKABLE QVariant sqlQuery(const QVariant&, const QVariant&, const QVariant&); + + QT(); + + QSqlDatabase db; +}; + +class DatabaseImageProvider : public QQuickImageProvider { + +public: + DatabaseImageProvider(QT* _qt) : QQuickImageProvider(QQuickImageProvider::Pixmap), qt(_qt) {} + + QPixmap requestPixmap(const QString&, QSize*, const QSize&); + + QT* qt; +}; diff --git a/examples/qsqlite/cpp/qt.pro b/examples/qsqlite/cpp/qt.pro new file mode 100644 index 0000000..ea1d7c1 --- /dev/null +++ b/examples/qsqlite/cpp/qt.pro @@ -0,0 +1,27 @@ +QT += sql quick +TEMPLATE = lib +CONFIG += c++17 plugin release no_keywords +DEFINES += PLUGIN +INCLUDEPATH = /usr/local/include ../../../src/cpp +LIBS = -L/usr/local/lib -lecl +DESTDIR = ./ +TARGET = qt +OBJECTS_DIR = ./tmp/ +MOC_DIR = ./tmp/ + +HEADERS += qt.h +SOURCES += qt.cpp + +linux { + LIBS += -L../../../platforms/linux/lib +} + +macx { + LIBS += -L../../../platforms/macos/lib +} + +win32 { + include(../../../src/windows.pri) + + LIBS += -L../../../platforms/windows/lib +} diff --git a/examples/qsqlite/lisp/db.lisp b/examples/qsqlite/lisp/db.lisp new file mode 100644 index 0000000..e393afb --- /dev/null +++ b/examples/qsqlite/lisp/db.lisp @@ -0,0 +1,34 @@ +(in-package :db) + +(defvar *file* nil) + +(defun query (query &rest values) + (let ((rows (and (x:starts-with "select" query) + (1+ (count #\, (subseq query 0 (search "from" query))))))) + (qrun* (qt:sql-query qt:*cpp* query values rows)))) + +(defun ini () + (setf *file* (app:in-data-path "db")) + (ensure-directories-exist *file*) + (qt:ini-db qt:*cpp* (namestring *file*) *quick-view*) + (query "create table if not exists images (id integer primary key autoincrement, name text, data blob)")) + +(defun size () + (first (query "select count(*) from images"))) + +(defun save-image (name data) + "Inserts image NAME, DATA (vector of octets) and returns the new image ID." + (first (query "insert into images (name, data) values (?, ?)" + name data))) + +(defun load-images () + (query "select name, data from images order by id")) + +(defun delete-image (id) + (query "delete from images where id = ?" + id) + (values)) + +(defun delete-all-images () + (query "delete from images")) + diff --git a/examples/qsqlite/lisp/main.lisp b/examples/qsqlite/lisp/main.lisp new file mode 100644 index 0000000..75ce565 --- /dev/null +++ b/examples/qsqlite/lisp/main.lisp @@ -0,0 +1,35 @@ +(in-package :app) + +(defun ini () + (qt:ini) + (db:ini) + (populate-db) + ;; loads image directly from database, see 'cpp/qt.cpp::DatabaseImageProvider' + (q> |source| ui:*logo* "image://db/logo-128")) + +(defun in-data-path (&optional (file "") (prefix "data/")) + #+mobile + (merge-pathnames (x:cc prefix file)) + #-mobile + (x:cc (qrun* (qt:data-path qt:*cpp* prefix)) file)) + +(defun file-bytes (file) + (with-open-file (s file :element-type '(unsigned-byte 8)) + (let ((arr (make-array (file-length s) :element-type '(unsigned-byte 8)))) + (read-sequence arr s) + arr))) + +;; put images in database + +(defun populate-db () + (let ((files (sort (directory (merge-pathnames "blob/*.png")) + 'string< :key 'pathname-name))) + (when (/= (length files) (db:size)) + (if (probe-file (merge-pathnames "blob/")) + (progn + (db:delete-all-images) + (dolist (file files) + (db:save-image (pathname-name file) (file-bytes file))))) + (x:d "No 'blob/' directory found.")))) + +(qlater 'ini) diff --git a/examples/qsqlite/lisp/package.lisp b/examples/qsqlite/lisp/package.lisp new file mode 100644 index 0000000..a5f8a50 --- /dev/null +++ b/examples/qsqlite/lisp/package.lisp @@ -0,0 +1,15 @@ +(defpackage :app + (:use :cl :qml) + (:export + #:in-data-path)) + +(defpackage :db + (:use :cl :qml) + (:export + #:delete-image + #:delete-all-images + #:ini + #:load-images + #:save-image + #:size)) + diff --git a/examples/qsqlite/lisp/qt.lisp b/examples/qsqlite/lisp/qt.lisp new file mode 100644 index 0000000..2da0be3 --- /dev/null +++ b/examples/qsqlite/lisp/qt.lisp @@ -0,0 +1,19 @@ +(defpackage :qt + (:use :cl :qml) + (:export + #:*cpp* + #:data-path + #:ini + #:ini-db + #:sql-query)) + +(in-package :qt) + +(defvar *cpp* nil) + +(defun ini () + (setf *cpp* + #+qt-plugin (qload-c++ "cpp/qt") + #-qt-plugin (qfind-child nil "QT")) + (let ((*package* (find-package :qt))) + (define-qt-wrappers *cpp*))) diff --git a/examples/qsqlite/lisp/ui-vars.lisp b/examples/qsqlite/lisp/ui-vars.lisp new file mode 100644 index 0000000..5ec1f72 --- /dev/null +++ b/examples/qsqlite/lisp/ui-vars.lisp @@ -0,0 +1,9 @@ +(defpackage ui + (:use :cl :qml) + (:export + #:*logo*)) + +(in-package :ui) + +(defparameter *logo* "logo") + diff --git a/examples/qsqlite/make.lisp b/examples/qsqlite/make.lisp new file mode 100644 index 0000000..c6745ac --- /dev/null +++ b/examples/qsqlite/make.lisp @@ -0,0 +1,88 @@ +;; 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))) + +#+(or android ios) +(pushnew :mobile *features*) + +(when (probe-file "/etc/sailfish-release") + (pushnew :sfos *features*)) + +(defun cc (&rest args) + (apply 'concatenate 'string args)) + +#+mobile +(defvar *assets* #+android "../platforms/android/assets/lib/" + #+ios "../platforms/ios/assets/Library/") + +#+mobile +(defun shell (command) + (ext:run-program "sh" (list "-c" command))) + +#+mobile +(let ((blob #+android *assets* + #+ios (cc *assets* "../Documents/"))) + (ensure-directories-exist blob) + (shell (cc "cp -r ../blob " blob))) + +(require :asdf) +(require :cmp) + +(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))))) + +(dolist (file (list "package" "x" "ecl-ext" "ini" "qml")) ; load LQML symbols + (load (merge-pathnames file "src/lisp/"))) + +(defun cc (&rest args) + (apply 'concatenate 'string args)) + +(progn + (defvar cl-user::*tr-path* (truename (cc *current* "i18n/"))) + (load "src/lisp/tr")) + +#-mobile +(asdf:make-build "app" + :monolithic t + :type :static-library + :move-here (cc *current* "build/tmp/") + :init-name "ini_app") + +#+mobile +(progn + (pushnew :interpreter *features*) + (defvar *asdf-system* "app") + (defvar *ql-libs* (cc *current* "ql-libs.lisp")) + (defvar *init-name* "ini_app") + (defvar *library-path* (format nil "~Abuild-~A/tmp/" + *current* + #+android "android" + #+ios "ios")) + (defvar *require* (list :ecl-curl)) + (load "platforms/shared/make")) + +;; rename lib +(let* ((from #-mobile (cc *current* (format nil "build/tmp/app--all-systems.~A" + #+msvc "lib" + #-msvc "a")) + #+mobile (cc *library-path* "app--all-systems.a")) + (to #-msvc "libapp.a" + #+msvc "app.lib") + (to* #-mobile (cc *current* "build/tmp/" to) + #+mobile (cc *library-path* to))) + (when (probe-file to*) + (delete-file to*)) + (rename-file from to)) diff --git a/examples/qsqlite/qml/main.qml b/examples/qsqlite/qml/main.qml new file mode 100644 index 0000000..3d11cb9 --- /dev/null +++ b/examples/qsqlite/qml/main.qml @@ -0,0 +1,12 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + width: 300 + height: 500 + + Image { + objectName: "logo" + anchors.centerIn: parent + } +} diff --git a/examples/qsqlite/readme.md b/examples/qsqlite/readme.md new file mode 100644 index 0000000..81dfa3e --- /dev/null +++ b/examples/qsqlite/readme.md @@ -0,0 +1,43 @@ + +Info +---- + +This is a simple example of: + +* using the **qsqlite** (Qt specific SQLite DB) library directly from Lisp +* providing a custom image provider, so we can directly load images in QML from + an SQL database + +The qsqlite library that comes with Qt has the advantage of being pulled in +automatically as a dependency, and behaving exactly the same, no matter what OS +is used. This is especially convenient on mobile. + +If you run the example with: +``` +$ lqml run.lisp -slime +``` +you can then try to change the image source from the REPL: +``` +(in-package :app) + +(q> |source| ui:*logo* "image://db/logo-256") +``` +This will load the image directly from the database. + + +Run +--- +``` +lqml run.lisp +``` +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 reload all QML files after +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 +short for `(qquit)`. + diff --git a/examples/qsqlite/run.lisp b/examples/qsqlite/run.lisp new file mode 100644 index 0000000..d0e113d --- /dev/null +++ b/examples/qsqlite/run.lisp @@ -0,0 +1,29 @@ +(in-package :qml-user) + +(pushnew :qt-plugin *features*) + +(require :asdf) + +(push (merge-pathnames "./") + asdf:*central-registry*) + +(asdf:operate 'asdf:load-source-op :app) + +(qset *quick-view* + |x| 75 + |y| 75) + +(defun option (name) + (find name (ext:command-args) :test 'search)) + +;;; trivial auto reload of all QML files after saving any change + +(when (option "-auto") + (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/' + +(when (option "-slime") + (load "~/slime/lqml-start-swank")) ; for 'slime-connect' from Emacs +