initial commit

This commit is contained in:
Renaud Casenave-Péré 2019-10-27 10:07:41 +01:00
commit 22ae586932
21 changed files with 649 additions and 0 deletions

66
README.org Normal file
View file

@ -0,0 +1,66 @@
This package is here to help bootstrap application development for Sailfish OS
using ecl and eql5.
* Dependencies
Youll 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 youll 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.

12
eql5-sfos.desktop Normal file
View file

@ -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

54
eql5-sfos.pro Normal file
View file

@ -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

BIN
icons/108x108/eql5-sfos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
icons/128x128/eql5-sfos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
icons/172x172/eql5-sfos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
icons/86x86/eql5-sfos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

11
lisp/app.asd Normal file
View file

@ -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")))

52
lisp/app.lisp Normal file
View file

@ -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
)

2
lisp/dependencies.lisp Normal file
View file

@ -0,0 +1,2 @@
(require :ecl-quicklisp)
(ql:quickload :alexandria)

197
lisp/qml.lisp Normal file
View file

@ -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))))))

22
make.lisp Normal file
View file

@ -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)

22
qml/cover/CoverPage.qml Normal file
View file

@ -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"
}
}
}

10
qml/eql5-sfos.qml Normal file
View file

@ -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
}

43
qml/pages/FirstPage.qml Normal file
View file

@ -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
}
}
}
}

30
qml/pages/SecondPage.qml Normal file
View file

@ -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 {}
}
}

18
rpm/eql5-sfos.changes.in Normal file
View file

@ -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 <author's email> version-release
# - Summary of changes
* Sun Apr 13 2014 Jack Tar <jack.tar@example.com> 0.0.1-1
- Scrubbed the deck
- Hoisted the sails

View file

@ -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

43
rpm/eql5-sfos.yaml Normal file
View file

@ -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

41
src/eql5-sfos.cc Normal file
View file

@ -0,0 +1,41 @@
#ifdef QT_QML_DEBUG
#include <QtQuick>
#endif
#include <ecl/ecl.h>
#include <eql5/eql.h>
#include <eql5/eql_fun.h>
#include <sailfishapp.h>
#include <QGuiApplication>
#include <QQuickView>
#include <QTextCodec>
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<QGuiApplication> app (SailfishApp::application (argc, argv));
QScopedPointer<QQuickView> 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)");
}

1
webserver.sh Executable file
View file

@ -0,0 +1 @@
python3 -m http.server 8000