commit 22ae586932b8b5180f735a9d0a1de606ece9b8e1 Author: Renaud Casenave-Péré Date: Sun Oct 27 10:07:41 2019 +0100 initial commit diff --git a/README.org b/README.org new file mode 100644 index 0000000..5fac012 --- /dev/null +++ b/README.org @@ -0,0 +1,66 @@ +This package is here to help bootstrap application development for Sailfish OS +using ecl and eql5. + +* Dependencies +You’ll need ecl and eql5 installed in the build engine that comes with sfos sdk. +These are available through my repository on openrepos.net. You can activate it +and install the packages with the following command: + +#+BEGIN_SRC sh +rpm --import https://sailfish.openrepos.net/openrepos.key +zypper ar -f https://sailfish.openrepos.net/razcampagne/personal-main.repo +zypper in eql5 +#+END_SRC + +* Build and deploy +You can use the build button in qtcreator or do this manually: + +#+BEGIN_SRC sh +qmake eql5-sfos.pro +make +#+END_SRC + +But the way I recommend to do it is by using the command-line tool sfdk that +comes with the sdk. Use the following command for a comprehensive manual: + +#+BEGIN_SRC sh +sfdk --help-building +#+END_SRC + +Once the target and device options have been set, you just have to issue the command: + +#+BEGIN_SRC sh +sfdk deploy +#+END_SRC + +This will compile and build the package and send it to your phone as a rpm. + +* Attach a REPL +Upon initialization of the application, a slynk server is created with the +port 4005. You just have to fire up emacs and connect to it using sly-connect. +Only sly-fancy contribs are supported for now so you’ll have to disable the +others you may have configured for it to work with this application. + +* Reload QML files +You can find a file `webserver.sh` in the repository which will launch a simple +http server. You can then edit the qml files on your host machine and issue the +following command in the application REPL: + +#+BEGIN_SRC common-lisp +(reload-qml "http://host-ip-address:8000") +#+END_SRC + +This will reload all qml files but not the ones packaged on the device, the ones +on your host system, served by the http server. +You can even do that directly in emacs with the following code: + +#+BEGIN_SRC emacs-lisp +(let ((httpd-port 8000)) ; default 8080 port is already used by the sdk build engine + (call-interactively ’httpd-serve-directory)) +#+END_SRC + +* Customize +The code included in this repository is voluntarily barebone. Fill free to use +it to bootstrap your application development and rename or replace files, +packages to better suit your needs. You can even replace slynk with swank if you +prefer. diff --git a/eql5-sfos.desktop b/eql5-sfos.desktop new file mode 100644 index 0000000..4c4f06b --- /dev/null +++ b/eql5-sfos.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +X-Nemo-Application-Type=silica-qt5 +Icon=eql5-sfos +Exec=eql5-sfos +Name=eql5-sfos +# translation example: +# your app name in German locale (de) +# +# Remember to comment out the following line, if you do not want to use +# a different app name in German locale (de). +Name[de]=eql5-sfos diff --git a/eql5-sfos.pro b/eql5-sfos.pro new file mode 100644 index 0000000..d84307e --- /dev/null +++ b/eql5-sfos.pro @@ -0,0 +1,54 @@ +# NOTICE: +# +# Application name defined in TARGET has a corresponding QML filename. +# If name defined in TARGET is changed, the following needs to be done +# to match new name: +# - corresponding QML filename must be changed +# - desktop icon filename must be changed +# - desktop filename must be changed +# - icon definition filename in desktop file must be changed +# - translation filenames have to be changed + +LISP_FILES = make.lisp \ + lisp/dependencies.lisp \ + lisp/qml.lisp \ + lisp/app.lisp \ + lisp/app.asd + +lisp.output = libapp.a +lisp.commands = eql5 -platform minimal $$PWD/make.lisp +lisp.input = LISP_FILES + +QMAKE_EXTRA_COMPILERS += lisp + +# The name of your application +TARGET = eql5-sfos +PRE_TARGETDEPS += libapp.a + +CONFIG += sailfishapp +LIBS += -lecl -leql5 -L. -lapp + +SOURCES += src/eql5-sfos.cc + +DISTFILES += qml/eql5-sfos.qml \ + qml/cover/CoverPage.qml \ + qml/pages/FirstPage.qml \ + qml/pages/SecondPage.qml \ + rpm/eql5-sfos.changes.in \ + rpm/eql5-sfos.changes.run.in \ + rpm/eql5-sfos.spec \ + rpm/eql5-sfos.yaml \ +# translations/*.ts \ + eql5-sfos.desktop + +SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 + +# to disable building translations every time, comment out the +# following CONFIG line +# CONFIG += sailfishapp_i18n + +# German translation is enabled as an example. If you aren't +# planning to localize your app, remember to comment out the +# following TRANSLATIONS line. And also do not forget to +# modify the localized app name in the the .desktop file. +# TRANSLATIONS += translations/eql5-sfos-de.ts diff --git a/icons/108x108/eql5-sfos.png b/icons/108x108/eql5-sfos.png new file mode 100644 index 0000000..ab10628 Binary files /dev/null and b/icons/108x108/eql5-sfos.png differ diff --git a/icons/128x128/eql5-sfos.png b/icons/128x128/eql5-sfos.png new file mode 100644 index 0000000..54375c5 Binary files /dev/null and b/icons/128x128/eql5-sfos.png differ diff --git a/icons/172x172/eql5-sfos.png b/icons/172x172/eql5-sfos.png new file mode 100644 index 0000000..36eee58 Binary files /dev/null and b/icons/172x172/eql5-sfos.png differ diff --git a/icons/86x86/eql5-sfos.png b/icons/86x86/eql5-sfos.png new file mode 100644 index 0000000..ad316d6 Binary files /dev/null and b/icons/86x86/eql5-sfos.png differ diff --git a/lisp/app.asd b/lisp/app.asd new file mode 100644 index 0000000..22fc786 --- /dev/null +++ b/lisp/app.asd @@ -0,0 +1,11 @@ +(defsystem app + :serial t + :defsystem-depends-on (:asdf-package-system) + :class :package-inferred-system + :around-compile (lambda (thunk) + #+app-debug + (proclaim '(optimize (debug 3) (safety 3) (speed 0))) + (funcall thunk)) + :depends-on ("alexandria") + :components ((:file "qml") + (:file "app"))) diff --git a/lisp/app.lisp b/lisp/app.lisp new file mode 100644 index 0000000..7d1e33b --- /dev/null +++ b/lisp/app.lisp @@ -0,0 +1,52 @@ +(defpackage :app + (:use :cl :eql :qml) + (:export #:start-slynk + #:stop-slynk + #:start + #:reload-qml)) +(in-package :app) + +(qrequire :quick) + +(defun sym (symbol package) + (intern (symbol-name symbol) package)) + +#+app-debug +(defun start-slynk () + (unless (find-package :slynk) + (require :ecl-quicklisp) + (funcall (sym 'quickload :ql) :slynk)) + (funcall (sym 'create-server :slynk) + :port 4005 :dont-close t :style :spawn)) + +#+app-debug +(defun stop-slynk () + (when (find-package :slynk) + (funcall (sym 'stop-server :slynk) 4005))) + +(defun start () + #+app-debug + (start-slynk) + (ini-quick-view (main-qml)) + (qconnect (qview) "statusChanged(QQuickView::Status)" + (lambda (status) + (case status + (#.|QQuickView.Ready| + (qml-reloaded)) + (#.|QQuickView.Error| + (qmsg (x:join (mapcar '|toString| (|errors| (qview))) + #.(make-string 2 :initial-element #\Newline))))))) + (qexec)) + +(defun reload-qml (&optional (url "http://localhost:8000/")) + "Reload QML file from an url, directly on the device." + (qrun* + (let ((src (|toString| (|source| (qview))))) + (if (x:starts-with (concatenate 'string "file://" (path-to "")) src) + (|setSource| (qview) (qnew "QUrl(QString)" (x:string-substitute url (concatenate 'string "file://" (path-to "")) src))) + (reload)) + (|toString| (|source| (qview)))))) + +(defun qml-reloaded () + ;; re-ini + ) diff --git a/lisp/dependencies.lisp b/lisp/dependencies.lisp new file mode 100644 index 0000000..35fccc5 --- /dev/null +++ b/lisp/dependencies.lisp @@ -0,0 +1,2 @@ +(require :ecl-quicklisp) +(ql:quickload :alexandria) diff --git a/lisp/qml.lisp b/lisp/qml.lisp new file mode 100644 index 0000000..9ab81a5 --- /dev/null +++ b/lisp/qml.lisp @@ -0,0 +1,197 @@ +;;; +;;; * enables QML to call Lisp functions +;;; * allows to get/set any QML property from Lisp (needs 'objectName' to be set) +;;; * allows to call QML methods from Lisp (needs 'objectName' to be set) +;;; * allows to evaluate JS code from Lisp (needs 'objectName' to be set) +;;; + +(defpackage :app/qml + (:use :cl :eql) + (:nicknames :qml) + (:export + #:*caller* + #:children + #:find-quick-item + #:qview + #:main-qml + #:path-to + #:ini-lib + #:ini-quick-view + #:js + #:qml-call + #:qml-get + #:qml-set + #:qml-set-all + #:paint + #:scale + #:reload + #:root-context + #:root-item)) +(in-package :app/qml) + +(defvar *caller* nil) + +(let (quick-view path-to-main-qml qml-root) + (defun ini-lib (view qml path) + (setf quick-view view + path-to-main-qml qml + qml-root path)) + (defun qview () quick-view) + (defun main-qml () path-to-main-qml) + (defun path-to (res) (concatenate 'string qml-root "/" res))) + +(defun string-to-symbol (name) + (let ((upper (string-upcase name)) + (p (position #\: name))) + (if p + (find-symbol (subseq upper (1+ (position #\: name :from-end t))) + (subseq upper 0 p)) + (find-symbol upper)))) + +;;; function calls from QML + +(defun print-js-readably (object) + "Prints (nested) lists, vectors, T, NIL, floats in JS notation, which will be passed to JS 'eval()'." + (if (and (not (stringp object)) + (vectorp object)) + (print-js-readably (coerce object 'list)) + (typecase object + (cons + (write-char #\[) + (do ((list object (rest list))) + ((null list) (write-char #\])) + (print-js-readably (first list)) + (when (rest list) + (write-char #\,)))) + (float + ;; cut off Lisp specific notations + (princ (string-right-trim "dl0" (princ-to-string object)))) + (t + (cond ((eql 't object) + (princ "true")) + ((eql 'nil object) + (princ "false")) + (t + (prin1 object))))))) + +(defun print-to-js-string (object) + (with-output-to-string (*standard-output*) + (princ "#<>") ; mark for passing to JS "eval()" + (print-js-readably object))) + +(defun qml-apply (caller function arguments) + "Every 'Lisp.call()' or 'Lisp.apply()' function call in QML will call this function. The variable *CALLER* will be bound to the calling QQuickItem, if passed with 'this' as first argument to 'Lisp.call()' / 'Lisp.apply()'." + (let* ((*caller* (if (qnull caller) *caller* (qt-object-? caller))) + (object (apply (string-to-symbol function) + arguments))) + (if (stringp object) + object + (print-to-js-string object)))) + +;;; utils + +(defun root-item () + (when (qview) + (qrun* (if (= (qt-object-id (qview)) (qid "QQmlApplicationEngine")) + (let ((object (first (|rootObjects| (qview))))) + (setf (qt-object-id object) (qid "QObject")) ; unknown to EQL, so resort to QObject + object) + (qt-object-? (|rootObject| (qview))))))) + +(defun root-context () + (when (qview) + (|rootContext| (qview)))) + +(defun find-quick-item (object-name) + "Finds the first QQuickItem matching OBJECT-NAME." + (let ((root (root-item))) + (unless (qnull root) + (if (string= (|objectName| root) object-name) + (root-item) + (qt-object-? (qfind-child root object-name)))))) + +(defun quick-item (item/name) + (cond ((stringp item/name) + (find-quick-item item/name)) + ((qt-object-p item/name) + item/name) + ((not item/name) + (root-item)))) + +(defun children (item/name) + "Like QML function 'children'." + (qrun* (mapcar 'qt-object-? (|childItems| (quick-item item/name))))) + +(defun scale () + "Returns the scale factor used on high dpi scaled devices (e.g. phones)." + (|effectiveDevicePixelRatio| (qview))) + +(defun reload () + "Force reloading of QML file after changes made to it." + (|clearComponentCache| (|engine| (qview))) + (|setSource| (qview) (|source| (qview)))) + +;;; call QML methods + +(defun qml-call (item/name method-name &rest arguments) + ;; QFUN+ comes in handy here + (qrun* (apply 'qfun+ (quick-item item/name) method-name arguments))) + +;;; get/set QQmlProperty + +(defun qml-get (item/name property-name) + "Gets QQmlProperty of either ITEM or first object matching NAME." + (qrun* (qlet ((property "QQmlProperty(QObject*,QString)" + (quick-item item/name) + property-name)) + (if (|isValid| property) + (qlet ((variant (|read| property))) + (values (qvariant-value variant) + t)) + (eql::%error-msg "QML-GET" (list item/name property-name)))))) + +(defun qml-set (item/name property-name value &optional update) + "Sets QQmlProperty of either ITEM, or first object matching NAME. Returns T on success. If UPDATE is not NIL and ITEM is a QQuickPaintedItem, |update| will be called on it." + (qrun* (let ((item (quick-item item/name))) + (qlet ((property "QQmlProperty(QObject*,QString)" item property-name)) + (if (|isValid| property) + (let ((type-name (|propertyTypeName| property))) + (qlet ((variant (qvariant-from-value value (if (find #\: type-name) "int" type-name)))) + (prog1 + (|write| property variant) + (when (and update (= (qt-object-id item) (qid "QQuickPaintedItem"))) + (|update| item))))) + (eql::%error-msg "QML-SET" (list item/name property-name value))))))) + +(defun qml-set-all (name property-name value &optional update) + "Sets QQmlProperty of all objects matching NAME." + (assert (stringp name)) + (qrun* (dolist (item (qfind-children (root-item) name)) + (qml-set item property-name value update)))) + +;;; JS + +(defun js (item/name js-format-string &rest arguments) + "Evaluates a JS string, with 'this' bound to either ITEM, or first object matching NAME. Arguments are passed through FORMAT." + (qrun* (qlet ((qml-exp "QQmlExpression(QQmlContext*,QObject*,QString)" + (root-context) + (quick-item item/name) + (apply 'format nil js-format-string arguments)) + (variant (|evaluate| qml-exp))) + (qvariant-value variant)))) + +;;; ini + +(defun ini-quick-view (file) + (when (qview) + (qconnect (|engine| (qview)) "quit()" (qapp) "quit()") + (qnew "QQmlFileSelector(QQmlEngine*,QObject*)" (|engine| (qview)) (qview)) + (|setSource| (qview) (|fromLocalFile.QUrl| file)) + (when (= |QQuickView.Error| (|status| (qview))) + ;; display eventual QML errors + (qmsg (x:join (mapcar '|toString| (|errors| (qview))) + #.(make-string 2 :initial-element #\Newline)))) + (let ((platform (|platformName.QGuiApplication|))) + (if (string= platform "wayland") + (|showFullScreen| (qview)) + (|show| (qview)))))) diff --git a/make.lisp b/make.lisp new file mode 100644 index 0000000..bf8c1ac --- /dev/null +++ b/make.lisp @@ -0,0 +1,22 @@ +#-eql5 +(error "Please use the EQL5 executable") + +(require :cmp) + +(push :app-debug *features*) + +(load "lisp/dependencies") + +(push "lisp/" asdf:*central-registry*) + +(asdf:make-build "app" + :monolithic t + :type :static-library + :move-here "./") + +(let ((lib-name "libapp.a")) + (when (probe-file lib-name) + (delete-file lib-name)) + (rename-file (x:cc "app--all-systems" ".a") lib-name)) + +(eql:qquit) diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml new file mode 100644 index 0000000..fc562d5 --- /dev/null +++ b/qml/cover/CoverPage.qml @@ -0,0 +1,22 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +CoverBackground { + Label { + id: label + anchors.centerIn: parent + text: qsTr("My Cover") + } + + CoverActionList { + id: coverAction + + CoverAction { + iconSource: "image://theme/icon-cover-next" + } + + CoverAction { + iconSource: "image://theme/icon-cover-pause" + } + } +} diff --git a/qml/eql5-sfos.qml b/qml/eql5-sfos.qml new file mode 100644 index 0000000..2942450 --- /dev/null +++ b/qml/eql5-sfos.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import "pages/" as Pages + +ApplicationWindow +{ + initialPage: Component { Pages.FirstPage { } } + cover: Qt.resolvedUrl("cover/CoverPage.qml") + allowedOrientations: defaultAllowedOrientations +} diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml new file mode 100644 index 0000000..447accf --- /dev/null +++ b/qml/pages/FirstPage.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + // The effective value will be restricted by ApplicationWindow.allowedOrientations + allowedOrientations: Orientation.All + + // To enable PullDownMenu, place our content in a SilicaFlickable + SilicaFlickable { + anchors.fill: parent + + // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView + PullDownMenu { + MenuItem { + text: qsTr("Show Page 2") + onClicked: pageStack.push(Qt.resolvedUrl("SecondPage.qml")) + } + } + + // Tell SilicaFlickable the height of its content. + contentHeight: column.height + + // Place our content in a Column. The PageHeader is always placed at the top + // of the page, followed by our content. + Column { + id: column + + width: page.width + spacing: Theme.paddingLarge + PageHeader { + title: qsTr("UI Template") + } + Label { + x: Theme.horizontalPageMargin + text: qsTr("Hello Sailors") + color: Theme.secondaryHighlightColor + font.pixelSize: Theme.fontSizeExtraLarge + } + } + } +} diff --git a/qml/pages/SecondPage.qml b/qml/pages/SecondPage.qml new file mode 100644 index 0000000..6dbadf4 --- /dev/null +++ b/qml/pages/SecondPage.qml @@ -0,0 +1,30 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + // The effective value will be restricted by ApplicationWindow.allowedOrientations + allowedOrientations: Orientation.All + + SilicaListView { + id: listView + model: 20 + anchors.fill: parent + header: PageHeader { + title: qsTr("Nested Page") + } + delegate: BackgroundItem { + id: delegate + + Label { + x: Theme.horizontalPageMargin + text: qsTr("Item") + " " + index + anchors.verticalCenter: parent.verticalCenter + color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor + } + onClicked: console.log("Clicked " + index) + } + VerticalScrollDecorator {} + } +} diff --git a/rpm/eql5-sfos.changes.in b/rpm/eql5-sfos.changes.in new file mode 100644 index 0000000..8a8d9c7 --- /dev/null +++ b/rpm/eql5-sfos.changes.in @@ -0,0 +1,18 @@ +# Rename this file as eql5-sfos.changes to include changelog +# entries in your RPM file. +# +# Add new changelog entries following the format below. +# Add newest entries to the top of the list. +# Separate entries from eachother with a blank line. +# +# Alternatively, if your changelog is automatically generated (e.g. with +# the git-change-log command provided with Sailfish OS SDK), create a +# eql5-sfos.changes.run script to let mb2 run the required commands for you. + +# * date Author's Name version-release +# - Summary of changes + +* Sun Apr 13 2014 Jack Tar 0.0.1-1 +- Scrubbed the deck +- Hoisted the sails + diff --git a/rpm/eql5-sfos.changes.run.in b/rpm/eql5-sfos.changes.run.in new file mode 100644 index 0000000..abb5adc --- /dev/null +++ b/rpm/eql5-sfos.changes.run.in @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Rename this file as eql5-sfos.changes.run to let mb2 automatically +# generate changelog from well formatted Git commit messages and tag +# annotations. + +git-change-log + +# Here are some basic examples how to change from the default behavior. Run +# git-change-log --help inside the Sailfish OS SDK chroot or build engine to +# learn all the options git-change-log accepts. + +# Use a subset of tags +#git-change-log --tags refs/tags/my-prefix/* + +# Group entries by minor revision, suppress headlines for patch-level revisions +#git-change-log --dense '/[0-9]+.[0-9+$' + +# Trim very old changes +#git-change-log --since 2014-04-01 +#echo '[ Some changelog entries trimmed for brevity ]' + +# Use the subjects (first lines) of tag annotations when no entry would be +# included for a revision otherwise +#git-change-log --auto-add-annotations diff --git a/rpm/eql5-sfos.yaml b/rpm/eql5-sfos.yaml new file mode 100644 index 0000000..d5e9e3e --- /dev/null +++ b/rpm/eql5-sfos.yaml @@ -0,0 +1,43 @@ +Name: eql5-sfos +Summary: Sailfish OS Application Template for EQL5 +Version: 0.1 +Release: 1 +# The contents of the Group field should be one of the groups listed here: +# https://github.com/mer-tools/spectacle/blob/master/data/GROUPS +Group: Qt/Qt +URL: http://example.org/ +License: LICENSE +# This must be generated before uploading a package to a remote build service. +# Usually this line does not need to be modified. +Sources: +- '%{name}-%{version}.tar.bz2' +Description: | + Short description of my Sailfish OS Application +Configure: none +Builder: qmake5 + +# This section specifies build dependencies that are resolved using pkgconfig. +# This is the preferred way of specifying build dependencies for your package. +PkgConfigBR: + - sailfishapp >= 1.0.2 + - Qt5Core + - Qt5Qml + - Qt5Quick + +# Build dependencies without a pkgconfig setup can be listed here +# PkgBR: +# - package-needed-to-build + +# Runtime dependencies which are not automatically detected +Requires: + - sailfishsilica-qt5 >= 0.10.9 + +# All installed files +Files: + - '%{_bindir}' + - '%{_datadir}/%{name}' + - '%{_datadir}/applications/%{name}.desktop' + - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' + +# For more information about yaml and what's supported in Sailfish OS +# build system, please see https://wiki.merproject.org/wiki/Spectacle diff --git a/src/eql5-sfos.cc b/src/eql5-sfos.cc new file mode 100644 index 0000000..8b5d785 --- /dev/null +++ b/src/eql5-sfos.cc @@ -0,0 +1,41 @@ +#ifdef QT_QML_DEBUG +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +extern "C" void init_lib_APP__ALL_SYSTEMS (cl_object); + +int main(int argc, char *argv[]) +{ + // SailfishApp::main() will display "qml/eql5-sfos.qml", if you need more + // control over initialization, you can use: + // + // - SailfishApp::application(int, char *[]) to get the QGuiApplication * + // - SailfishApp::createView() to get a new QQuickView * instance + // - SailfishApp::pathTo(QString) to get a QUrl to a resource file + // - SailfishApp::pathToMainQml() to get a QUrl to the main QML file + // + // To display the view, call "show()" (will show fullscreen on device). + + QScopedPointer app (SailfishApp::application (argc, argv)); + QScopedPointer view (SailfishApp::createView ()); + QUrl mainQml = SailfishApp::pathToMainQml (); + QUrl resRoot = SailfishApp::pathTo (""); + + QTextCodec* utf8 = QTextCodec::codecForName("UTF-8"); + QTextCodec::setCodecForLocale(utf8); + + EQL eql; + eql.exec (init_lib_APP__ALL_SYSTEMS); + eql_fun ("app/qml:ini-lib", Q_ARG (QQuickView*, view.data ()), + Q_ARG (const char*, mainQml.toLocalFile ().toLatin1 ().constData ()), + Q_ARG (const char*, resRoot.toLocalFile ().toLatin1 ().constData ())); + EQL::eval ("(app:start)"); +} diff --git a/webserver.sh b/webserver.sh new file mode 100755 index 0000000..cb71a61 --- /dev/null +++ b/webserver.sh @@ -0,0 +1 @@ +python3 -m http.server 8000