From 07705b012a0078ab6cac8546df7165e1cf16fb63 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Mon, 16 Dec 2024 12:59:19 +0100 Subject: [PATCH] Add a very ugly login page to test database connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I want to perform all SQL queries in a thread, to avoid freezing the UI, that sometimes might happen when there is a lot of data to fetch; should not happen very often, though. Neither libpq nor Qt SQL allow queries on the same connection from differents threads, and, in Qt SQL, all queries must be performed from the same thread where the connection was established. In Qt5 i had to either create a connection per thread, or use a QThread-derived object to hold the connection and use signals and slots to pass query and response data between the UI and database threads; it was usable but not pretty. With Qt6 and Concurrent’s QThreadPool now i can use QFutures instead, that are not as cumbersome as with Qt5, because i no longer need QFutureWatcher. I still have the problem that all queries must be done from within the same thread, and QThreadPool uses an arbitrary thread. The solution is to create a “pool” with a single, non-expirable thread, and call all Concurrent::run onto that pool. I have to test it properly, and first need to open the database to test whether that, at least, works. I added a simple “login page” for that, and to make a first attempt to error messages; i use a control that is like Kirigami’s InlineMessage for now, but i am not sure. I also do not know how i will configure database’s connection details. I usually make use of pg_service.conf, because then the application only need to know its service name, but i am not sure whether other people would find it as comfortable as i do. --- CMakeLists.txt | 2 + src/CMakeLists.txt | 11 +++- src/ErrorNotification.qml | 117 ++++++++++++++++++++++++++++++++++++++ src/Main.qml | 71 +++++++++++++++++++++++ src/SelectableLabel.qml | 21 +++++++ src/database.cpp | 27 +++++++++ src/database.h | 27 +++++++++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 src/ErrorNotification.qml create mode 100644 src/SelectableLabel.qml create mode 100644 src/database.cpp create mode 100644 src/database.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fecbfc5..cb25d97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,10 @@ project(camper VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS + Concurrent Quick QuickControls2 + Sql ) qt_standard_project_setup(REQUIRES 6.5) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1522f1b..b7fb64e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,8 +6,12 @@ qt_add_qml_module(${PROJECT_NAME} URI Camper VERSION 1.0 DEPENDENCIES QtCore + SOURCES + database.cpp database.h QML_FILES + ErrorNotification.qml Main.qml + SelectableLabel.qml ) set_target_properties(${PROJECT_NAME} PROPERTIES @@ -19,8 +23,11 @@ set_target_properties(${PROJECT_NAME} PROPERTIES ) target_link_libraries(${PROJECT_NAME} - PRIVATE Qt6::Quick - PRIVATE Qt6::QuickControls2 + PRIVATE + Qt6::Concurrent + Qt6::Quick + Qt6::QuickControls2 + Qt6::Sql ) include(GNUInstallDirs) diff --git a/src/ErrorNotification.qml b/src/ErrorNotification.qml new file mode 100644 index 0000000..7e5d3d7 --- /dev/null +++ b/src/ErrorNotification.qml @@ -0,0 +1,117 @@ +import QtQuick +import QtQuick.Controls + +Control { + id: control + + property alias text: label.text + + function show(errorMessage: string) { + control.text = errorMessage; + control.visible = true; + hideTimer.start(); + } + + Accessible.ignored: !visible + Accessible.role: Accessible.AlertMessage + implicitHeight: visible ? (contentLayout.implicitHeight + topPadding + bottomPadding) : 0 + opacity: visible ? 1 : 0 + padding: 4 + visible: false + + background: Rectangle { + id: borderRect + + border.color: "#da4453" + color: "#ebced2" + radius: 5 + } + contentItem: Item { + id: contentLayout + + Accessible.ignored: true + implicitHeight: Math.max(label.implicitHeight, closeButton.implicitHeight) + + Behavior on opacity { + enabled: control.visible + + NumberAnimation { + duration: 200 + } + } + + SelectableLabel { + id: label + + Accessible.ignored: !control.visible + + anchors { + left: parent.left + leftMargin: 4 + right: closeButton.left + rightMargin: 4 + top: parent.top + } + } + + ToolButton { + id: closeButton + + Accessible.ignored: !control.visible + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + display: ToolButton.IconOnly + height: implicitHeight + icon.name: "dialog-close" + text: qsTr("Close") + + onClicked: function () { + control.visible = false; + } + } + } + Behavior on implicitHeight { + enabled: !control.visible + + NumberAnimation { + duration: 200 + } + } + Behavior on opacity { + enabled: !control.visible + + NumberAnimation { + duration: 200 + } + } + + onImplicitHeightChanged: function () { + height = implicitHeight; + } + onOpacityChanged: function () { + if (opacity === 0) { + contentLayout.opacity = 0; + } else if (opacity === 1) { + contentLayout.opacity = 1; + } + } + + anchors { + bottom: parent.bottom + bottomMargin: 8 + left: parent.left + margins: 18 * 4 + right: parent.right + } + + Timer { + id: hideTimer + + interval: 10000 + repeat: false + + onTriggered: function () { + control.visible = false; + } + } +} diff --git a/src/Main.qml b/src/Main.qml index 53feddc..32993a0 100644 --- a/src/Main.qml +++ b/src/Main.qml @@ -1,9 +1,80 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts +import Camper ApplicationWindow { height: 480 title: qsTr("Camper") visible: true width: 640 + + ColumnLayout { + Label { + text: qsTr("&User:") + } + + TextField { + id: user + + focus: true + + validator: RegularExpressionValidator { + regularExpression: /[^s].*/ + } + + onAccepted: function () { + loginAction.trigger(); + } + } + + Label { + text: qsTr("&Password:") + } + + TextField { + id: password + + echoMode: TextInput.Password + + onAccepted: function () { + loginAction.trigger(); + } + } + + Button { + action: loginAction + } + } + + ErrorNotification { + id: errorNotification + + anchors { + bottom: parent.bottom + bottomMargin: 8 + left: parent.left + margins: 18 * 4 + right: parent.right + } + } + + Action { + id: loginAction + + enabled: user.acceptableInput + text: "&Login" + + onTriggered: function () { + Database.open(user.text, password.text); + } + } + + Connections { + function onErrorOcurred(errorMessage) { + errorNotification.show(errorMessage); + } + + target: Database + } } diff --git a/src/SelectableLabel.qml b/src/SelectableLabel.qml new file mode 100644 index 0000000..fc24841 --- /dev/null +++ b/src/SelectableLabel.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Controls + +Control { + id: control + + property alias text: textArea.text + + contentItem: TextArea { + id: textArea + + padding: 0 + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + + HoverHandler { + cursorShape: Qt.IBeamCursor + } + } +} diff --git a/src/database.cpp b/src/database.cpp new file mode 100644 index 0000000..aa2b4fd --- /dev/null +++ b/src/database.cpp @@ -0,0 +1,27 @@ +#include "database.h" +#include +#include +#include + +Database::Database(QObject *parent) + : QObject{parent} + , m_pool{} +{ + m_pool.setMaxThreadCount(1); + m_pool.setExpiryTimeout(-1); +} + +QFuture Database::open(const QString &user, const QString &password) +{ + return QtConcurrent::run(&m_pool, [this, user, password]() { + QString connectionName("main"); + QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL", connectionName); + db.setConnectOptions("service=camper; options=-csearch_path=camper,public"); + if (!db.open(user, password)) { + const QString errorMessage(db.lastError().text()); + db = QSqlDatabase(); // Otherwise removeDatabase complains is still being used. + QSqlDatabase::removeDatabase(connectionName); + emit errorOcurred(errorMessage); + } + }); +} diff --git a/src/database.h b/src/database.h new file mode 100644 index 0000000..1c6b5b5 --- /dev/null +++ b/src/database.h @@ -0,0 +1,27 @@ +#ifndef DATABASE_H +#define DATABASE_H + +#include +#include +#include +#include + +class Database : public QObject +{ + Q_OBJECT + QML_SINGLETON + QML_ELEMENT + +public: + explicit Database(QObject *parent = nullptr); + + Q_INVOKABLE QFuture open(const QString &user, const QString &password); + +signals: + void errorOcurred(const QString &errorMessage); + +private: + QThreadPool m_pool; +}; + +#endif // DATABASE_H