add Qt6 version of some examples (see below); revisions

'9999', 'advanced-qml-auto-reload', 'planets', 'sokoban'
This commit is contained in:
pls.153 2024-10-22 13:27:56 +02:00
parent ca79dec909
commit dc29ac9084
87 changed files with 963 additions and 74 deletions

2
examples/Qt6/9999/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Window
Rectangle {
id: main
width: 200
height: 300 + input.height
color: "lavender"
TextField {
id: input
objectName: "input"
width: parent.width
horizontalAlignment: Qt.AlignHCenter
text: "0000"
inputMask: "9999"
inputMethodHints: Qt.ImhDigitsOnly
focus: true
onTextChanged: Lisp.call("app:draw-number", Number(text))
}
Canvas {
id: canvas
objectName: "canvas"
y: input.height
width: parent.width
height: {
var h = Qt.inputMethod.keyboardRectangle.y
var f = (Qt.platform.os === "android") ? Screen.devicePixelRatio : 1
h = (h === 0) ? main.height : h / f
return (h - input.height)
}
property var ctx
// functions to be called from Lisp
function begin(color, width) {
ctx.beginPath()
ctx.strokeStyle = color
ctx.lineWidth = width
ctx.lineCap = "round"
}
function end() {
ctx.stroke()
}
function drawLine(x1, y1, x2, y2) {
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
}
onPaint: {
ctx = getContext("2d")
ctx.reset()
ctx.translate(canvas.width / 2, canvas.height / 2)
var s = height / 340
ctx.scale(s, s)
Lisp.call("app:paint")
ctx.stroke()
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,15 @@
import QtQuick
Loader {
objectName: "ext/Page1.qml"
source: objectName
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
Loader {
objectName: "ext/Page2.qml"
source: objectName
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
Loader {
objectName: "ext/Page3.qml"
source: objectName
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
Loader {
objectName: "ext/Repl.qml"
source: objectName
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
Loader {
objectName: "~A"
source: objectName
Component.onCompleted: if (width === 0) { anchors.fill = parent }
function reload() {
var src = source
source = ""
Engine.clearCache()
source = src
}
}

View file

@ -0,0 +1,15 @@
import QtQuick
Item {
Rectangle {
anchors.fill: parent
color: Qt.lighter("red", 1.5)
border.width: 10
border.color: "red"
Text {
anchors.centerIn: parent
text: "<h2>page 1</h2>"
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
Item {
Rectangle {
anchors.fill: parent
radius: 100
color: Qt.lighter("green", 3.0)
border.width: 10
border.color: "green"
Text {
anchors.centerIn: parent
text: "<h2>page 2</h2>"
}
}
}

View file

@ -0,0 +1,18 @@
import QtQuick
Item {
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height)
height: width
radius: width
color: Qt.lighter("blue", 1.7)
border.width: 10
border.color: "blue"
Text {
anchors.centerIn: parent
text: "<h2>page 3</h2>"
}
}
}

View file

@ -0,0 +1,160 @@
import QtQuick
import QtQuick.Controls
Item {
id: repl
z: 1
anchors.fill: parent
Row {
anchors.right: parent.right
z: 1
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.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.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 }}
]
}

View file

@ -0,0 +1,49 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import ".ext/" as Ext // for single file auto reload (development)
//import "ext/" as Ext // release version
Rectangle {
id: main
width: 300
height: 500
objectName: "main"
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" }
}

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

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

View file

@ -0,0 +1,87 @@
<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.cl.meshtastic"
android:installLocation="auto"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.USB_PERMISSION"/>
<uses-permission android:name="android.permission.USB_HOST"/>
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"/>
<application
android:name=".MeServiceApplication"
android:label="Mesh SMS"
android:hardwareAccelerated="true"
android:extractNativeLibs="true"
android:icon="@drawable/icon">
<activity
android:name=".MeActivity"
android:label="Mesh SMS"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:screenOrientation="unspecified"
android:launchMode="singleTop"
android:theme="@style/splashScreenTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter"/>
<meta-data
android:name="android.app.lib_name"
android:value="app"/>
<meta-data
android:name="android.app.arguments"
android:value=""/>
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal"/>
<meta-data
android:name="android.app.load_local_jars"
android:value="jar/QtAndroid.jar:jar/QtAndroidExtras.jar:jar/QtAndroidBluetooth.jar:jar/QtAndroidBearer.jar"/>
<meta-data
android:name="android.app.static_init_classes"
android:value="org.qtproject.qt.android.bluetooth.QtBluetoothBroadcastReceiver"/>
<meta-data
android:name="android.app.background_running"
android:value="true"/>
<meta-data
android:name="android.app.splash_screen_drawable"
android:resource="@drawable/splashscreen"/>
</activity>
<service
android:process=":qt_service"
android:name=".MeAndroidService"
android:foregroundServiceType="connectedDevice"
android:stopWithTask="true"
android:exported="false">
<meta-data
android:name="android.app.lib_name"
android:value="service"/>
<meta-data
android:name="android.app.background_running"
android:value="true"/>
</service>
</application>
</manifest>

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
ComboBox {
id: control
font.pixelSize: 16
font.family: fontText.name
delegate: ItemDelegate {
width: control.width
height: control.height
contentItem: Text {
text: modelData
font: control.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
highlighted: control.highlightedIndex === index
}
contentItem: Text {
text: control.displayText
font: control.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 5
color: "#f0f0f0"
}
popup: Popup {
objectName: "popup"
y: control.height
width: control.width + 24
implicitHeight: contentItem.implicitHeight + 14
contentItem: ListView {
clip: true
implicitHeight: contentHeight + 10
model: control.popup.visible ? control.delegateModel : null
currentIndex: control.highlightedIndex
}
background: Rectangle {
color: "#e0e0e0"
border.width: 1
border.color: "gray"
radius: 10
}
}
}

View file

@ -0,0 +1,172 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Rectangle {
id: help
y: -rootItem.height
color: "#e0e0f0"
opacity: 0
Button {
width: 42
height: width
z: 1 // stay on top
anchors.right: parent.right
flat: true
font.family: fontText.name
font.pixelSize: 22
text: "x"
onClicked: help.enabled = false
}
Flickable {
id: flick
anchors.fill: parent
contentWidth: html.paintedWidth + 2 * html.padding
contentHeight: html.paintedHeight + 50
clip: true
Text {
id: html
width: help.width
padding: 10
wrapMode: Text.WordWrap
font.family: fontText.name
font.pixelSize: 18
color: "#303030"
textFormat: Text.RichText
text: "
<h3>
<img src='../../img/radio.png' width=60 height=60>
<br>Radios
</h3>
<h4>BLE</h4>
<p>
If you use more than 1 radio, switch here to the radio you want to use.
</p>
<p>
To manually restart device discovery, press-and-hold on the radio icon.
</p>
<p>
If your radio is not found, it may help to turn it off/on again.
</p>
%1
%2
<h4>WiFi</h4>
%3
<p>
Use the Python CLI to setup your connection like this:
</p>
<pre>
meshtastic &#92;
--set network.wifi_enabled true &#92;
--set network.wifi_ssid \"&lt;name&gt;\" &#92;
--set network.wifi_psk \"&lt;password&gt;\"
</pre>
<p>
The app will ask for your radio IP, which can be found on its screen as soon as it is connected to WiFi.
</p>
<h3>
<img src='../../img/group.png' width=60 height=60>
<br>Group
</h3>
<p>
Here you can see the list of all radios using your same channel name. Every radio represents a person. This view is populated automatically.
</p>
<p>
Choose 'Broadcast' (on top) to send a message to every person in the group.
</p>
<p>
You can set a name to every radio/person listed here, which defaults to 'Anonym': a press-and-hold on the name will enter edit mode.
</p>
<p>
In the main menu you can change your channel name (which defaults to 'LongFast'). Only radios which share the same channel name are able to exchange messages.
</p>
<p>
A tap on the location item on the right shows a map with all known positions of the persons. The map is cached automatically for offline usage, which means: once you visited a place on the map, it will remain available even without internet connection.
</p>
<p>
To set your location manually, see 'hand' button (top right). This will override any eventually received GPS location.
</p>
<h3>
<img src='../../img/message.png' width=60 height=60>
<br>Messages
</h3>
<p>
Since the message length is limited, the border of the editor will turn red if the message is too long for sending.
</p>
<p>
To copy a message to the clipboard, press-and-hold it.
</p>
<p>
To see the exact date of a message, tap on its hour.
</p>
<p>
To delete a message, swipe it to the right and tap on the delete button.
</p>
<p>
Tap on search (icon on the right) to enter/leave search mode. The search term (case insensitive) is highlighted in red.
</p>
<p>
Eventual unread messages from other persons are indicated by a red circle, and the number of unread messages in <b>Group</b>.
</p>
<p>
A double click on a message will switch to <b>Group</b>.
</p>
<p>&nbsp;</p>
<h3>Advanced topics</h3>
<h4>Simple signal strength test</h4>
<p>
For a trivial signal test you can use the special text message <b>:e</b> (for 'echo'), which will send back the text you sent, adding signal <b>SNR</b>/<b>RSSI</b>, position and distance. This is convenient to test signal strength from different places, and have it logged in your messages.
</p>
<p>
Please note that this requires the receiver to run this app in foreground mode.
</p>
<h4>Save / Restore app data</h4>
<p>
A local web-server is included on mobile for saving and restoring all of: message database, app settings, eventually cached map tiles (for offline usage). Just use special text message <b>:w</b> (for 'web-server') and <b>:ws</b> (for 'stop web-server') after you're done.
</p>
<p>
After starting the server, enter the shown URL in your desktop browser, and follow the instructions.
</p>
<p>
Using this method you can easily transfer all data from one mobile device to any other device.
</p>
<p>
The desktop data paths are:
<ul>
<li><b>Linux</b>: <br><code>/home/&lt;user&gt;/.local/share/cl-meshtastic/</code>
<li><b>macOS</b>: <br><code>/Users/&lt;user&gt;/Library/Application Support/cl-meshtastic/</code>
<li><b>Windows</b>: <br><code>C:\\Users\\&lt;user&gt;\\AppData\\Local\\cl-meshtastic\\</code>
</ul>
<p>
Eventual backups are saved in above path under <code>backups/</code>. On the desktop see 'Make backup' in main menu.
</p>
<p>
To autmatically restore data from a backup on the desktop, put the backup files directly in above path (that is, under <code>.../cl-meshtastic/</code>) and restart the app. The data will be restored and the (obsolete) backup files will be deleted.
</p>".arg((Qt.platform.os === "android")
? "<p>On some devices it may be necessary to first unpair your radio, then press-and-hold on the radio icon (to restart device discovery).</p><p><i>N.B: If you previously used a radio with the official app, you'll need to disable the radio in the official app first, otherwise it will not show up in this app.</i></p>"
: "")
.arg((Qt.platform.os !== "ios")
? "<h4>USB</h4>
<p>
You may need to install serial drivers first (except on android), and you need to use a data USB cable.
</p>"
: "")
.arg((Qt.platform.os === "ios")
? "<p><i><font color=crimson><b>Warning:</b></font> WiFi will disconnect in background mode, and only re-connect when app is brought back to foreground (iOS only).</i></p>"
: "")
}
}
states: [
State { when: help.enabled; PropertyChanges { target: help; opacity: 1; y: 0; }},
State { when: !help.enabled; PropertyChanges { target: help; opacity: 0; y: -rootItem.height; }}
]
transitions: [
Transition { NumberAnimation { properties: "opacity,y"; duration: 300; easing.type: Easing.InOutQuad }}
]
}

View file

@ -0,0 +1,42 @@
import QtQuick
Rectangle {
anchors.fill: parent
color: "#ccebc5"
visible: animation.running
Image {
id: hourglass1
anchors.centerIn: parent
width: 40
fillMode: Image.PreserveAspectFit
source: "../../img/hourglass.png"
}
Image {
id: hourglass2
anchors.centerIn: parent
width: hourglass1.width
fillMode: Image.PreserveAspectFit
source: "../../img/hourglass.png"
opacity: 0
}
SequentialAnimation {
id: animation
objectName: "hourglass"
loops: Animation.Infinite
running: true
RotationAnimation { target: hourglass1; from: 0; to: 180; duration: 1000; easing.type: Easing.InOutSine }
ParallelAnimation {
NumberAnimation { target: hourglass1; property: "opacity"; from: 1; to: 0; duration: 1500; easing.type: Easing.InOutSine }
NumberAnimation { target: hourglass2; property: "opacity"; from: 0; to: 1; duration: 1500; easing.type: Easing.InOutSine }
}
// reset
NumberAnimation { target: hourglass1; property: "opacity"; to: 1; duration: 0 }
NumberAnimation { target: hourglass2; property: "opacity"; to: 0; duration: 0 }
}
}

View file

@ -0,0 +1,14 @@
import QtQuick
Image {
horizontalAlignment: Image.AlignHCenter
verticalAlignment: Image.AlignVCenter
width: header.height
height: width
MouseArea {
anchors.fill: parent
onClicked: swipeView.currentIndex = parent.Positioner.index
onPressAndHold: Lisp.call("app:icon-press-and-hold", parent.objectName)
}
}

View file

@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Ext
import "../group/" as Grp
import "../messages/" as Msg
import "../radios/" as Rad
Item {
anchors.fill: parent
property alias pageIndex: swipeView.currentIndex
Rectangle {
id: header
width: parent.width
height: rootItem.headerHeight
color: "#f2f2f2"
Row {
height: parent.height
spacing: 5
anchors.horizontalCenter: parent.horizontalCenter
Ext.MainIcon {
objectName: "group_icon"
source: "../../img/group.png"
Rectangle {
objectName: "unread_messages"
width: 10
height: width
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 7
radius: width / 2
color: "#ff4040"
visible: false
}
}
Ext.MainIcon {
objectName: "message_icon"
source: "../../img/message.png"
}
Ext.MainIcon {
objectName: "radio_icon"
source: "../../img/radio.png"
}
}
}
SwipeView {
id: swipeView
objectName: "main_view"
y: header.height
width: parent.width
height: parent.height - header.height
currentIndex: 1
interactive: false
Grp.Group { id: group }
Msg.Messages {}
Rad.Radios {}
onCurrentIndexChanged: Lisp.call("app:view-index-changed", currentIndex)
}
PageIndicator {
id: control
y: header.height - 12
count: swipeView.count
currentIndex: swipeView.currentIndex
anchors.horizontalCenter: parent.horizontalCenter
delegate: Rectangle {
width: header.height
height: 5
radius: width / 2
color: "dodgerblue"
opacity: (index === control.currentIndex) ? 1 : 0
Behavior on opacity { OpacityAnimator { duration: 500 }}
}
}
}

View file

@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Menu {
width: 250
font.family: fontText.name
font.pixelSize: 18
}

View file

@ -0,0 +1,9 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
MenuItem {
font.family: fontText.name
font.pixelSize: 18
}

View file

@ -0,0 +1,67 @@
import QtQuick
Rectangle {
id: toast
objectName: "toast"
x: (parent.width - width) / 2
y: (parent.height - height) / 2
z: 99
width: msg.contentWidth + 70
height: msg.contentHeight + 30
color: "#303030"
border.width: 2
border.color: "white"
radius: Math.min(25, height / 2)
opacity: 0
visible: false
function message(text, seconds) { // called from Lisp
pause.duration = 1000 * ((seconds === 0) ? (24 * 60 * 60) : seconds)
toast.visible = true
msg.text = text
anim.start()
}
Text {
id: msg
font.pixelSize: 16
font.bold: true
anchors.centerIn: parent
color: "white"
wrapMode: Text.WordWrap
width: toast.parent.width - 2 * toast.radius - 10
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
MouseArea {
anchors.fill: parent
onClicked: toast.visible = false
}
}
SequentialAnimation {
id: anim
onFinished: { toast.visible = false }
OpacityAnimator {
from: 0
to: 0.8
target: toast
easing.type: Easing.InOutQuart
duration: 500
}
PauseAnimation {
id: pause
duration: 3000
}
OpacityAnimator {
from: 0.8
to: 0
target: toast
easing.type: Easing.InOutQuart
duration: 1500
}
}
}

View file

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs
Dialog {
anchors.centerIn: parent
standardButtons: Dialog.Ok | Dialog.Cancel
property alias text: message.text
property string callback
Column {
width: parent.width
spacing: 5
Text {
id: message
width: parent.width
wrapMode: Text.Wrap
}
}
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,28 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Dialog {
anchors.centerIn: parent
font.pixelSize: 18
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
property alias text: message.text
property string callback
Column {
width: parent.width
spacing: 5
Text {
id: message
width: parent.width
wrapMode: Text.Wrap
font.pixelSize: 18
}
}
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,65 @@
import QtQuick
Item {
id: dialogs
objectName: "dialogs"
anchors.fill: parent
Loader {
id: loader
anchors.centerIn: parent
}
function message(text) {
loader.active = false // force reload
if (rootItem.mobile) {
loader.source = "MessageMobile.qml"
} else {
loader.source = "Message.qml"
}
loader.active = true
loader.item.text = text
rootItem.showKeyboard(false)
loader.item.open()
}
function confirm(text, callback) {
loader.active = false // force reload
if (rootItem.mobile) {
loader.source = "ConfirmMobile.qml"
} else {
loader.source = "Confirm.qml"
}
loader.active = true
loader.item.text = text
loader.item.callback = callback
rootItem.showKeyboard(false)
loader.item.open()
}
function input(label, callback, text, placeholderText,
maxLength, inputMask, numbersOnly,
from, to, value) {
loader.active = false // force reload
if (rootItem.mobile) {
loader.source = "InputMobile.qml"
} else {
loader.source = "Input.qml"
}
loader.active = true
loader.item.label = label
loader.item.callback = callback
loader.item.text = text
loader.item.placeholderText = placeholderText
loader.item.maxLength = maxLength
loader.item.inputMask = inputMask
loader.item.numbersOnly = numbersOnly
loader.item.from = from
loader.item.to = to
loader.item.value = value
var keyboard = (text !== "") || (placeholderText !== "")
loader.item.open()
if (keyboard) loader.item.setFocus()
Qt.callLater(rootItem.showKeyboard, keyboard)
}
}

View file

@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs
Dialog {
anchors.centerIn: parent
standardButtons: Dialog.Ok | Dialog.Cancel
property alias label: label.text
property alias text: edit.text
property alias placeholderText: edit.placeholderText
property alias inputMask: edit.inputMask
property alias maxLength: edit.maximumLength
property alias from: spinBox.from
property alias to: spinBox.to
property alias value: spinBox.value
property bool numbersOnly
property string callback
function setFocus() { edit.forceActiveFocus() }
Column {
width: parent.width
spacing: 5
Text {
id: label
width: parent.width
wrapMode: Text.Wrap
visible: (text !== "")
}
TextField {
id: edit
objectName: "dialog_line_edit"
width: parent.width
visible: !spinBox.visible
}
SpinBox {
id: spinBox
objectName: "dialog_spin_box"
anchors.horizontalCenter: parent.horizontalCenter
visible: !!value
}
}
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Dialog {
anchors.centerIn: parent
font.pixelSize: 18
modal: true
standardButtons: Dialog.Ok | Dialog.Cancel
property alias label: label.text
property alias text: edit.text
property alias placeholderText: edit.placeholderText
property alias inputMask: edit.inputMask
property alias maxLength: edit.maximumLength
property alias from: spinBox.from
property alias to: spinBox.to
property alias value: spinBox.value
property bool numbersOnly
property string callback
function setFocus() { edit.forceActiveFocus() }
Column {
width: parent.width
spacing: 5
Text {
id: label
width: parent.width
wrapMode: Text.Wrap
font.pixelSize: 18
visible: (text !== "")
}
TextField {
id: edit
objectName: "dialog_line_edit"
width: parent.width
visible: !spinBox.visible
inputMethodHints: numbersOnly ? Qt.ImhFormattedNumbersOnly : Qt.ImhNone
}
SpinBox {
id: spinBox
objectName: "dialog_spin_box"
anchors.horizontalCenter: parent.horizontalCenter
visible: !!value
}
}
onAccepted: Lisp.call(callback, true)
onRejected: Lisp.call(callback, false)
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Dialogs
Dialog {
anchors.centerIn: parent
title: qsTr("Info")
standardButtons: Dialog.Ok
property alias text: message.text
Text {
id: message
width: parent.width
wrapMode: Text.Wrap
}
}

View file

@ -0,0 +1,20 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Dialog {
anchors.centerIn: parent
title: qsTr("Info")
font.pixelSize: 18
modal: true
standardButtons: Dialog.Ok
property alias text: message.text
Text {
id: message
width: parent.width
wrapMode: Text.Wrap
font.pixelSize: 18
}
}

View file

@ -0,0 +1,275 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Grp
import "../common/" as Com
Rectangle {
id: rect
color: "#ccebc5"
Row {
id: rowModem
padding: 9
spacing: 9
Com.ComboBox {
id: modem
objectName: "modem"
width: 160
font.pixelSize: 16
font.family: fontText.name
onActivated: Lisp.call("lora:change-modem-preset")
}
Text {
height: modem.height
font.pixelSize: 16
font.family: fontText.name
verticalAlignment: Text.AlignVCenter
text: qsTr("modem preset")
}
}
ListView {
id: view
objectName: "group_view"
anchors.topMargin: rowModem.height
anchors.bottomMargin: channel.height
anchors.fill: parent
anchors.margins: 9
spacing: 9
clip: true
delegate: groupDelegate
model: group
currentIndex: -1
}
Rectangle {
id: channel
anchors.bottom: parent.bottom
width: parent.width
height: 28
color: "#555"
Text {
objectName: "channel_name"
anchors.centerIn: parent
font.pixelSize: 16
font.family: fontText.name
font.weight: Font.DemiBold
color: rect.color
text: "cl-app"
}
}
ListModel {
id: group
objectName: "group"
// hack to define all model key _types_
ListElement {
radioName: ""; customName: ""; nodeNum: ""; unread: 0; current: false
}
function addPerson(person) {
// insert sorted
var i = 1; // 0 is broadcast
var broadcast = (count === 0)
for (; i < count; i++) {
if (person.customName < get(i).customName) {
insert(i, person)
break
}
}
if (broadcast || (i === count)) {
append(person)
}
if (person.current) {
view.currentIndex = broadcast ? 0 : i
view.positionViewAtIndex(view.currentIndex, ListView.Contain)
rootItem.broadcast = broadcast
}
}
function sortRenamed(name, index) {
var to = -1
if (name < get(1).customName) { // 0 is broadcast
to = 1
} else if (name >= get(count - 1).customName) {
to = count - 1
} else {
for (var i = 1; i < count; i++) {
if ((i !== index) && (name < get(i).customName)) {
to = (index > i) ? i : i - 1
break
}
}
}
if (to !== -1) {
move(index, to, 1)
view.currentIndex = to
view.positionViewAtIndex(to, ListView.Contain)
}
}
function radioNames() {
var names = []
for (var i = 0; i < count; i++) {
names.push(get(i).radioName)
}
return names
}
function setUnread(name, n) {
for (var i = 0; i < count; i++) {
if (get(i).radioName === name) {
setProperty(i, "unread", n)
break
}
}
}
Component.onCompleted: remove(0) // see hack above
}
Component {
id: groupDelegate
Rectangle {
id: delegate
width: Math.min(265, view.width)
height: 35
color: (index === view.currentIndex) ? "firebrick" : "darkcyan"
radius: height / 2
Rectangle {
id: rectRadio
x: (index === 0) ? 18 : 10
width: (index === 0) ? 28 : 42
height: (index === 0) ? width : 15
anchors.verticalCenter: parent.verticalCenter
color: "#f0f0f0"
radius: height / 2
Image {
anchors.centerIn: parent
width: 20
height: width
source: "../../img/broadcast.png"
visible: (index === 0)
}
Text {
anchors.centerIn: parent
font.pixelSize: 12
font.family: fontText.name
font.weight: Font.DemiBold
color: "black"
text: model.radioName
visible: (index !== 0)
}
}
function selected() {
view.currentIndex = index
Lisp.call("lora:change-receiver", model.nodeNum)
rootItem.broadcast = (index === 0)
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: selected()
}
// custom name
TextField {
id: name
x: 58
anchors.verticalCenter: parent.verticalCenter
leftPadding: 2
font.pixelSize: 18
font.family: fontText.name
font.weight: Font.DemiBold
color: readOnly ? "white" : "#505050"
palette.highlight: "darkcyan"
palette.highlightedText: "white"
text: (model.customName === "~") ? "Anonym" : model.customName
readOnly: true
background: Rectangle {
y: 4
width: delegate.width - 1.5 * delegate.height - rectRadio.width
height: delegate.height - 12
color: name.readOnly ? "transparent" : "#f0f0f0"
border.width: 0
}
onPressAndHold: {
if (index !== 0) {
readOnly = false
selectAll()
forceActiveFocus()
Qt.inputMethod.show() // needed for SailfishOS
}
}
onEditingFinished: {
if (!readOnly) {
readOnly = true
var _text = (text === "") ? "~" : text
group.setProperty(index, "customName", _text)
Lisp.call("group:name-edited", model.radioName, _text)
Qt.callLater(group.sortRenamed, _text, index) // 'Qt.callLater': prevent UI thread related crash
if (_text === "~") text = "Anonym"
}
}
onReleased: if (readOnly) selected()
}
// unread messages
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 8
width: 22
height: width
radius: width / 2
color: "#ff4040"
visible: (model.unread > 0)
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 12
font.weight: Font.DemiBold
font.family: fontText.name
text: model.unread
color: "white"
}
}
}
}
Grp.Map {
objectName: "map_view"
anchors.fill: rect
visible: false
}
Timer {
interval: 15 * 60 * 1000 // 15 min
repeat: true
running: true
onTriggered: Lisp.call("lora:get-node-config")
}
}

View file

@ -0,0 +1,204 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtLocation
import QtPositioning
import "." as Ext
Item {
anchors.fill: parent
Component {
id: mapComponent
Item {
anchors.fill: parent
property bool manualLocation: false
property var myMarker: null
Map {
id: map
objectName: "map"
anchors.fill: parent
plugin: mapPlugin
zoomLevel: 14
property geoCoordinate startCentroid
SequentialAnimation {
id: markerAnimation
loops: Animation.Infinite
running: manualLocation && (myMarker !== null)
OpacityAnimator { target: myMarker; from: 1.0; to: 0.2; duration: 500; easing.type: Easing.InOutSine }
OpacityAnimator { target: myMarker; from: 0.2; to: 1.0; duration: 500; easing.type: Easing.InOutSine }
}
MouseArea {
anchors.fill: parent
onClicked: (mouse) => {
if (manualLocation) {
manualLocation = false
var coord = map.toCoordinate(Qt.point(mouse.x, mouse.y))
myMarker.coordinate = coord
myMarker.opacity = 1
Lisp.call("loc:position-selected", coord.latitude, coord.longitude)
}
}
}
function coordinate(pos) {
return QtPositioning.coordinate(pos[0], pos[1])
}
function setCenter(pos) {
center = coordinate(pos)
}
function showMarker(n, nodeNum, name, customName = "") {
var pos = Lisp.call("loc:position*", nodeNum)
if (pos) {
var marker = markers.itemAt(n)
marker.radioName = name
marker.customName = (customName === "~") ? "" : customName
marker.coordinate = coordinate(pos)
marker.visible = true
}
}
function updatePositions(myNum, myName, group) {
var n = 0
showMarker(n++, myNum, myName)
for (var i = 1; i < group.count; i++) {
var data = group.get(i)
showMarker(n++, data.nodeNum, data.radioName, data.customName)
}
}
Plugin {
id: mapPlugin
name: "osm" // Open Street Map
// for downloading tiles
PluginParameter {
name: "osm.mapping.cache.directory"
value: Lisp.call("loc:tile-path")
}
// for offline tiles (from cache)
PluginParameter {
name: "osm.mapping.offline.directory"
value: Lisp.call("loc:tile-path")
}
// number tiles (instead of MB)
PluginParameter {
name: "osm.mapping.cache.disk.cost_strategy"
value: "unitary"
}
// max number cached/offline tiles
PluginParameter {
name: "osm.mapping.cache.disk.size"
value: 10000
}
// local tile provider (no API key needed)
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: Lisp.call("loc:tile-provider-path")
}
}
// handlers and shortcuts taken from Qt6 minimal map example
PinchHandler {
id: pinch
target: null
onActiveChanged: if (active) {
map.startCentroid = map.toCoordinate(pinch.centroid.position, false)
}
onScaleChanged: (delta) => {
map.zoomLevel += Math.log2(delta)
map.alignCoordinateToPoint(map.startCentroid, pinch.centroid.position)
}
onRotationChanged: (delta) => {
map.bearing -= delta
map.alignCoordinateToPoint(map.startCentroid, pinch.centroid.position)
}
grabPermissions: PointerHandler.TakeOverForbidden
}
WheelHandler {
id: wheel
// workaround
acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland"
? PointerDevice.Mouse | PointerDevice.TouchPad
: PointerDevice.Mouse
rotationScale: 1/120
property: "zoomLevel"
}
DragHandler {
id: drag
target: null
onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y)
}
Shortcut {
enabled: map.zoomLevel < map.maximumZoomLevel
sequence: StandardKey.ZoomIn
onActivated: map.zoomLevel = Math.round(map.zoomLevel + 1)
}
Shortcut {
enabled: map.zoomLevel > map.minimumZoomLevel
sequence: StandardKey.ZoomOut
onActivated: map.zoomLevel = Math.round(map.zoomLevel - 1)
}
// node markers
Ext.Markers {
id: markers
objectName: "markers"
}
}
// manual marker buttons
Ext.MapButton {
id: hand
objectName: "add_manual_marker"
anchors.top: parent.top
icon.source: "../../img/hand.png"
visible: false
onClicked: {
manualLocation = !manualLocation
if (manualLocation) {
if (markers.count === 0) {
Lisp.call("loc:add-manual-marker")
}
myMarker = markers.itemAt(0)
} else {
myMarker = null
}
}
}
Ext.MapButton {
objectName: "remove_marker"
anchors.top: hand.bottom
icon.source: "../../img/remove-marker.png"
onClicked: {
markers.itemAt(0).visible = false
Lisp.call("loc:remove-marker")
visible = false
}
}
}
}
Loader {
id: mapLoader
objectName: "map_loader"
anchors.fill: parent
sourceComponent: mapComponent
active: false
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
RoundButton {
z: 1
anchors.right: parent.right
anchors.margins: 5
icon.color: "#eee"
icon.width: 28
width: 38
height: width
radius: width / 2
palette.button: "#555"
focusPolicy: Qt.NoFocus
}

View file

@ -0,0 +1,61 @@
import QtQuick
import QtLocation
Repeater {
model: Lisp.call("loc:position-count")
MapQuickItem {
anchorPoint.x: image.width / 2
anchorPoint.y: image.height
visible: false
property alias radioName: radioName.text
property alias customName: customName.text
sourceItem: Image {
id: image
width: 25
height: width
source: "../../img/marker.png"
Rectangle {
x: -(width - image.width) / 2
y: image.height + 5
width: customName.width + 42
height: 20
color: (index === 0) ? "#ff3d00" : "darkcyan"
radius: height / 2
Rectangle {
x: 2
anchors.verticalCenter: parent.verticalCenter
width: 38
height: 16
color: "#f0f0f0"
radius: height / 2
Text {
id: radioName
anchors.centerIn: parent
font.pixelSize: 12
font.family: fontText.name
font.weight: Font.DemiBold
color: "black"
}
}
Text {
id: customName
x: 44
anchors.verticalCenter: parent.verticalCenter
width: paintedWidth ? (paintedWidth + 10) : 0
font.pixelSize: 14
font.family: fontText.name
font.weight: Font.DemiBold
color: "white"
}
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
ScrollView {
width: parent.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
property alias model: grid.model
GridView {
id: grid
anchors.fill: parent
cellWidth: emojis.itemSize
cellHeight: emojis.itemSize
leftMargin: 2
topMargin: 2
clip: true
highlightFollowsCurrentItem: false
focus: true
delegate: Text {
width: emojis.itemSize
height: emojis.itemSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: emojis.itemSize - 4
text: modelData
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
Lisp.call("app:emoji-clicked",
grid.itemAtIndex(grid.indexAt(mouse.x, mouse.y + grid.contentY)).text)
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Msg
Rectangle {
id: emojis
objectName: "emojis"
height: 7 * itemSize + 2 * column.spacing + 8
radius: 12
color: "white"
border.width: 1
border.color: "gray"
property int itemSize: 42
Column {
id: column
anchors.fill: parent
spacing: 5
Msg.EmojiView {
objectName: "recent_emojis"
height: itemSize + 1
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
model: ["🙂","🤣","👍"]
}
Msg.EmojiView {
height: itemSize * 3 + 1
model: ["😃","😄","😁","😆","😅","😂","🤣","🥲","🥹","😊","😇","🙂","🙃","😉","😌","😍","🥰","😘","😗","😙","😚","😋","😛","😝","😜","🤪","🤨","🧐","🤓","😎","🥸","🤩","🥳","😏","😒","😞","😔","😟","😕","🙁","😣","😖","😫","😩","🥺","😢","😭","😮‍💨","😤","😠","😡","🤬","🤯","😳","🥵","🥶","😱","😨","😰","😥","😓","🫣","🤗","🫡","🤔","🫢","🤭","🤫","🤥","😶","😶‍🌫️","😐","😑","😬","🫨","🫠","🙄","😯","😦","😧","😮","😲","🥱","😴","🤤","😪","😵","😵‍💫","🫥","🤐","🥴","🤢","🤮","🤧","😷","🤒","🤕","🤑","🤠","😈","👿","👹","👺","🤡","💩","👻","💀","👽","👾","🤖","🎃","😺","😸","😹","😻","😼","😽","🙀","😿","😾"]
}
Msg.EmojiView {
height: itemSize * 3 + 1
model: ["👋","🤚","🖐","✋","🖖","👌","🤌","🤏","🤞","🫰","🤟","🤘","🤙","🫵","🫱","🫲","🫸","🫷","🫳","🫴","👈","👉","👆","🖕","👇","👍","👎","✊","👊","🤛","🤜","👏","🫶","🙌","👐","🤲","🤝","🙏","💅","🤳","💪","🦾","🦵","🦿","🦶","👣","👂","🦻","👃","🫀","🫁","🧠","🦷","🦴","👀","👁","👅","👄","🫦","💋","🩸"]
}
}
}

View file

@ -0,0 +1,331 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Msg
Rectangle {
id: main
color: hourglass.visible ? "#d2eecc" : "#e5d8bd"
ListView {
id: view
objectName: "message_view"
anchors.topMargin: rectFind.height + 4
anchors.fill: parent
anchors.bottomMargin: rectEdit.height + 3
anchors.margins: 5
model: messages
clip: true
visible: false
property int fontSize: 18
delegate: SwipeDelegate {
id: swipeDelegate
width: view.width
height: delegate.height
clip: true
onPressAndHold: Lisp.call("msg:message-press-and-hold", model.text)
onDoubleClicked: Lisp.call("msg:swipe-to-left")
background: Item {
id: delegate
width: Math.max(text.paintedWidth, rowSender.width + view.fontSize / 4 * text.padding)
+ 2 * text.padding + view.fontSize / 4
height: model.hidden ? 0 : (text.contentHeight + 2 * text.padding + sender.contentHeight + 8)
Rectangle {
anchors.centerIn: parent
width: parent.width
height: parent.height - 4
color: model.me ? "#f2f2f2" : "#ffffcc"
radius: 12
Row {
id: rowSender
padding: text.padding
spacing: padding - 2
AnimatedImage {
id: semaphore
anchors.verticalCenter: sender.verticalCenter
anchors.verticalCenterOffset: -0.5
width: view.fontSize / 2 - 1
height: width
playing: false
source: "../../img/semaphore.gif"
currentFrame: model.ackState ? parseInt(model.ackState.substr(2), 16) : 0 // see 'qml:hex'
visible: model.me
}
Text {
id: sender
font.pixelSize: 2/3 * view.fontSize
font.family: fontText.name
color: "#8B0000"
text: model.senderName ? model.senderName : model.sender
}
}
Text {
id: timestamp
x: delegate.width - contentWidth - text.padding
y: text.padding
font.pixelSize: 2/3 * view.fontSize
font.family: fontText.name
color: "#505050"
text: model.hour
MouseArea {
anchors.fill: parent
onClicked: Lisp.call("msg:show-date", model.timestamp)
}
}
Text {
id: text
y: sender.contentHeight
width: main.width - 10
padding: 5
wrapMode: Text.Wrap
font.pixelSize: view.fontSize
font.family: fontText.name
color: "#303030"
textFormat: Text.StyledText // for 'paintedWidth' to always work
text: model.text
}
}
}
ListView.onRemove: SequentialAnimation {
PropertyAction {
target: swipeDelegate
property: "ListView.delayRemove"
value: true
}
NumberAnimation {
target: swipeDelegate
property: "height"
to: 0
easing.type: Easing.InOutQuad
}
PropertyAction {
target: swipeDelegate
property: "ListView.delayRemove"
value: false
}
}
swipe.left: Rectangle {
y: 2
width: 35
height: parent.height - 2 * y
color: "#dd4141"
radius: 12
Image {
anchors.centerIn: parent
width: 12
height: width
source: "../../img/delete.png"
}
MouseArea {
anchors.fill: parent
onClicked: {
var mid = model.mid
view.model.remove(index)
Lisp.call("db:delete-message", mid)
}
}
}
}
}
ListModel {
id: messages
objectName: "messages"
// hack to define all model key _types_
ListElement {
receiver: ""; sender: ""; senderName: ""; timestamp: ""; hour: "";
text: ""; text2: ""; mid: ""; ackState: ""; me: true; hidden: false
}
function addMessage(message) { append(message) }
function changeState(state, mid) {
for (var i = count - 1; i >= 0; i--) {
if (get(i).mid === mid) {
setProperty(i, "ackState", state)
break
}
}
}
function find(term) {
for (var i = 0; i < count; i++) {
var text = get(i).text
var highlighted = Lisp.call("msg:highlight-term", text, term)
if (highlighted) {
if (!get(i).text2) {
setProperty(i, "text2", text)
}
setProperty(i, "text", highlighted)
}
setProperty(i, "hidden", !highlighted)
}
view.positionViewAtBeginning()
}
function clearFind() {
for (var i = 0; i < count; i++) {
var text2 = get(i).text2
if (text2) {
setProperty(i, "text", text2)
setProperty(i, "text2", "")
}
setProperty(i, "hidden", false)
}
}
Component.onCompleted: remove(0) // see hack above
}
// find text
TextField {
id: findText
objectName: "find_text"
y: 1
width: parent.width
height: visible ? (edit.paintedHeight + 14) : 0
font.pixelSize: view.fontSize
font.family: fontText.name
selectionColor: "#228ae3"
selectedTextColor: "white"
placeholderText: qsTr("search")
visible: false
background: Rectangle {
id: rectFind
color: "white"
border.width: 3
border.color: findText.focus ? "dodgerblue" : "#c0c0c0"
radius: 12
}
onEditingFinished: Lisp.call("msg:find-text", text)
}
// send text
Rectangle {
id: rectEdit
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
width: parent.width
height: edit.paintedHeight + 14
color: "white"
border.width: 3
border.color: edit.focus ? (edit.tooLong ? "#ff5f57" : "dodgerblue") : "#c0c0c0"
radius: 12
TextArea {
id: edit
objectName: "edit"
anchors.fill: parent
textFormat: TextEdit.PlainText
font.pixelSize: view.fontSize
font.family: fontText.name
selectionColor: "#228ae3"
selectedTextColor: "white"
wrapMode: TextEdit.Wrap
textMargin: 0
placeholderText: qsTr("message")
property bool tooLong: false
onLengthChanged: if (length > 150) Lisp.call("msg:check-utf8-length", text)
Keys.onEscapePressed: emojis.visible = false
Image {
y: 8
anchors.right: parent.right
anchors.rightMargin: 7
width: edit.font.pixelSize + 1
height: width
source: "../../img/emoji.png"
opacity: 0.55
visible: edit.focus && (Qt.platform.os !== "android") && (Qt.platform.os !== "ios")
MouseArea {
anchors.fill: parent
onClicked: emojis.visible = !emojis.visible
}
}
}
Image {
id: send
anchors.right: parent.right
anchors.bottom: parent.top
anchors.margins: 3
width: 38
height: width
source: "../../img/send.png"
visible: edit.focus && !edit.tooLong
MouseArea {
anchors.fill: parent
onClicked: {
edit.focus = Qt.NoFocus
Lisp.call("lora:send-message", edit.text)
edit.clear()
}
}
}
Image {
id: broadcast
anchors.right: send.left
anchors.bottom: parent.top
anchors.margins: 3
width: 38
height: width
opacity: 0.7
source: "../../img/broadcast.png"
visible: send.visible && animation.running
SequentialAnimation {
id: animation
loops: Animation.Infinite
running: rootItem.broadcast
ScaleAnimator {
target: broadcast
from: 0.8; to: 1.0
duration: 500
easing.type: Easing.InOutSine
}
ScaleAnimator {
target: broadcast
from: 1.0; to: 0.8
duration: 500
easing.type: Easing.InOutSine
}
}
}
}
Msg.Emojis {
id: emojis
anchors.bottom: rectEdit.top
anchors.bottomMargin: -1
width: main.width
visible: false
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 12
height: 25
color: (percent() > 15) ? "#f0f0f0" : "yellow"
radius: 2
border.width: 1
border.color: "#808080"
property string voltage
property string level
function percent() { return parseInt(level, 10) }
Rectangle {
x: 1
width: parent.width - 2
height: (parent.height - 2) * percent() / 100
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
color: (percent() > 15) ? "#28c940" : "#ff5f57"
}
Text {
x: -4 - paintedWidth
height: parent.height
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignRight
font.pixelSize: 11
font.family: fontText.name
font.weight: Font.DemiBold
color: "white"
text: voltage + "\n" + level
}
}

View file

@ -0,0 +1,129 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import "." as Rad
import "../common/" as Com
Rectangle {
id: rect
color: "#b3cde3"
Row {
id: rowRegion
padding: 9
spacing: 9
Com.ComboBox {
id: region
objectName: "region"
width: 110
font.pixelSize: 16
font.family: fontText.name
onActivated: Lisp.call("lora:change-region", currentIndex ? currentText : "")
}
Text {
height: region.height
font.pixelSize: 16
font.family: fontText.name
verticalAlignment: Text.AlignVCenter
text: qsTr("region")
}
}
ListView {
id: view
anchors.topMargin: rowRegion.height
anchors.fill: parent
anchors.margins: 9
spacing: 9
clip: true
delegate: radioDelegate
model: radios
}
ListModel {
id: radios
objectName: "radios"
// hack to define all model key _types_
ListElement {
name: ""; ini: false; hwModel: ""; voltage: ""; batteryLevel: ""; current: false
}
function addRadio(radio) {
// prevent multiple entries on device discovery problems
for (var i = 0; i < count; i++) {
if (get(i).name === radio.name) {
return
}
}
append(radio)
if (radio.current) {
view.currentIndex = view.count - 1
}
}
Component.onCompleted: remove(0) // see hack above
}
Component {
id: radioDelegate
Rectangle {
id: delegate
width: Math.min(265, view.width)
height: 35
color: (index === view.currentIndex) ? "firebrick" : (model.ini ? "#808080" : "steelblue")
radius: height / 2
Rectangle {
x: 10
width: 42
height: 15
anchors.verticalCenter: parent.verticalCenter
color: "#f0f0f0"
radius: height / 2
Text {
anchors.centerIn: parent
font.pixelSize: 12
font.family: fontText.name
font.weight: Font.DemiBold
color: "black"
text: model.name
}
}
Text {
x: 58
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 18
font.family: fontText.name
font.weight: Font.DemiBold
color: "white"
text: model.hwModel
}
Rad.BatteryLevel {
anchors.right: parent.right
anchors.rightMargin: 14
voltage: model.voltage
level: model.batteryLevel
visible: !model.ini
}
MouseArea {
anchors.fill: parent
onClicked: {
if (index > 0) { // current radio is 0
view.currentIndex = index
Lisp.call("radios:change-radio", model.name)
}
}
}
}
}
}

View file

@ -0,0 +1,220 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Window
import QtPositioning
import "ext/common/" as Com
import "ext/dialogs/" as Dlg
Item {
id: rootItem
objectName: "main"
width: 350
height: 550
property double headerHeight: 48
property bool mobile: Lisp.call("mobile-p")
property bool broadcast: false
function showKeyboard(show) {
show ? Qt.inputMethod.show() : Qt.inputMethod.hide()
}
Com.MainView { id: view }
Com.Menu {
id: menu
objectName: "menu"
function show() { popup(0, headerHeight) }
Com.MenuItem {
objectName: "help"
text: qsTr("Help")
onTriggered: help.active ? help.item.enabled = !help.item.enabled : help.active = true
}
Com.MenuItem {
text: qsTr("Update group/nodes")
onTriggered: Lisp.call("lora:get-node-config")
enabled: (view.pageIndex === 0)
}
Com.MenuItem {
text: qsTr("Channel name...")
onTriggered: Lisp.call("lora:edit-channel-name")
enabled: (view.pageIndex === 0)
}
Com.MenuItem {
text: qsTr("Message font size...")
onTriggered: Lisp.call("msg:font-size-dialog")
enabled: (view.pageIndex === 1)
}
MenuSeparator {}
Com.MenuItem {
objectName: "share_location"
text: qsTr("Share my location")
checkable: true
onTriggered: Lisp.call("loc:share-my-location", checked)
}
MenuSeparator {}
Com.Menu {
id: connection
title: qsTr("Connection")
enabled: (view.pageIndex === 2)
function changed(name) { Lisp.call("radios:connection-changed", name) }
Com.MenuItem {
objectName: "BLE"
text: "BLE"
autoExclusive: true
checkable: true
checked: true
onTriggered: connection.changed(objectName)
}
Com.MenuItem {
objectName: "USB"
text: "USB"
autoExclusive: true
checkable: true
onTriggered: connection.changed(objectName)
}
Com.MenuItem {
objectName: "WIFI"
text: "WiFi"
autoExclusive: true
checkable: true
onTriggered: connection.changed(objectName)
Component.onCompleted: if (Qt.platform.os === "ios") { palette.windowText = "crimson" }
}
}
Com.MenuItem {
text: qsTr("Reset node DB")
onTriggered: Lisp.call("lora:reset-node-db")
enabled: (view.pageIndex === 0)
}
Com.MenuItem {
text: qsTr("Export message DB (Lisp)")
onTriggered: Lisp.call("db:export-to-list")
enabled: (view.pageIndex === 1)
}
Com.MenuItem {
text: qsTr("Make backup")
onTriggered: Lisp.call("app:make-backup")
enabled: !mobile
}
}
Image {
source: "img/logo.png"
x: 2
y: 2
width: headerHeight
height: width
MouseArea {
anchors.fill: parent
onClicked: menu.show()
}
}
Image { // location icon ('Group')
objectName: "location"
source: "img/location.png"
width: headerHeight
height: width
anchors.right: parent.right
visible: (view.pageIndex === 0)
MouseArea {
anchors.fill: parent
onClicked: Lisp.call("loc:show-map-clicked")
}
}
Image { // find icon ('Messages')
objectName: "find"
source: "img/find.png"
width: headerHeight
height: width
anchors.right: parent.right
visible: (view.pageIndex === 1)
MouseArea {
anchors.fill: parent
onClicked: Lisp.call("msg:find-clicked")
}
}
Com.Hourglass { // animation while loading app
id: hourglass
}
AnimatedImage {
objectName: "busy"
anchors.centerIn: parent
width: 34
height: width
z: 10
source: "img/busy.gif"
visible: playing
playing: false
}
// GPS
PositionSource {
objectName: "position_source"
updateInterval: 2000
active: false
property double lat: 0
property double lon: 0
property double alt: 0
property string time: "0" // no 'long' in JS
onPositionChanged: {
if (position.latitudeValid && position.longitudeValid) {
var coor = position.coordinate;
lat = coor.latitude
lon = coor.longitude
alt = position.altitudeValid ? coor.altitude : 0
if (position.timestamp) {
var stime = String(position.timestamp.getTime())
time = stime.substring(0, stime.length - 3)
} else {
time = "0"
}
}
}
function lastPosition() {
return [lat, lon, alt, time]
}
}
Com.Toast {}
Dlg.Dialogs {}
Loader {
id: help
y: headerHeight
width: parent.width
height: parent.height - headerHeight
source: "ext/common/Help.qml"
active: false
}
FontLoader { id: fontText; source: "fonts/Ubuntu.ttf" }
FontLoader { id: fontText2; source: "fonts/Ubuntu-Medium.ttf" }
}

2
examples/Qt6/planets/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,193 @@
import QtQuick
Item {
id: main
objectName: "main"
width: 300
height: 500
Rectangle {
anchors.fill: parent
color: "#101010"
}
ListView {
id: view
objectName: "view"
anchors.fill: parent
delegate: planetInfo
model: planets
}
ListModel {
id: planets
objectName: "planets"
// example of inline item
//ListElement { name: "Earth"; shape: "img/earth.png"; map: "img/earth-map.jpg"; info: "..." }
function addPlanet(planet) { append(planet) }
}
property int itemHeight: 44
Component {
id: planetInfo
Item {
id: wrapper
width: view.width
height: itemHeight
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: itemHeight
color: "#303060"
border.color: Qt.lighter(color, 1.2)
Text {
x: 15
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 4
font.pixelSize: parent.height - 22
color: "#f0f0f0"
text: model.name // see Lisp keyword name
}
}
Rectangle {
id: image
width: itemHeight - 4
height: width
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 2
color: "#101010"
Column {
id: imageColumn
anchors.fill: parent
Image {
id: shapeImage
height: parent.height - mapImage.height
width: parent.width
fillMode: Image.PreserveAspectFit
source: model.shape // see Lisp keyword name
}
Image {
id: mapImage
width: parent.width
height: 0
fillMode: Image.PreserveAspectFit
source: model.map // see Lisp keyword name
}
}
}
MouseArea {
anchors.fill: parent
onClicked: parent.state = "expanded"
}
Item {
id: infoView
anchors.top: image.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
opacity: 0
Rectangle {
anchors.fill: parent
color: "#303060"
border.color: "#101010"
border.width: 1
Flickable {
id: flick
anchors.fill: parent
anchors.margins: 4
contentWidth: edit.paintedWidth
contentHeight: edit.paintedHeight
clip: true
function ensureVisible(r) {
if (contentX >= r.x)
contentX = r.x;
else if (contentX+width <= r.x + r.width)
contentX = r.x + r.width-width;
if (contentY >= r.y)
contentY = r.y;
else if (contentY+height <= r.y + r.height)
contentY = r.y + r.height-height;
}
TextEdit {
id: edit
width: flick.width
color: "#f0f0f0"
font.pixelSize: 16
readOnly: true
focus: true
wrapMode: TextEdit.Wrap
onCursorRectangleChanged: flick.ensureVisible(cursorRectangle)
text: model.info // see Lisp keyword name
}
}
}
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 2
width: itemHeight - 4
height: width
color: "transparent"
border.color: "#f0f0f0"
opacity: 0
Text {
anchors.centerIn: parent
color: "#f0f0f0"
font.bold: true
text: "X"
}
MouseArea {
anchors.fill: parent
onClicked: wrapper.state = ""
}
}
states: [
State {
name: "expanded"
PropertyChanges { target: wrapper; height: view.height }
PropertyChanges { target: image; width: view.width; height: view.height * 2/3; anchors.rightMargin: 0; anchors.topMargin: itemHeight }
PropertyChanges { target: mapImage; height: view.height * 1/3 }
PropertyChanges { target: infoView; opacity: 1 }
PropertyChanges { target: closeButton; opacity: 1 }
PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false }
}
]
transitions: [
Transition {
NumberAnimation {
duration: 250
properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY"
}
}
]
}
}
}

1
examples/Qt6/readme.md Normal file
View file

@ -0,0 +1 @@
This only contains files that differ from the Qt5 build.

2
examples/Qt6/sokoban/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,16 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 37 : 50
height: width
flat: true
focusPolicy: Qt.NoFocus
font.family: fontAwesome.name
font.pixelSize: 1.2 * width
opacity: 0.2
scale: 1.2
onPressed: Lisp.call(this, "qsoko:button-pressed")
}

View file

@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
Button {
width: main.small ? 32 : 50
height: width
font.family: fontAwesome.name
font.pixelSize: width - 6
opacity: 0.8
onPressed: Lisp.call(this, "qsoko:button-pressed")
}

View file

@ -0,0 +1,21 @@
import QtQuick
Item {
objectName: "dynamic"
property Component box: Qt.createComponent("dynamic/Box.qml")
property Component box2: Qt.createComponent("dynamic/Box2.qml")
property Component player: Qt.createComponent("dynamic/Player.qml")
property Component fixed: Qt.createComponent("dynamic/Fixed.qml")
function createItem(name) {
switch (name) {
case "object": return box.createObject()
case "object2": return box2.createObject()
case "player":
case "player2": return player.createObject()
case "wall":
case "goal": return fixed.createObject()
}
}
}

View file

@ -0,0 +1,6 @@
import QtQuick
NumberAnimation {
onRunningChanged: Lisp.call("qsoko:animation-change", running)
}

View file

@ -0,0 +1,6 @@
import QtQuick
RotationAnimation {
onRunningChanged: Lisp.call("qsoko:animation-change", running)
}

View file

@ -0,0 +1,5 @@
import QtQuick
ScaleAnimator {
onRunningChanged: Lisp.call("qsoko:animation-change", running)
}

View file

@ -0,0 +1,6 @@
import QtQuick
SequentialAnimation {
onRunningChanged: Lisp.call("qsoko:animation-change", running)
}

View file

@ -0,0 +1,18 @@
import QtQuick
import "../" as Ext
Image {
Behavior on x {
Ext.NumberAnimation {
duration: 150
easing.type: Easing.InQuart
}
}
Behavior on y {
Ext.NumberAnimation {
duration: 150
easing.type: Easing.InQuart
}
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
import "../" as Ext
Image {
id: box2
Behavior on x {
Ext.NumberAnimation {
duration: 150
easing.type: Easing.InQuart
}
}
Behavior on y {
Ext.NumberAnimation {
duration: 150
easing.type: Easing.InQuart
}
}
// final animation
Ext.SequentialAnimation {
objectName: "wiggle_box"
loops: 3
RotationAnimation {
target: box2
property: "rotation"
from: 0; to: 30
duration: 150
}
RotationAnimation {
target: box2
property: "rotation"
from: 30; to: -30
duration: 300
}
RotationAnimation {
target: box2
property: "rotation"
from: -30; to: 0
duration: 150
}
}
}

View file

@ -0,0 +1,4 @@
import QtQuick
Image {
}

View file

@ -0,0 +1,30 @@
import QtQuick
import "../" as Ext
Image {
id: player
Behavior on x {
Ext.NumberAnimation {
duration: 120
easing.type: Easing.InOutSine
}
}
Behavior on y {
Ext.NumberAnimation {
duration: 120
easing.type: Easing.InOutSine
}
}
// final animation
Ext.RotationAnimation {
objectName: "rotate_player"
target: player
property: "rotation"
from: 0; to: 360
duration: 600
}
}

View file

@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Window
import "ext/" as Ext
Rectangle {
id: main
width: Screen.desktopAvailableWidth
height: Screen.desktopAvailableHeight
color: Qt.darker("lightsteelblue", 1.25)
property bool small: (Math.max(width, height) < 1000)
function isLandscape() { return (Screen.primaryOrientation === Qt.LandscapeOrientation) }
Ext.Dynamic {}
Row {
anchors.centerIn: parent
// adapt 'level' and 'board' scale to screen size
scale: isLandscape()
? ((Screen.desktopAvailableHeight - 10) / board.height)
: ((Screen.desktopAvailableWidth - 10) / (board.width + 2 * level.width))
Slider {
id: level
objectName: "level"
height: board.height
orientation: Qt.Vertical
stepSize: 1.0
onValueChanged: Lisp.call("qsoko:set-maze")
}
Rectangle {
id: board
objectName: "board"
width: 512; height: 512
color: "lightsteelblue"
}
// dummy to have it exactly centered
Item {
width: level.width
height: level.height
}
}
Row {
id: buttons1
objectName: "buttons1"
spacing: main.small ? 10 : 15
padding: 10
anchors.bottom: parent.bottom
Ext.Button {
objectName: "previous"
text: "\uf100"
}
Ext.Button {
objectName: "next"
text: "\uf101"
}
}
Row {
id: buttons2
objectName: "buttons2"
spacing: main.small ? 10 : 15
padding: 10
anchors.right: parent.right
anchors.bottom: parent.bottom
Ext.Button {
objectName: "undo"
text: "\uf112"
}
Ext.Button {
objectName: "restart"
text: "\uf0e2"
}
Ext.Button {
objectName: "solve"
text: "\uf17b"
}
}
// container for arrow buttons
Item {
id: arrows
y: buttons1.y - height - (main.small ? 25 : 50)
width: up.width * 3
height: up.height * 3
anchors.margins: 10
anchors.horizontalCenter: buttons2.horizontalCenter
Ext.ArrowButton {
id: up
objectName: "up"
text: "\uf139"
anchors.horizontalCenter: parent.horizontalCenter
}
Ext.ArrowButton {
objectName: "left"
text: "\uf137"
anchors.verticalCenter: parent.verticalCenter
}
Ext.ArrowButton {
objectName: "right"
text: "\uf138"
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
}
Ext.ArrowButton {
objectName: "down"
text: "\uf13a"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
}
}
// level change animations
Ext.ScaleAnimator {
objectName: "zoom_board_out"
target: board
from: 1.0
to: 0.0
duration: 250
}
Ext.ScaleAnimator {
objectName: "zoom_board_in"
target: board
from: 0.0
to: 1.0
duration: 250
}
// etc
Keys.onPressed: {
if (event.key === Qt.Key_Back) {
event.accepted = true
Lisp.call("qml:qquit")
}
}
FontLoader {
id: fontAwesome
source: "fonts/fontawesome-webfont.ttf"
}
}