RESTful API client¶
Example of how to create a RESTful API QML client.
This example shows how to create a basic QML RESTful API client with an imaginary color palette service. The application uses RESTful communication with a local server to request and send data. The REST service is provided as a QML element whose child elements wrap the individual JSON data APIs provided by the server.
Application functionality¶
The example provides the following basic functionalities:
List users and colors
Login and logout users
Modify and create new colors
Running a server¶
The client talks to a local REST server on http://127.0.0.1:49425. Start
one of the following before launching the client:
A FastAPI-based REST API server bundled under
server/— the easiest option to get started.A Qt-based REST API server C++ example from the QtHttpServer Module, which listens on the same port by default — for developers who want to test against the C++ backend.
Both servers expose the same REST API on the same port, so the client connects to whichever one is running. To run the bundled FastAPI server:
cd server
pip install -r requirements.txt
./start_server.sh
or on Windows:
.\start_server.bat
The FastAPI server is stateful: color additions, edits, and deletions persist for the lifetime of the server process.
The client connects on startup. If no server is reachable, it shows a dialog reporting the failed connection; start a server and relaunch the client.
The users and colors are paginated resources on the server-side. This means that the server provides the data in chunks called pages. The UI listing reflects this pagination and views the data on pages.
Logging in happens via the login function provided by the login popup. Under the hood the login sends a HTTP POST request. Upon receiving a successful response the authorization token is extracted from the response, which in turn is then used in subsequent HTTP requests which require the token.
Editing and adding new colors is done in a popup. Note that uploading the color changes to the server requires that a user has logged in.
REST implementation¶
The example illustrates one way to compose a REST service from individual resource elements. In this example the resources are the paginated user and color resources plus the login service. The resource elements are bound together by the base URL (server URL) and the shared network access manager.
The basis of the REST service is the RestService QML element whose child items compose the actual service.
Upon instantiation the RestService element loops its children elements and sets them up to use the same network access manager. This way the individual resources share the same access details such as the server URL and authorization token.
The actual communication is done with a rest access manager which implements
some convenience functionality to deal specifically with HTTP REST APIs and
effectively deals with sending and receiving the
QNetworkRequest and
QNetworkReply as needed.
Viewing the data on UI is done with standard QML views populated by
JSON data received from the server via the data property of the class
PaginatedResource. For C++ compatibility, it is declared to be of type
QList<QJsonObject>. It can be passed a list of dicts as obtained from
parsing using QJsonDocument.
from __future__ import annotations
from PySide6.QtCore import QObject
from PySide6.QtQml import QmlAnonymous
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlAnonymous
class AbstractResource(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.m_manager = None # QRestAccessManager
self.m_api = None # QNetworkRequestFactory
def setAccessManager(self, manager):
self.m_manager = manager
def setServiceApi(self, serviceApi):
self.m_api = serviceApi
from __future__ import annotations
import sys
from functools import partial
from dataclasses import dataclass
from PySide6.QtCore import Property, Signal, Slot
from PySide6.QtNetwork import QHttpHeaders
from PySide6.QtQml import QmlElement
from abstractresource import AbstractResource
tokenField = "token"
emailField = "email"
idField = "id"
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
class BasicLogin(AbstractResource):
@dataclass
class User:
email: str
token: bytes
id: int
userChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.m_user = None
self.m_loginPath = ""
self.m_logoutPath = ""
self.m_user = None
@Property(str, notify=userChanged)
def user(self):
return self.m_user.email if self.m_user else ""
@Property(bool, notify=userChanged)
def loggedIn(self):
return bool(self.m_user)
@Property(str)
def loginPath(self):
return self.m_loginPath
@loginPath.setter
def loginPath(self, p):
self.m_loginPath = p
@Property(str)
def logoutPath(self):
return self.m_logoutPath
@logoutPath.setter
def logoutPath(self, p):
self.m_logoutPath = p
@Slot("QVariantMap")
def login(self, data):
request = self.m_api.createRequest(self.m_loginPath)
self.m_manager.post(request, data, self, partial(self.loginReply, data))
def loginReply(self, data, reply):
self.m_user = None
if not reply.isSuccess():
print("login: ", reply.errorString(), file=sys.stderr)
(json, error) = reply.readJson()
if json and json.isObject():
json_object = json.object()
if token := json_object.get(tokenField):
email = data[emailField]
token = json_object[tokenField]
id = data[idField]
self.m_user = BasicLogin.User(email, token, id)
headers = QHttpHeaders()
headers.append("token", self.m_user.token if self.m_user else "")
self.m_api.setCommonHeaders(headers)
self.userChanged.emit()
@Slot()
def logout(self):
request = self.m_api.createRequest(self.m_logoutPath)
self.m_manager.post(request, b"", self, self.logoutReply)
def logoutReply(self, reply):
if reply.isSuccess():
self.m_user = None
self.m_api.clearCommonHeaders() # clears 'token' header
self.userChanged.emit()
else:
print("logout: ", reply.errorString(), file=sys.stderr)
from __future__ import annotations
"""PySide6 port of the Qt RESTful API client demo from Qt v6.x"""
import os
import sys
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from pathlib import Path
from PySide6.QtCore import QUrl
from PySide6.QtGui import QIcon, QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle
from basiclogin import BasicLogin # noqa: F401
from paginatedresource import PaginatedResource # noqa: F401
from restservice import RestService # noqa: F401
import rc_colorpaletteclient # noqa: F401
if __name__ == "__main__":
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter)
parser.add_argument("--url", type=str, help="The URL to open")
args = parser.parse_args()
url = args.url if args.url else "http://127.0.0.1:49425/api"
app = QGuiApplication(sys.argv)
QIcon.setThemeName("colorpaletteclient")
QQuickStyle.setStyle("Fusion")
engine = QQmlApplicationEngine()
engine.setInitialProperties({"serverUrl": QUrl.fromUserInput(url)})
app_dir = Path(__file__).parent
app_dir_url = QUrl.fromLocalFile(os.fspath(app_dir))
engine.addImportPath(os.fspath(app_dir))
engine.loadFromModule("ColorPalette", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = app.exec()
del engine
sys.exit(exit_code)
from __future__ import annotations
import sys
from PySide6.QtCore import (QUrlQuery, Property, Signal, Slot)
from PySide6.QtNetwork import QNetworkReply
from PySide6.QtQml import QmlElement
from abstractresource import AbstractResource
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
totalPagesField = "total_pages"
currentPageField = "page"
@QmlElement
class PaginatedResource(AbstractResource):
"""This class manages a simple paginated Crud resource,
where the resource is a paginated list of JSON items."""
dataUpdated = Signal()
pageUpdated = Signal()
pagesUpdated = Signal()
errorOccurred = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
# The total number of pages as reported by the server responses
self.m_pages = 0
# The default page we request if the user hasn't set otherwise
self.m_currentPage = 1
self.m_path = ""
self._data = []
@Property(str)
def path(self):
return self.m_path
@path.setter
def path(self, p):
self.m_path = p
@Property(int, notify=pagesUpdated)
def pages(self):
return self.m_pages
@Property(int, notify=pageUpdated)
def page(self):
return self.m_currentPage
@page.setter
def page(self, page):
if self.m_currentPage == page or page < 1:
return
self.m_currentPage = page
self.pageUpdated.emit()
self.refreshCurrentPage()
@Slot()
def refreshCurrentPage(self):
query = QUrlQuery()
query.addQueryItem("page", str(self.m_currentPage))
request = self.m_api.createRequest(self.m_path, query)
self.m_manager.get(request, self, self.refreshCurrentPageReply)
def refreshCurrentPageReply(self, reply):
error = ""
if reply.isSuccess():
(json, jsonError) = reply.readJson()
if json:
self.refreshRequestFinished(json)
else:
error = jsonError.errorString()
else:
error = (reply.errorString() if reply.error() != QNetworkReply.NetworkError.NoError
else f"HTTP status: {reply.httpStatus()}")
if error:
url = reply.networkReply().url().toString()
print(f'PaginatedResource: request "{url}" failed: "{error}"', file=sys.stderr)
self.errorOccurred.emit(error)
self.refreshRequestFailed()
def refreshRequestFinished(self, json):
json_object = json.object()
data = json_object.get("data")
totalPages = json_object.get(totalPagesField)
currentPage = json_object.get(currentPageField)
self._data = data if data else []
self.m_pages = int(totalPages) if totalPages else 1
self.m_currentPage = int(currentPage) if currentPage else 1
self.pageUpdated.emit()
self.pagesUpdated.emit()
self.dataUpdated.emit()
def refreshRequestFailed(self):
if self.m_currentPage != 1:
# A failed refresh. If we weren't on page 1, try that.
# Last resource on currentPage might have been deleted, causing a failure
self.setPage(1)
else:
# Refresh failed and we we're already on page 1 => clear data
self.m_pages = 0
self.pagesUpdated.emit()
self._data = []
self.dataUpdated.emit()
@Slot("QVariantMap", int)
def update(self, data, id):
request = self.m_api.createRequest(f"{self.m_path}/{id}")
self.m_manager.put(request, data, self, self.updateReply)
def updateReply(self, reply):
if reply.isSuccess():
self.refreshCurrentPage()
@Slot("QVariantMap")
def add(self, data):
request = self.m_api.createRequest(self.m_path)
self.m_manager.post(request, data, self, self.updateReply)
@Slot(int)
def remove(self, id):
request = self.m_api.createRequest(f"{self.m_path}/{id}")
self.m_manager.deleteResource(request, self, self.updateReply)
@Property("QList<QJsonObject>", notify=dataUpdated, final=True)
def data(self):
return self._data
from __future__ import annotations
from PySide6.QtCore import Property, Signal, ClassInfo
from PySide6.QtNetwork import (QNetworkAccessManager, QRestAccessManager,
QNetworkRequestFactory, QSslSocket)
from PySide6.QtQml import QmlElement, QPyQmlParserStatus, ListProperty
from abstractresource import AbstractResource
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
@ClassInfo(DefaultProperty="resources")
class RestService(QPyQmlParserStatus):
urlChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.m_resources = []
self.m_qnam = QNetworkAccessManager()
self.m_qnam.setAutoDeleteReplies(True)
self.m_manager = QRestAccessManager(self.m_qnam)
self.m_serviceApi = QNetworkRequestFactory()
@Property(str, notify=urlChanged)
def url(self):
return self.m_serviceApi.baseUrl()
@url.setter
def url(self, url):
if self.m_serviceApi.baseUrl() != url:
self.m_serviceApi.setBaseUrl(url)
self.urlChanged.emit()
@Property(bool, constant=True)
def sslSupported(self):
return QSslSocket.supportsSsl()
def classBegin(self):
pass
def componentComplete(self):
for resource in self.m_resources:
resource.setAccessManager(self.m_manager)
resource.setServiceApi(self.m_serviceApi)
def appendResource(self, r):
self.m_resources.append(r)
resources = ListProperty(AbstractResource, appendResource)
<RCC>
<qresource prefix="/qt/qml/ColorPalette">
<file>icons/close.svg</file>
<file>icons/close_dark.svg</file>
<file>icons/delete.svg</file>
<file>icons/delete_dark.svg</file>
<file>icons/dots.svg</file>
<file>icons/edit.svg</file>
<file>icons/edit_dark.svg</file>
<file>icons/login.svg</file>
<file>icons/login_dark.svg</file>
<file>icons/logout.svg</file>
<file>icons/logout_dark.svg</file>
<file>icons/ok.svg</file>
<file>icons/ok_dark.svg</file>
<file>icons/plus.svg</file>
<file>icons/plus_dark.svg</file>
<file>icons/qt.png</file>
<file>icons/update.svg</file>
<file>icons/update_dark.svg</file>
<file>icons/user.svg</file>
<file>icons/userMask.svg</file>
<file>icons/user_dark.svg</file>
</qresource>
</RCC>
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Popup {
id: colorDeleter
padding: 10
modal: true
focus: true
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
signal deleteClicked(int cid)
property int colorId: -1
property string colorName: ""
function maybeDelete(data) {
colorName = data.name
colorId = data.id
open()
}
ColumnLayout {
anchors.fill: parent
spacing: 10
Label {
text: qsTr("Delete Color?")
font.bold: true
}
Label {
text: qsTr("Are you sure, you want to delete color") + " \"" + colorDeleter.colorName + "\"?"
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
Layout.fillWidth: true
text: qsTr("Cancel")
onClicked: colorDeleter.close()
}
Button {
Layout.fillWidth: true
text: qsTr("Delete")
onClicked: {
colorDeleter.deleteClicked(colorDeleter.colorId)
colorDeleter.close()
}
}
}
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
Popup {
id: colorEditor
// Popup for adding or updating a color
padding: 10
modal: true
focus: true
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
signal colorAdded(string name, string color, string pantone_value)
signal colorUpdated(string name, string color, string pantone_value, int cid)
property bool newColor: true
property int colorId: -1
property color currentColor: "white"
function createNewColor() {
newColor = true
colorNameField.text = "cute green"
currentColor = Qt.color("#41cd52")
colorPantoneField.text = "PMS 802C"
colorDialog.selectedColor = currentColor
open()
}
function updateColor(data) {
newColor = false
colorNameField.text = data.name
currentColor = Qt.color(data.color)
colorPantoneField.text = data.pantone_value
colorId = data.id
colorDialog.selectedColor = currentColor
open()
}
ColorDialog {
id: colorDialog
title: qsTr("Choose a color")
onAccepted: {
colorEditor.currentColor = Qt.color(colorDialog.selectedColor)
colorDialog.close()
}
onRejected: {
colorDialog.close()
}
}
ColumnLayout {
anchors.fill: parent
spacing: 10
GridLayout {
columns: 2
rowSpacing: 10
columnSpacing: 10
Label {
text: qsTr("Color Name")
}
TextField {
id: colorNameField
padding: 10
}
Label {
text: qsTr("Pantone Value")
}
TextField {
id: colorPantoneField
padding: 10
}
Label {
text: qsTr("Rgb Value")
}
TextField {
id: colorRGBField
text: colorEditor.currentColor.toString()
readOnly: true
padding: 10
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Rectangle {
Layout.preferredWidth: 30
Layout.preferredHeight: 30
Layout.minimumWidth: 30
Layout.minimumHeight: 30
radius: 4
border.width: 1
border.color: palette.mid
color: colorEditor.currentColor
}
Button {
Layout.fillWidth: true
text: qsTr("Change Color")
onClicked: colorDialog.open()
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
text: qsTr("Cancel")
onClicked: colorEditor.close()
Layout.fillWidth: true
}
Button {
Layout.fillWidth: true
text: colorEditor.newColor ? qsTr("Add") : qsTr("Update")
onClicked: {
if (colorEditor.newColor) {
colorEditor.colorAdded(colorNameField.text,
colorRGBField.text,
colorPantoneField.text)
} else {
colorEditor.colorUpdated(colorNameField.text,
colorRGBField.text,
colorPantoneField.text,
colorEditor.colorId)
}
colorEditor.close()
}
}
}
}
}
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes
Rectangle {
id: root
required property BasicLogin loginService
required property PaginatedResource colors
required property PaginatedResource colorViewUsers
color: root.palette.window
function iconPath(baseImagePath) {
return Application.styleHints.colorScheme === Qt.ColorScheme.Dark
? `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}_dark.svg`
: `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg`
}
ColorDialogEditor {
id: colorPopup
onColorAdded: (colorNameField, colorRGBField, colorPantoneField) => {
root.colors.add({"name" : colorNameField,
"color" : colorRGBField,
"pantone_value" : colorPantoneField})
}
onColorUpdated: (colorNameField, colorRGBField, colorPantoneField, cid) => {
root.colors.update({"name" : colorNameField,
"color" : colorRGBField,
"pantone_value" : colorPantoneField},
cid)
}
}
ColorDialogDelete {
id: colorDeletePopup
onDeleteClicked: (cid) => {
root.colors.remove(cid)
}
}
ColumnLayout {
// The main application layout
anchors.fill :parent
spacing: 0
ToolBar {
Layout.fillWidth: true
Layout.minimumHeight: 35
UserMenu {
id: userMenu
userMenuUsers: root.colorViewUsers
userLoginService: root.loginService
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
Button {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.minimumWidth: 25
Layout.minimumHeight: 25
Layout.alignment: Qt.AlignVCenter
display: AbstractButton.IconOnly
icon.source: root.iconPath("plus")
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Add a new color")
enabled: root.loginService.loggedIn
onClicked: colorPopup.createNewColor()
}
Button {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.minimumWidth: 25
Layout.minimumHeight: 25
Layout.alignment: Qt.AlignVCenter
display: AbstractButton.IconOnly
icon.source: root.iconPath("update")
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Refresh colors and users")
onClicked: {
root.colors.refreshCurrentPage()
root.colorViewUsers.refreshCurrentPage()
}
}
Item { Layout.fillWidth: true }
Image {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.minimumWidth: 25
Layout.minimumHeight: 25
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
source: "qrc:/qt/qml/ColorPalette/icons/qt.png"
fillMode: Image.PreserveAspectFit
}
Label {
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
text: qsTr("Color Palette")
font.bold: true
}
Item { Layout.fillWidth: true }
AbstractButton {
id: loginButton
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.minimumWidth: 25
Layout.minimumHeight: 25
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: root.loginService.loggedIn
? qsTr("Logged in as %1").arg(root.loginService.user)
: qsTr("Log in")
Item {
id: userImageCliped
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 25
height: 25
Image {
id: userImage
anchors.fill: parent
source: getCurrentUserImage()
visible: false
function getCurrentUserImage() {
if (!root.loginService.loggedIn)
return root.iconPath("user");
let users = root.colorViewUsers
for (let i = 0; i < users.data.length; i++) {
if (users.data[i].email === root.loginService.user)
return users.data[i].avatar;
}
}
}
Image {
id: userMask
source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
anchors.fill: userImage
anchors.margins: 4
visible: false
}
MultiEffect {
source: userImage
anchors.fill: userImage
maskSource: userMask
maskEnabled: true
}
}
onClicked: {
userMenu.open()
var pos = mapToGlobal(Qt.point(x, y))
pos = userMenu.parent.mapFromGlobal(pos)
userMenu.x = x - userMenu.width + 50
userMenu.y = y + 15
}
Shape {
id: bubble
x: -text.width - 25
y: -3
anchors.margins: 3
preferredRendererType: Shape.CurveRenderer
visible: !root.loginService.loggedIn
ShapePath {
strokeWidth: 0
fillColor: root.palette.highlight
strokeColor: Qt.darker(root.palette.highlight, 1.3)
startX: 5; startY: 0
PathLine { x: 5 + text.width + 6; y: 0 }
PathArc { x: 10 + text.width + 6; y: 5; radiusX: 5; radiusY: 5}
// arrow
PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 - 6 }
PathLine { x: 10 + text.width + 6 + 6; y: 8 + text.height / 2 }
PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 + 6}
PathLine { x: 10 + text.width + 6; y: 5 + text.height + 6 }
// end arrow
PathArc { x: 5 + text.width + 6; y: 10 + text.height + 6 ; radiusX: 5; radiusY: 5}
PathLine { x: 5; y: 10 + text.height + 6 }
PathArc { x: 0; y: 5 + text.height + 6 ; radiusX: 5; radiusY: 5}
PathLine { x: 0; y: 5 }
PathArc { x: 5; y: 0 ; radiusX: 5; radiusY: 5}
}
Text {
x: 8
y: 8
id: text
color: root.palette.highlightedText
text: qsTr("Log in to edit")
font.bold: true
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
}
}
}
//! [View and model]
ListView {
id: colorListView
model: root.colors.data
//! [View and model]
footerPositioning: ListView.OverlayFooter
spacing: 15
clip: true
Layout.fillHeight: true
Layout.fillWidth: true
header: Rectangle {
height: 32
width: parent.width
color: palette.window
RowLayout {
anchors.fill: parent
component HeaderText : Label {
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
}
HeaderText {
id: headerName
text: qsTr("Color Name")
Layout.fillWidth: true
Layout.horizontalStretchFactor: 30
}
HeaderText {
id: headerRgb
text: qsTr("Rgb Value")
Layout.fillWidth: true
Layout.horizontalStretchFactor: 25
}
HeaderText {
id: headerPantone
text: qsTr("Pantone Value")
Layout.fillWidth: true
Layout.horizontalStretchFactor: 25
}
HeaderText {
id: headerAction
text: qsTr("Action")
Layout.fillWidth: true
Layout.horizontalStretchFactor: 20
}
}
}
delegate: Item {
id: colorInfo
required property var modelData
width: colorListView.width
height: (colorListView.height - 55) / 6 - colorListView.spacing
// Header: 35, Footer 20, 55 together
RowLayout {
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
Rectangle {
id: colorSample
Layout.alignment: Qt.AlignVCenter
Layout.minimumWidth: 36
Layout.minimumHeight: 36
implicitWidth: 36
implicitHeight: 36
radius: 6
color: colorInfo.modelData.color
}
Label {
Layout.preferredWidth: colorInfo.width * 0.3 - colorSample.width
horizontalAlignment: Qt.AlignLeft
leftPadding: 5
text: colorInfo.modelData.name
}
Label {
Layout.preferredWidth: colorInfo.width * 0.25
horizontalAlignment: Qt.AlignHCenter
text: colorInfo.modelData.color
}
Label {
Layout.preferredWidth: colorInfo.width * 0.25
horizontalAlignment: Qt.AlignHCenter
text: colorInfo.modelData.pantone_value
}
Item {
Layout.maximumHeight: 28
Layout.minimumWidth: buttonBox.implicitWidth
Layout.minimumHeight: buttonBox.implicitHeight
implicitHeight: buttonBox.implicitHeight
implicitWidth: buttonBox.implicitWidth
RowLayout {
id: buttonBox
anchors.fill: parent
ToolButton {
icon.source: root.iconPath("delete")
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Delete color")
enabled: root.loginService.loggedIn
onClicked: colorDeletePopup.maybeDelete(colorInfo.modelData)
}
ToolButton {
icon.source: root.iconPath("edit")
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Edit color")
enabled: root.loginService.loggedIn
onClicked: colorPopup.updateColor(colorInfo.modelData)
}
}
}
}
}
footer: ToolBar {
// Paginate buttons if more than one page
visible: root.colors.pages > 1
implicitWidth: parent.width
RowLayout {
anchors.fill: parent
Item { Layout.fillWidth: true /* spacer */ }
Repeater {
model: root.colors.pages
ToolButton {
text: page
font.bold: root.colors.page === page
required property int index
readonly property int page: (index + 1)
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Go to page %1").arg(page)
onClicked: root.colors.page = page
}
}
}
}
}
}
}
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Dialogs
Window {
id: window
width: 500
height: 400
minimumWidth: 500
minimumHeight: 400
visible: true
title: qsTr("Color Palette Client")
enum DataView {
UserView = 0,
ColorView = 1
}
// The Qt colorpalette REST API server listens here, so the client
// just connects on startup.
required property var serverUrl
ColorView {
id: colorview
anchors.fill: parent
loginService: colorLogin
colors: colorPalette
colorViewUsers: users
}
MessageDialog {
id: connectionErrorDialog
title: qsTr("Connection failed")
text: qsTr("Could not reach the server at %1.").arg(window.serverUrl)
informativeText: qsTr("Start the Qt colorpalette REST API server "
+ "on port 49425, then retry.")
buttons: MessageDialog.Cancel | MessageDialog.Retry
onButtonClicked: function(button) {
if (button === MessageDialog.Retry) {
colorPalette.refreshCurrentPage()
users.refreshCurrentPage()
} else if (button === MessageDialog.Cancel) {
Qt.quit()
}
}
}
Connections {
target: colorPalette
function onErrorOccurred(message) { connectionErrorDialog.open() }
}
//! [RestService QML element]
RestService {
id: paletteService
url: window.serverUrl
PaginatedResource {
id: users
path: "users"
}
PaginatedResource {
id: colorPalette
path: "unknown"
}
BasicLogin {
id: colorLogin
loginPath: "login"
logoutPath: "logout"
}
}
//! [RestService QML element]
Component.onCompleted: {
colorPalette.refreshCurrentPage()
users.refreshCurrentPage()
}
}
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
Popup {
id: userMenu
required property BasicLogin userLoginService
required property PaginatedResource userMenuUsers
width: 280
height: 270
background: Item {}
function iconPath(baseImagePath) {
return Application.styleHints.colorScheme === Qt.ColorScheme.Dark
? `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}_dark.svg`
: `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg`
}
Rectangle {
radius: 8
border.width: 0
color: palette.window
anchors.fill: parent
ListView {
id: userListView
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 5
anchors.topMargin: 5
anchors.bottomMargin: 2
model: userMenu.userMenuUsers.data
spacing: 7
footerPositioning: ListView.PullBackFooter
clip: true
Layout.fillHeight: true
Layout.fillWidth: true
delegate: Item {
id: userInfo
height: 30
width: userListView.width
required property var modelData
readonly property bool logged: (modelData.email === userMenu.userLoginService.user)
Item {
id: userImageCliped
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 30
height: 30
Image {
id: userImage
anchors.fill: parent
source: userInfo.modelData.avatar
visible: false
}
Image {
id: userMask
source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
anchors.fill: userImage
anchors.margins: 4
visible: false
}
MultiEffect {
source: userImage
anchors.fill: userImage
maskSource: userMask
maskEnabled: true
}
}
Label {
id: userMailLabel
anchors.left: userImageCliped.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 5
text: userInfo.modelData.email
font.bold: userInfo.logged
}
ToolButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 5
icon.source: userMenu.iconPath(userInfo.logged
? "logout" : "login")
enabled: userInfo.logged || !userMenu.userLoginService.loggedIn
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: userInfo.logged
? qsTr("Log out")
: qsTr("Log in as %1").arg(userInfo.modelData.email)
onClicked: {
if (userInfo.logged) {
userMenu.userLoginService.logout()
} else {
//! [Login]
userMenu.userLoginService.login({"email" : userInfo.modelData.email,
"password" : "apassword",
"id" : userInfo.modelData.id})
//! [Login]
userMenu.close()
}
}
}
}
footer: ToolBar {
// Paginate buttons if more than one page
visible: userMenu.userMenuUsers.pages > 1
implicitWidth: parent.width
RowLayout {
anchors.fill: parent
Item { Layout.fillWidth: true /* spacer */ }
Repeater {
model: userMenu.userMenuUsers.pages
ToolButton {
text: page
font.bold: userMenu.userMenuUsers.page === page
required property int index
readonly property int page: (index + 1)
ToolTip.visible: hovered
ToolTip.delay: 500
ToolTip.text: qsTr("Go to page %1").arg(page)
onClicked: userMenu.userMenuUsers.page = page
}
}
}
}
}
}
Rectangle {
radius: 8
border.color: palette.mid
border.width: 2
color: "transparent"
anchors.fill: parent
}
}
module ColorPalette
Main 1.0 Main.qml
ColorDialogDelete 1.0 ColorDialogDelete.qml
ColorDialogEditor 1.0 ColorDialogEditor.qml
ColorView 1.0 ColorView.qml
UserMenu 1.0 UserMenu.qml
<RCC>
<qresource prefix="/qt/qml/ColorPalette">
<file>icons/close.svg</file>
<file>icons/close_dark.svg</file>
<file>icons/delete.svg</file>
<file>icons/delete_dark.svg</file>
<file>icons/dots.svg</file>
<file>icons/edit.svg</file>
<file>icons/edit_dark.svg</file>
<file>icons/login.svg</file>
<file>icons/login_dark.svg</file>
<file>icons/logout.svg</file>
<file>icons/logout_dark.svg</file>
<file>icons/ok.svg</file>
<file>icons/ok_dark.svg</file>
<file>icons/plus.svg</file>
<file>icons/plus_dark.svg</file>
<file>icons/qt.png</file>
<file>icons/update.svg</file>
<file>icons/update_dark.svg</file>
<file>icons/user.svg</file>
<file>icons/userMask.svg</file>
<file>icons/user_dark.svg</file>
</qresource>
</RCC>