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