Compare commits

...

17 Commits

Author SHA1 Message Date
28f9de31d5 Clean up and formatting. 2026-04-08 17:23:50 +02:00
61ae4fc02c Disabling unnecessary function when undo stack's cleanness changed. 2026-03-12 09:30:07 +01:00
f5efa975c6 When trying to close the application changes can be saved, discarded or the exit can be canceled via dialog. 2026-03-09 15:48:56 +01:00
3e3aae63ae Added visualization for unclean undo stack. (No safe guard before closing on an unclean stack yet.) 2026-03-09 15:04:36 +01:00
d08325cc3c Model undo stack is accessible in QML. Added Undo & Redo buttons, which are properly en-/disabled and have informative texts. 2026-03-09 12:26:41 +01:00
e76fdaae9f Added controls to edit the role "amount", "factor" and "type" from the expandable item delegate. 2026-03-09 10:35:39 +01:00
609f777d13 Added an editable item delegate. Only Factor plus/minus button are working for now. (based on https://doc.qt.io/qt-6/qtquick-views-example.html as well) 2026-03-09 07:51:16 +01:00
ab5f9b1952 Displaying images in item delegates. 2026-03-07 12:07:17 +01:00
dc8a4683db Using a expandable list item as delegate. (adapted Qt example: https://doc.qt.io/qt-6/qtquick-views-example.html)
Icons not working yet!
2026-03-07 11:02:44 +01:00
5b9f796a6f Using a color scheme based on the elixir mentor gist clone tutorial. https://github.com/ElixirMentor/elixir_gist 2026-03-06 10:41:47 +01:00
ca59fc2b20 Selecting items by mouse click. 2026-03-05 12:43:51 +01:00
b2e3b49814 Added header and footer with gradients to the list view. 2026-03-05 12:43:25 +01:00
f2d4461bbc Moving the list item delegate into its own QML file. 2026-03-05 12:34:02 +01:00
f1ad289411 Using a ListView with a simple delegate component. 2026-03-05 10:53:52 +01:00
7b6979288a Tested a QML TableModel to view the model data. 2026-03-04 19:56:12 +01:00
2ff97c588c Accessing the core and the model from this QML UI project. 2026-03-03 18:58:34 +01:00
ed54d5e54e Initial basic QML app. 2026-03-03 09:33:03 +01:00
16 changed files with 772 additions and 0 deletions

56
CMakeLists.txt Normal file
View File

@ -0,0 +1,56 @@
cmake_minimum_required(VERSION 3.16)
set(TARGET_APP "GenericQMLApp")
project(${TARGET_APP} VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Quick)
qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(${TARGET_APP}
main.cpp
)
qt_add_qml_module(${TARGET_APP}
URI GenericQML
QML_FILES
Main.qml
ListPage.qml
ListItemDelegate.qml
ExpandableItemDelegate.qml
EditableItemDelegate.qml
controls/PressAndHoldButton.qml
RESOURCES
icons/software-application.png
icons/moreUp.png icons/moreDown.png
icons/arrow-down.png icons/arrow-up.png
icons/list-delete.png
icons/minus-sign.png icons/plus-sign.png
)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
set_target_properties(${TARGET_APP} PROPERTIES
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.${TARGET_APP}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
target_include_directories(${TARGET_APP} PRIVATE ${CORE_LIB_DIR}/)
target_link_libraries(${TARGET_APP} PRIVATE GenericCore)
target_link_libraries(${TARGET_APP}
PRIVATE Qt6::Quick
)
include(GNUInstallDirs)
install(TARGETS ${TARGET_APP}
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

163
EditableItemDelegate.qml Normal file
View File

@ -0,0 +1,163 @@
import QtQuick
import "controls"
Item {
//! [0]
id: delegateItem
width: listView.width
height: 80
clip: true
required property int index
// required property string edit
// required property QtObject model
required property string name
required property string description
required property string info
required property int amount
required property real factor
Rectangle {
id: background
x: 2
y: 2
width: parent.width - x * 2
height: parent.height - y * 2
color: wccDarkLight
border.color: index === listView.currentIndex ? "blue" : wccDarkDefault
border.width: 3
radius: 5
}
Column {
id: arrows
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
Image {
source: "icons/arrow-up.png"
MouseArea {
anchors.fill: parent
// onClicked: fruitModel.move(delegateItem.index,
// delegateItem.index - 1, 1)
}
}
Image {
source: "icons/arrow-down.png"
MouseArea {
anchors.fill: parent
// onClicked: fruitModel.move(delegateItem.index,
// delegateItem.index + 1, 1)
}
}
}
Column {
anchors {
left: arrows.right
horizontalCenter: parent.horizontalCenter
bottom: parent.verticalCenter
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: delegateItem.name
font.pixelSize: 15
color: "white"
}
Text {
text: delegateItem.description
color: "White"
}
}
Item {
anchors {
left: arrows.right
horizontalCenter: parent.horizontalCenter
top: parent.verticalCenter
bottom: parent.bottom
}
Row {
anchors.centerIn: parent
spacing: 10
PressAndHoldButton {
anchors.verticalCenter: parent.verticalCenter
source: "icons/plus-sign.png"
onClicked: delegateItem.factor = delegateItem.factor + 0.25
}
Text {
id: factorText
anchors.verticalCenter: parent.verticalCenter
text: 'Factor: ' + Number(delegateItem.factor).toFixed(2)
font.pixelSize: 15
color: "white"
font.bold: true
}
PressAndHoldButton {
anchors.verticalCenter: parent.verticalCenter
source: "icons/minus-sign.png"
onClicked: delegateItem.factor = Math.max(
0, delegateItem.factor - 0.25)
}
Image {
source: "icons/list-delete.png"
MouseArea {
anchors.fill: parent
onClicked: fruitModel.remove(delegateItem.index)
}
}
}
}
// Animate adding and removing of items:
//! [1]
SequentialAnimation {
id: addAnimation
PropertyAction {
target: delegateItem
property: "height"
value: 0
}
NumberAnimation {
target: delegateItem
property: "height"
to: 80
duration: 250
easing.type: Easing.InOutQuad
}
}
ListView.onAdd: addAnimation.start()
SequentialAnimation {
id: removeAnimation
PropertyAction {
target: delegateItem
property: "ListView.delayRemove"
value: true
}
NumberAnimation {
target: delegateItem
property: "height"
to: 0
duration: 250
easing.type: Easing.InOutQuad
}
// Make sure delayRemove is set back to false so that the item can be destroyed
PropertyAction {
target: delegateItem
property: "ListView.delayRemove"
value: false
}
}
ListView.onRemove: removeAnimation.start()
}

276
ExpandableItemDelegate.qml Normal file
View File

@ -0,0 +1,276 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
id: item
required property int index
required property string name
required property string description
required property string info
required property int amount
required property real factor
required property string type
property real detailsOpacity: 0
width: ListView.view.width
height: 70
Rectangle {
id: background
x: 2
y: 2
width: parent.width - x * 2
height: parent.height - y * 2
color: wccDarkLight
border.color: index === listView.currentIndex ? "blue" : wccDarkDefault
border.width: 3
radius: 5
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
parent.ListView.view.currentIndex = parent.index
if (mouse.button === Qt.RightButton) {
item.state = 'Details'
}
}
}
Row {
id: topLayout
x: 10
y: 10
height: icon.height
width: parent.width
spacing: 10
Image {
id: icon
width: 50
height: 50
source: "icons/software-application.png"
}
Column {
width: background.width - icon.width - 20
height: icon.height
spacing: 5
Text {
text: item.name
font.bold: true
font.pointSize: 16
}
Text {
text: qsTr("Description")
font.bold: true
font.pointSize: 9
opacity: item.detailsOpacity
}
Text {
text: item.description
wrapMode: Text.WordWrap
width: parent.width
font.pointSize: 9
opacity: item.detailsOpacity
}
}
}
Item {
id: details
x: 10
width: parent.width - 20
anchors {
top: topLayout.bottom
topMargin: 10
bottom: parent.bottom
bottomMargin: 10
}
opacity: item.detailsOpacity
Text {
id: moreInfoTitle
anchors.top: parent.top
text: qsTr("Further information")
font.pointSize: 9
font.bold: true
}
Flickable {
id: flick
width: parent.width
anchors {
top: moreInfoTitle.bottom
bottom: parent.bottom
topMargin: 5
}
contentHeight: infoText.height
clip: true
ColumnLayout {
Text {
id: infoText
text: item.info
wrapMode: Text.WordWrap
width: details.width
}
Text {
id: amountText
text: "Amount: " + item.amount
wrapMode: Text.WordWrap
}
SpinBox {
value: item.amount
width: 80
height: 25
onValueModified: item.amount = value
}
Text {
id: factorText
text: "Factor: " + item.factor
wrapMode: Text.WordWrap
width: details.width
}
SpinBox {
id: spinBox
from: 0
value: decimalToInt(item.factor)
to: decimalToInt(100)
stepSize: decimalFactor
editable: true
property int decimals: 2
property real realValue: value / decimalFactor
readonly property int decimalFactor: Math.pow(10, decimals)
function decimalToInt(decimal) {
return decimal * decimalFactor
}
onValueModified: item.factor = value / decimalFactor
validator: DoubleValidator {
bottom: Math.min(spinBox.from, spinBox.to)
top: Math.max(spinBox.from, spinBox.to)
decimals: spinBox.decimals
notation: DoubleValidator.StandardNotation
}
textFromValue: function (value, locale) {
return Number(value / decimalFactor).toLocaleString(
locale, 'f', spinBox.decimals)
}
valueFromText: function (text, locale) {
return Math.round(Number.fromLocaleString(
locale, text) * decimalFactor)
}
}
Text {
id: typeText
text: "Type: " + item.type
wrapMode: Text.WordWrap
}
ComboBox {
// TODO use model from metadata.h (in some way)
model: ["A", "B", "C", ""]
// BUG type is not been updated due to undo/redo step
currentIndex: find(item.type)
Component.onCompleted: currentIndex = find(item.type)
onCurrentTextChanged: {
item.type = currentText
}
width: 80
height: 25
}
}
}
Image {
anchors {
right: flick.right
top: flick.top
}
source: "icons/moreUp.png"
opacity: flick.atYBeginning ? 0 : 1
}
Image {
anchors {
right: flick.right
bottom: flick.bottom
}
source: "icons/moreDown.png"
opacity: flick.atYEnd ? 0 : 1
}
}
Button {
y: 10
anchors {
right: background.right
rightMargin: 10
}
opacity: item.detailsOpacity
text: qsTr("Close")
onClicked: item.state = ''
}
states: State {
name: "Details"
PropertyChanges {
background.color: "white"
icon {
// Make picture bigger
width: 130
height: 130
}
item {
// Make details visible
detailsOpacity: 1
x: 0
// Fill the entire list area with the detailed view
height: listView.height
}
}
// Move the list so that this item is at the top.
PropertyChanges {
item.ListView.view.contentY: item.y
explicit: true
}
// Disallow flicking while we're in detailed view
PropertyChanges {
item.ListView.view.interactive: false
}
}
transitions: Transition {
// Make the state changes smooth
ParallelAnimation {
ColorAnimation {
property: "color"
duration: 500
}
NumberAnimation {
duration: 300
properties: "detailsOpacity,x,contentY,height,width"
}
}
}
}

24
ListItemDelegate.qml Normal file
View File

@ -0,0 +1,24 @@
import QtQuick
Item {
id: myItem
required property int index
required property string name
required property string info
property int fontSize: 16
width: parent.width
height: 40
Column {
Text {
text: '<b>Name:</b> ' + myItem.name
}
Text {
text: '<b>Info:</b> ' + myItem.info
}
}
MouseArea {
anchors.fill: parent
onClicked: parent.ListView.view.currentIndex = parent.index
}
}

82
ListPage.qml Normal file
View File

@ -0,0 +1,82 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Material
Page {
id: page
ColumnLayout {
anchors.fill: parent
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
focus: true
clip: true
model: mainModel
// delegate: ListItemDelegate {}
delegate: ExpandableItemDelegate {}
// delegate: EditableItemDelegate {}
delegateModelAccess: DelegateModel.ReadWrite
header: bannercomponent
footer: Rectangle {
width: parent.width
height: 30
gradient: mainGradient
}
}
Button {
Layout.fillWidth: true
text: "Undo: " + appUndoStack.undoText
enabled: appUndoStack.canUndo
onClicked: {
appUndoStack.undo()
}
}
Button {
Layout.fillWidth: true
text: "Redo: " + appUndoStack.redoText
enabled: appUndoStack.canRedo
onClicked: {
appUndoStack.redo()
}
}
}
Component {
//instantiated when header is processed
id: bannercomponent
Rectangle {
id: banner
width: parent.width
height: 50
gradient: mainGradient
border {
color: wccPurpleDark
width: 2
}
Text {
anchors.centerIn: parent
text: window.title
font.pixelSize: 32
color: wccDarkLight
}
}
}
Gradient {
id: mainGradient
GradientStop {
position: 0.0
color: wccPurpleDefault
}
GradientStop {
position: 0.66
color: wccPurpleDark
}
}
}

88
Main.qml Normal file
View File

@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
import QtQml.Models
Window {
id: window
property bool discardChangesOnExit: false
property bool isDataModified: !appUndoStack.isClean
property string titleClean: `${Application.name}`
property string titleDirty: `${Application.name}` + " *"
width: 480
height: 800
visible: true
title: appUndoStack.clean ? titleClean : titleDirty
property int fontSize: 16
property color textColor: "black"
property color wccDarkDark: "#010101"
property color wccDarkDefault: "#3C3B3B"
property color wccDarkLight: "#828282"
property color wccPurpleDark: "#631A61"
property color wccPurpleDefault: "#A834A5"
property color wccPurpleLight: "#E88FE5"
property color wccLavenderDark: "#8C52FF"
property color wccLavenderDefault: "#9D74EE"
property color wccLavenderLight: "#BC9AFF"
ListPage {
id: listPage
anchors.fill: parent
}
// Component.onCompleted: {
// // core.displayStatusMessage.connect(displayStatusMessage)
// appUndoStack.cleanChanged.connect(cleanChanged)
// // core.userConfigChanged.connect(onUserConfigChanged)
// }
// function cleanChanged() {
// let clean = appUndoStack.clean
// console.debug("Clean state changed to: " + clean)
// // if (!clean) {
// // footerText.text = ""
// // }
// }
onClosing: event => {
if (appUndoStack.clean) {
console.debug("Closing on a clean undo stack.");
} else {
console.debug("Closing on an unclean undo stack!");
if (!window.discardChangesOnExit) {
event.accepted = false;
exitOnUnsavedChangesDialog.open();
}
}
}
Dialog {
id: exitOnUnsavedChangesDialog
title: "Unsaved Changes"
modal: false
anchors.centerIn: parent
width: 300
standardButtons: Dialog.Yes | Dialog.Cancel | Dialog.Discard
contentItem: Label {
text: "Do you want save your changes?\n" + "Or discard them?"
}
onAccepted: {
core.saveItems();
window.close();
}
onDiscarded: {
window.discardChangesOnExit = true;
window.close();
}
onRejected: {
console.debug("Canceling exit...");
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Image {
id: container
property int repeatDelay: 300
property int repeatDuration: 75
property bool pressed
signal clicked
scale: pressed ? 0.9 : 1
function release() {
autoRepeatClicks.stop()
container.pressed = false
}
SequentialAnimation on pressed {
id: autoRepeatClicks
running: false
PropertyAction { target: container; property: "pressed"; value: true }
ScriptAction { script: container.clicked() }
PauseAnimation { duration: container.repeatDelay }
SequentialAnimation {
loops: Animation.Infinite
ScriptAction { script: container.clicked() }
PauseAnimation { duration: container.repeatDuration }
}
}
MouseArea {
anchors.fill: parent
onPressed: autoRepeatClicks.start()
onReleased: container.release()
onCanceled: container.release()
}
}

BIN
icons/arrow-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

BIN
icons/arrow-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

BIN
icons/list-delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

BIN
icons/minus-sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

BIN
icons/moreDown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

BIN
icons/moreUp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

BIN
icons/plus-sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

38
main.cpp Normal file
View File

@ -0,0 +1,38 @@
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QUndoCommand>
#include "model/generalsortfiltermodel.h"
#include "genericcore.h"
#ifdef QT_DEBUG
#include "utils/messagehandler.h"
#endif
int main(int argc, char* argv[]) {
#ifdef QT_DEBUG
qInstallMessageHandler(consoleHandlerColoredVerboseInDarkTheme);
#endif
QGuiApplication app(argc, argv);
std::unique_ptr<GenericCore> core = std::make_unique<GenericCore>();
std::shared_ptr<GeneralSortFilterModel> mainModel = core->getSortFilterModel();
QUndoStack* undoStack = core->getModelUndoStack();
// qInfo() << "QMLApp Version:" << QMLAPP_VERSION;
qInfo() << "core->getString():" << core->toString();
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("core"), core.get());
engine.rootContext()->setContextProperty(QStringLiteral("mainModel"), mainModel.get());
engine.rootContext()->setContextProperty(QStringLiteral("appUndoStack"), undoStack);
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[]() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
engine.loadFromModule("GenericQML", "Main");
return app.exec();
}