diff --git a/ProjectTemplate/App.qml b/ProjectTemplate/App.qml new file mode 100644 index 0000000..11b615f --- /dev/null +++ b/ProjectTemplate/App.qml @@ -0,0 +1,6 @@ +import QtQuick 2.8 +import de.skycoder42.qtmvvm.quick 1.0 + +App { + +} diff --git a/ProjectTemplate/app.cpp b/ProjectTemplate/app.cpp new file mode 100644 index 0000000..1aa3938 --- /dev/null +++ b/ProjectTemplate/app.cpp @@ -0,0 +1,41 @@ +#include "%{AppHdrName}" + +%{AppCn}::%{AppCn}(QObject *parent) : + CoreApp(parent), + mainControl(nullptr) +{ + //register metatypes etc here, just like you would do in your main before call QCoreApplication::exec + + //if you are using a qt resource (e.g. "%{AppQrcFile}"), initialize it here +@if '%{UseSettings}' + Q_INIT_RESOURCE(%{AppQrcName}); +@else + //Q_INIT_RESOURCE(%{AppQrcName}); +@endif +} + +void %{AppCn}::setupParser(QCommandLineParser &parser, bool &allowInvalid) const +{ + CoreApp::setupParser(parser, allowInvalid); + //add additional command line arguments etc here +} + +bool %{AppCn}::startApp(const QCommandLineParser &parser) +{ + //shows help or version automatically + if(autoShowHelpOrVersion(parser)) + return true; + + //use this method to create services, controls, etc + + //create and show the inital control + mainControl = new %{ControlCn}(this); + showControl(mainControl); + + return true; +} + +void %{AppCn}::aboutToQuit() +{ + //if you need to perform any cleanups, do it here +} diff --git a/ProjectTemplate/app.h b/ProjectTemplate/app.h new file mode 100644 index 0000000..e9a234f --- /dev/null +++ b/ProjectTemplate/app.h @@ -0,0 +1,29 @@ +#ifndef %{AppGuard} +#define %{AppGuard} + +#include + +#include "%{ControlHdrName}" + +class %{AppCn} : public CoreApp +{ + Q_OBJECT + +public: + explicit %{AppCn}(QObject *parent = nullptr); + +protected: + void setupParser(QCommandLineParser &parser, bool &allowInvalid) const override; + bool startApp(const QCommandLineParser &parser) override; + +protected slots: + void aboutToQuit() override; + +private: + %{ControlCn} *mainControl; +}; + +#undef coreApp +#define coreApp static_cast<%{AppCn}*>(CoreApp::instance()) + +#endif // %{AppGuard} diff --git a/ProjectTemplate/control.cpp b/ProjectTemplate/control.cpp new file mode 100644 index 0000000..d022543 --- /dev/null +++ b/ProjectTemplate/control.cpp @@ -0,0 +1,44 @@ +#include "%{ControlHdrName}" +@if '%{UseSettings}' +#include +@endif + +%{ControlCn}::%{ControlCn}(QObject *parent) : + Control(parent), + _text(QStringLiteral("hello world")) +{} + +QString %{ControlCn}::text() const +{ + return _text; +} + +@if '%{UseSettings}' +void %{ControlCn}::showSettings() +{ + auto settings = new SettingsControl(this); + settings->setDeleteOnClose(true); + settings->show(); +} + +@endif +void %{ControlCn}::setText(QString text) +{ + if (_text == text) + return; + + _text = text; + emit textChanged(_text); +} + +void %{ControlCn}::onShow() +{ + qDebug("%{ControlName} gui is now visible"); + //logic to execute when the gui is shown +} + +void %{ControlCn}::onClose() +{ + qDebug("%{ControlName} gui is now closed"); + //logic to execute when the gui was closed +} diff --git a/ProjectTemplate/control.h b/ProjectTemplate/control.h new file mode 100644 index 0000000..614c724 --- /dev/null +++ b/ProjectTemplate/control.h @@ -0,0 +1,34 @@ +#ifndef %{ControlGuard} +#define %{ControlGuard} + +#include + +class %{ControlCn} : public Control +{ + Q_OBJECT + + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + +public: + explicit %{ControlCn}(QObject *parent = nullptr); + + void onShow() override; + void onClose() override; + + QString text() const; + +public slots: +@if '%{UseSettings}' + void showSettings(); + +@endif + void setText(QString text); + +signals: + void textChanged(QString text); + +private: + QString _text; +}; + +#endif // %{ControlGuard} diff --git a/ProjectTemplate/core.pro b/ProjectTemplate/core.pro new file mode 100644 index 0000000..3e98f55 --- /dev/null +++ b/ProjectTemplate/core.pro @@ -0,0 +1,33 @@ +TEMPLATE = lib + +QT += core gui +CONFIG += c++11 staticlib #important because dlls are problematic + +TARGET = %{CoreName} + +DEFINES += QT_DEPRECATED_WARNINGS + +HEADERS += \\ + %{AppHdrName} \\ + %{ControlHdrName} + +SOURCES += \\ + %{AppSrcName} \\ + %{ControlSrcName} + +@if '%{UseSettings}' +RESOURCES += \\ + %{AppQrcFile} + +@endif +TRANSLATIONS += %{ProjectLowerName}_core_de.ts \\ + %{ProjectLowerName}_core_template.ts + +@if '%{UseSettings}' +QTMVVM_SETTINGS_FILES = settings.xml +never_true_lupdate_only: SOURCES += .qtmvvm_settings_xml_ts.cppdummy +CONFIG += no_settings_ts_warn + +@endif +!ReleaseBuild:!DebugBuild:!system(qpmx -d $$shell_quote($$_PRO_FILE_PWD_) --qmake-run init $$QPMX_EXTRA_OPTIONS $$shell_quote($$QMAKE_QMAKE) $$shell_quote($$OUT_PWD)): error(qpmx initialization failed. Check the compilation log for details.) +else: include($$OUT_PWD/qpmx_generated.pri) diff --git a/ProjectTemplate/core.qrc b/ProjectTemplate/core.qrc new file mode 100644 index 0000000..9d84d5c --- /dev/null +++ b/ProjectTemplate/core.qrc @@ -0,0 +1,5 @@ + + + settings.xml + + diff --git a/ProjectTemplate/git.ignore b/ProjectTemplate/git.ignore new file mode 100644 index 0000000..2f0e60b --- /dev/null +++ b/ProjectTemplate/git.ignore @@ -0,0 +1,76 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# qpmx +.qpmx-dev-cache +*.cppdummy diff --git a/ProjectTemplate/guiapplication.png b/ProjectTemplate/guiapplication.png new file mode 100644 index 0000000..c121959 Binary files /dev/null and b/ProjectTemplate/guiapplication.png differ diff --git a/ProjectTemplate/project.pro b/ProjectTemplate/project.pro new file mode 100644 index 0000000..a0e9544 --- /dev/null +++ b/ProjectTemplate/project.pro @@ -0,0 +1,17 @@ +TEMPLATE = subdirs + +SUBDIRS += \\ +@if '%{CreateWidgets}' + %{WidgetsName} \\ +@endif +@if '%{CreateQuick}' + %{QuickName} \\ +@endif + %{CoreName} + +@if '%{CreateWidgets}' +%{WidgetsName}.depends += %{CoreName} +@endif +@if '%{CreateQuick}' +%{QuickName}.depends += %{CoreName} +@endif diff --git a/ProjectTemplate/qpmx_core.json b/ProjectTemplate/qpmx_core.json new file mode 100644 index 0000000..c7a0630 --- /dev/null +++ b/ProjectTemplate/qpmx_core.json @@ -0,0 +1,27 @@ +{ + "dependencies": [ +@if '%{UseSettings}' + { + "package": "de.skycoder42.qtmvvm.settings.core", + "provider": "qpm", + "version": "1.1.2" + }, +@endif + { + "package": "de.skycoder42.qtmvvm.core", + "provider": "qpm", + "version": "1.1.1" + } + ], + "license": { + "file": "", + "name": "" + }, + "prcFile": "", + "priFile": "", + "priIncludes": [ + ], + "publishers": { + }, + "source": false +} diff --git a/ProjectTemplate/qpmx_quick.json b/ProjectTemplate/qpmx_quick.json new file mode 100644 index 0000000..87805b2 --- /dev/null +++ b/ProjectTemplate/qpmx_quick.json @@ -0,0 +1,28 @@ +{ + "dependencies": [ +@if '%{UseSettings}' + { + "package": "de.skycoder42.qtmvvm.settings.quick", + "provider": "qpm", + "version": "1.1.2" + }, +@endif + { + "package": "de.skycoder42.qtmvvm.quick", + "provider": "qpm", + "version": "1.1.2" + } + ], + "license": { + "file": "", + "name": "" + }, + "prcFile": "", + "priFile": "", + "priIncludes": [ + "../%{CoreName}" + ], + "publishers": { + }, + "source": false +} diff --git a/ProjectTemplate/qpmx_widgets.json b/ProjectTemplate/qpmx_widgets.json new file mode 100644 index 0000000..1c7c823 --- /dev/null +++ b/ProjectTemplate/qpmx_widgets.json @@ -0,0 +1,28 @@ +{ + "dependencies": [ +@if '%{UseSettings}' + { + "package": "de.skycoder42.qtmvvm.settings.widgets", + "provider": "qpm", + "version": "1.1.3" + }, +@endif + { + "package": "de.skycoder42.qtmvvm.widgets", + "provider": "qpm", + "version": "1.1.2" + } + ], + "license": { + "file": "", + "name": "" + }, + "prcFile": "", + "priFile": "", + "priIncludes": [ + "../%{CoreName}" + ], + "publishers": { + }, + "source": false +} diff --git a/ProjectTemplate/quick.pro b/ProjectTemplate/quick.pro new file mode 100644 index 0000000..24674d0 --- /dev/null +++ b/ProjectTemplate/quick.pro @@ -0,0 +1,44 @@ +TEMPLATE = app + +QT += core gui qml quick quickcontrols2 +CONFIG += c++11 + +TARGET = %{QuickName} + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH = + +DEFINES += QT_DEPRECATED_WARNINGS + +SOURCES += main.cpp + +RESOURCES += \\ + %{QuickQrcFile} + +TRANSLATIONS += %{ProjectLowerName}_quick_de.ts \\ + %{ProjectLowerName}_quick_template.ts + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +# Link with core project +win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../%{CoreName}/release/ -l%{CoreName} +else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../%{CoreName}/debug/ -l%{CoreName} +else:unix: LIBS += -L$$OUT_PWD/../%{CoreName}/ -l%{CoreName} + +INCLUDEPATH += $$PWD/../%{CoreName} +DEPENDPATH += $$PWD/../%{CoreName} + +win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/release/lib%{CoreName}.a +else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/debug/lib%{CoreName}.a +else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/release/%{CoreName}.lib +else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/debug/%{CoreName}.lib +else:unix: PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/lib%{CoreName}.a + +!ReleaseBuild:!DebugBuild:!system(qpmx -d $$shell_quote($$_PRO_FILE_PWD_) --qmake-run init $$QPMX_EXTRA_OPTIONS $$shell_quote($$QMAKE_QMAKE) $$shell_quote($$OUT_PWD)): error(qpmx initialization failed. Check the compilation log for details.) +else: include($$OUT_PWD/qpmx_generated.pri) diff --git a/ProjectTemplate/quick.qrc b/ProjectTemplate/quick.qrc new file mode 100644 index 0000000..6688d20 --- /dev/null +++ b/ProjectTemplate/quick.qrc @@ -0,0 +1,8 @@ + + + App.qml + + + %{QuickQmlName} + + diff --git a/ProjectTemplate/quick_main.cpp b/ProjectTemplate/quick_main.cpp new file mode 100644 index 0000000..d3646db --- /dev/null +++ b/ProjectTemplate/quick_main.cpp @@ -0,0 +1,18 @@ +#include +#include +#include +#include <%{AppHdrName}> + +REGISTER_CORE_APP(%{AppCn}) + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication app(argc, argv); + + qmlRegisterUncreatableType<%{ControlCn}>("com.example.%{ProjectLowerName}", 1, 0, "%{ControlCn}", "Controls cannot be created!"); + + QuickPresenter::createAppEngine(QUrl(QLatin1String("qrc:/qml/App.qml"))); + + return app.exec(); +} diff --git a/ProjectTemplate/settings.xml b/ProjectTemplate/settings.xml new file mode 100644 index 0000000..d34233a --- /dev/null +++ b/ProjectTemplate/settings.xml @@ -0,0 +1,8 @@ + + + + diff --git a/ProjectTemplate/view.qml b/ProjectTemplate/view.qml new file mode 100644 index 0000000..a6bf8d4 --- /dev/null +++ b/ProjectTemplate/view.qml @@ -0,0 +1,66 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 +import de.skycoder42.quickextras 2.0 +import de.skycoder42.qtmvvm.quick 1.0 +import com.example.%{ProjectLowerName} 1.0 + +Page { + id: mainView + property %{ControlCn} control: null + + header: ActionBar { + id: toolbar + title: qsTr("%{ControlClassName}") + showMenuButton: false +@if '%{UseSettings}' + + moreMenu: Menu { + MenuItem { + id: settings + text: qsTr("Settings") + onClicked: control.showSettings() + } + } +@endif + } + + PresenterProgress {} + + Pane { + anchors.fill: parent + + ColumnLayout { + anchors.fill: parent + + TextField { + id: textEdit + Layout.fillWidth: true + + QtMvvmBinding { + control: mainView.control + controlProperty: "text" + view: textEdit + viewProperty: "text" + } + } + + Label { + id: textLabel + Layout.fillWidth: true + + QtMvvmBinding { + control: mainView.control + controlProperty: "text" + view: textLabel + viewProperty: "text" + type: QtMvvmBinding.OneWayFromControl + } + } + + Item { + Layout.fillHeight: true + } + } + } +} diff --git a/ProjectTemplate/widgets.pro b/ProjectTemplate/widgets.pro new file mode 100644 index 0000000..283ef1b --- /dev/null +++ b/ProjectTemplate/widgets.pro @@ -0,0 +1,35 @@ +TEMPLATE = app + +QT += core gui widgets +CONFIG += c++11 + +TARGET = %{WidgetsName} + +DEFINES += QT_DEPRECATED_WARNINGS + +HEADERS += %{WindowHdrName} + +SOURCES += main.cpp \\ + %{WindowSrcName} + +FORMS += %{WindowFormName} + +TRANSLATIONS += %{ProjectLowerName}_widgets_de.ts \\ + %{ProjectLowerName}_widgets_template.ts + +# Link with core project +win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../%{CoreName}/release/ -l%{CoreName} +else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../%{CoreName}/debug/ -l%{CoreName} +else:unix: LIBS += -L$$OUT_PWD/../%{CoreName}/ -l%{CoreName} + +INCLUDEPATH += $$PWD/../%{CoreName} +DEPENDPATH += $$PWD/../%{CoreName} + +win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/release/lib%{CoreName}.a +else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/debug/lib%{CoreName}.a +else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/release/%{CoreName}.lib +else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/debug/%{CoreName}.lib +else:unix: PRE_TARGETDEPS += $$OUT_PWD/../%{CoreName}/lib%{CoreName}.a + +!ReleaseBuild:!DebugBuild:!system(qpmx -d $$shell_quote($$_PRO_FILE_PWD_) --qmake-run init $$QPMX_EXTRA_OPTIONS $$shell_quote($$QMAKE_QMAKE) $$shell_quote($$OUT_PWD)): error(qpmx initialization failed. Check the compilation log for details.) +else: include($$OUT_PWD/qpmx_generated.pri) diff --git a/ProjectTemplate/widgets_main.cpp b/ProjectTemplate/widgets_main.cpp new file mode 100644 index 0000000..801c97e --- /dev/null +++ b/ProjectTemplate/widgets_main.cpp @@ -0,0 +1,25 @@ +#include +#include +#include <%{AppHdrName}> + +@if '%{UseSettings}' +#include +@endif +#include "%{WindowHdrName}" + +//register the core app to be used +REGISTER_CORE_APP(%{AppCn}) + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + // automatically sets "WidgetPresenter" as presenter and registers the %{WindowName} class as a widget + // the control this widget belongs to is detected automatically via naming conventions + WidgetPresenter::registerWidget<%{WindowCn}>(); +@if '%{UseSettings}' + WidgetPresenter::registerWidget(); +@endif + + return a.exec(); +} diff --git a/ProjectTemplate/window.cpp b/ProjectTemplate/window.cpp new file mode 100644 index 0000000..f191184 --- /dev/null +++ b/ProjectTemplate/window.cpp @@ -0,0 +1,24 @@ +#include "%{WindowHdrName}" +#include "ui_%{WindowHdrName}" +#include + +%{WindowCn}::%{WindowCn}(Control *mControl, QWidget *parent) : + QMainWindow(parent), + control(static_cast<%{ControlCn}*>(mControl)), + ui(new Ui::%{WindowCn}) +{ + ui->setupUi(this); +@if '%{UseSettings}' + + connect(ui->actionSettings, &QAction::triggered, + control, &%{ControlCn}::showSettings); +@endif + + QtMvvmBinding::bind(control, "text", ui->lineEdit, "text"); + QtMvvmBinding::bind(control, "text", ui->label, "text", QtMvvmBinding::OneWayFromControl); +} + +%{WindowCn}::~%{WindowCn}() +{ + delete ui; +} diff --git a/ProjectTemplate/window.h b/ProjectTemplate/window.h new file mode 100644 index 0000000..c5b3964 --- /dev/null +++ b/ProjectTemplate/window.h @@ -0,0 +1,24 @@ +#ifndef %{WindowGuard} +#define %{WindowGuard} + +#include +#include <%{ControlHdrName}> + +namespace Ui { +class %{WindowCn}; +} + +class %{WindowCn} : public QMainWindow +{ + Q_OBJECT + +public: + Q_INVOKABLE %{WindowCn}(Control *mControl, QWidget *parent = nullptr); + ~%{WindowCn}(); + +private: + %{ControlCn} *control; + Ui::%{WindowCn} *ui; +}; + +#endif // %{WindowGuard} diff --git a/ProjectTemplate/window.ui b/ProjectTemplate/window.ui new file mode 100644 index 0000000..10104b5 --- /dev/null +++ b/ProjectTemplate/window.ui @@ -0,0 +1,86 @@ + + + %{WindowCn} + + + + 0 + 0 + 400 + 300 + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 77 + + + + + + + + + + 0 + 0 + 400 + 23 + + +@if '%{UseSettings}' + + + &File + + + + +@endif + + + + TopToolBarArea + + + false + + + + +@if '%{UseSettings}' + + + + :/qtmvvm/icons/settings.ico:/qtmvvm/icons/settings.ico + + + Settings + + + Ctrl+Alt+S + + + QAction::PreferencesRole + + +@endif + + + + + diff --git a/ProjectTemplate/wizard.json b/ProjectTemplate/wizard.json new file mode 100644 index 0000000..5f92001 --- /dev/null +++ b/ProjectTemplate/wizard.json @@ -0,0 +1,285 @@ +{ + "version": 1, + "supportedProjectTypes": [ "Qt4ProjectManager.Qt4Project" ], + "id": "G.QtMvvm", + "category": "F.Application", + "trDescription": "Creates a QtMvvm project skeleton, for easy creation of new QtMvvm projects.", + "trDisplayName": "QtMvvm Application Project", + "trDisplayCategory": "Application", + "icon": "guiapplication.png", + "featuresRequired": [ "QtSupport.Wizards.FeatureQt" ], + "enabled": "%{JS: [ %{Plugins} ].indexOf('QmakeProjectManager') >= 0}", + "options": + [ + { "key": "ProjectLowerName", "value": "%{JS: '%{ProjectName}'.toLowerCase()}" }, + { "key": "ProFileName", "value": "%{JS: Util.fileName('%{ProjectDirectory}/%{ProjectName}', 'pro')}" }, + + { "key": "CoreName", "value": "%{ProjectName}Core" }, + { "key": "CoreDirectory", "value": "%{ProjectDirectory}/%{CoreName}" }, + { "key": "CoreProName", "value": "%{JS: Util.fileName('%{CoreDirectory}/%{CoreName}', 'pro')}" }, + + { "key": "WidgetsName", "value": "%{ProjectName}Widgets" }, + { "key": "WidgetsDirectory", "value": "%{ProjectDirectory}/%{WidgetsName}" }, + { "key": "WidgetsProName", "value": "%{JS: Util.fileName('%{WidgetsDirectory}/%{WidgetsName}', 'pro')}" }, + + { "key": "QuickName", "value": "%{ProjectName}Quick" }, + { "key": "QuickDirectory", "value": "%{ProjectDirectory}/%{QuickName}" }, + { "key": "QuickProName", "value": "%{JS: Util.fileName('%{QuickDirectory}/%{QuickName}', 'pro')}" }, + + { "key": "AppName", "value": "%{ProjectName}App" }, + { "key": "AppHdrName", "value": "%{JS: Cpp.classToFileName('%{AppName}', '%{JS: Util.preferredSuffix('text/x-c++hdr')}')}" }, + { "key": "AppSrcName", "value": "%{JS: Cpp.classToFileName('%{AppName}', '%{JS: Util.preferredSuffix('text/x-c++src')}')}" }, + { "key": "AppCn", "value": "%{JS: Cpp.className('%{AppName}')}" }, + { "key": "AppGuard", "value": "%{JS: Cpp.headerGuard('%{AppHdrName}')}" }, + + { "key": "ControlHdrName", "value": "%{JS: Cpp.classToFileName('%{ControlClassName}', '%{JS: Util.preferredSuffix('text/x-c++hdr')}')}" }, + { "key": "ControlSrcName", "value": "%{JS: Cpp.classToFileName('%{ControlClassName}', '%{JS: Util.preferredSuffix('text/x-c++src')}')}" }, + { "key": "ControlCn", "value": "%{JS: Cpp.className('%{ControlClassName}')}" }, + { "key": "ControlGuard", "value": "%{JS: Cpp.headerGuard('%{ControlHdrName}')}" }, + + { "key": "AppQrcName", "value": "%{JS: '%{CoreName}'.toLowerCase()}" }, + { "key": "AppQrcFile", "value": "%{JS: Util.fileName('%{AppQrcName}', 'qrc')}" }, + + { "key": "WindowHdrName", "value": "%{JS: Cpp.classToFileName('%{WindowClassName}', '%{JS: Util.preferredSuffix('text/x-c++hdr')}')}" }, + { "key": "WindowSrcName", "value": "%{JS: Cpp.classToFileName('%{WindowClassName}', '%{JS: Util.preferredSuffix('text/x-c++src')}')}" }, + { "key": "WindowFormName", "value": "%{JS: Cpp.classToFileName('%{WindowClassName}', '%{JS: Util.preferredSuffix('application/x-designer')}')}" }, + { "key": "WindowCn", "value": "%{JS: Cpp.className('%{WindowClassName}')}" }, + { "key": "WindowGuard", "value": "%{JS: Cpp.headerGuard('%{WindowHdrName}')}" }, + + { "key": "QuickQrcFile", "value": "%{JS: Util.fileName('%{JS: '%{QuickName}'.toLowerCase()}', 'qrc')}" }, + { "key": "QuickQmlName", "value": "%{JS: Util.fileName('%{QuickClassName}', '%{JS: Util.preferredSuffix('text/x-qml')}')}" } + ], + + "pages": + [ + { + "trDisplayName": "Project Location", + "trShortTitle": "Location", + "typeId": "Project", + "data": { "trDescription": "This wizard creates the skeleton." } + }, + { + "trDisplayName": "Define Project Details", + "trShortTitle": "Details", + "typeId": "Fields", + "data" : + [ + { + "name": "CreateWidgets", + "trDisplayName": "Create Widgets GUI", + "type": "CheckBox", + "data": + { + "checkedValue": "CreateWidgets", + "uncheckedValue": "", + "checked": "true" + } + }, + { + "name": "CreateQuick", + "trDisplayName": "Create Quick GUI", + "type": "CheckBox", + "data": + { + "checkedValue": "CreateQuick", + "uncheckedValue": "", + "checked": "true" + } + }, + { + "name": "UseSettings", + "trDisplayName": "Add Settings template", + "type": "CheckBox", + "data": + { + "checkedValue": "UseSettings", + "uncheckedValue": "", + "checked": "true" + } + } + ] + }, + { + "trDisplayName": "Define Initial Control", + "trShortTitle": "Control", + "typeId": "Fields", + "data" : + [ + { + "name": "ControlName", + "trDisplayName": "Control base name:", + "mandatory": true, + "type": "LineEdit", + "data": + { + "trText": "Main", + "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)*[a-zA-Z_][a-zA-Z_0-9]*|)" + } + }, + { + "name": "Sp1", + "type": "Spacer", + "data": { "factor": 2 } + }, + { + "name": "ControlClassName", + "type": "LineEdit", + "trDisplayName": "Control class name:", + "mandatory": true, + "data": + { + "trText": "%{ControlName}Control", + "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)*[a-zA-Z_][a-zA-Z_0-9]*|)" + } + }, + { + "name": "WindowClassName", + "type": "LineEdit", + "trDisplayName": "Widget class name:", + "mandatory": true, + "data": + { + "trText": "%{ControlName}Window", + "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)*[a-zA-Z_][a-zA-Z_0-9]*|)" + } + }, + { + "name": "QuickClassName", + "type": "LineEdit", + "trDisplayName": "Qml class name:", + "mandatory": true, + "data": + { + "trText": "%{ControlName}View", + "validator": "(?:(?:[a-zA-Z_][a-zA-Z_0-9]*::)*[a-zA-Z_][a-zA-Z_0-9]*|)" + } + } + ] + }, + { + "trDisplayName": "Kit Selection", + "trShortTitle": "Kits", + "typeId": "Kits", + "enabled": "%{IsTopLevelProject}", + "data": { "projectFilePath": "%{ProFileName}" } + }, + { + "trDisplayName": "Project Management", + "trShortTitle": "Summary", + "typeId": "Summary" + } + ], + "generators": + [ + { + "typeId": "File", + "data": + [ + { + "source": "project.pro", + "target": "%{ProFileName}", + "openAsProject": true + }, + { + "source": "core.pro", + "target": "%{CoreProName}" + }, + { + "source": "qpmx_core.json", + "target": "%{CoreDirectory}/qpmx.json" + }, + { + "source": "app.h", + "target": "%{CoreDirectory}/%{AppHdrName}" + }, + { + "source": "app.cpp", + "target": "%{CoreDirectory}/%{AppSrcName}" + }, + { + "source": "control.h", + "target": "%{CoreDirectory}/%{ControlHdrName}" + }, + { + "source": "control.cpp", + "target": "%{CoreDirectory}/%{ControlSrcName}" + }, + { + "source": "core.qrc", + "target": "%{CoreDirectory}/%{AppQrcFile}", + "condition": "%{JS: '%{UseSettings}' !== ''}" + }, + { + "source": "settings.xml", + "target": "%{CoreDirectory}/settings.xml", + "condition": "%{JS: '%{UseSettings}' !== ''}" + }, + { + "source": "widgets.pro", + "target": "%{WidgetsProName}", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "qpmx_widgets.json", + "target": "%{WidgetsDirectory}/qpmx.json", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "widgets_main.cpp", + "target": "%{WidgetsDirectory}/main.cpp", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "window.h", + "target": "%{WidgetsDirectory}/%{WindowHdrName}", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "window.cpp", + "target": "%{WidgetsDirectory}/%{WindowSrcName}", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "window.ui", + "target": "%{WidgetsDirectory}/%{WindowFormName}", + "condition": "%{JS: '%{CreateWidgets}' !== ''}" + }, + { + "source": "quick.pro", + "target": "%{QuickProName}", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "qpmx_quick.json", + "target": "%{QuickDirectory}/qpmx.json", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "quick_main.cpp", + "target": "%{QuickDirectory}/main.cpp", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "quick.qrc", + "target": "%{QuickDirectory}/%{QuickQrcFile}", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "App.qml", + "target": "%{QuickDirectory}/App.qml", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "view.qml", + "target": "%{QuickDirectory}/%{QuickQmlName}", + "condition": "%{JS: '%{CreateQuick}' !== ''}" + }, + { + "source": "git.ignore", + "target": "%{ProjectDirectory}/.gitignore", + "condition": "%{JS: !%{IsSubproject} && '%{VersionControl}' === 'G.Git'}" + } + ] + } + ] +} diff --git a/README.md b/README.md index 0f03192..f9df130 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,179 @@ # QtMvvm -A mvvm oriented library for Qt, to create Projects for Widgets and Quick in parallel +A mvvm oriented library for Qt, to create Projects for Widgets and Quick Controls 2 in parallel. -**Note:** This project consists of many small github repositories. This repository is a meta-repository to keep documentation, examples, etc. in one place. Read the [Installation](#installation) chapter to learn how to use those repos. +[![Travis Build Status](https://travis-ci.org/Skycoder42/QtMvvm.svg?branch=master)](https://travis-ci.org/Skycoder42/QtMvvm) +[![Appveyor Build status](https://ci.appveyor.com/api/projects/status/cwtt2n6af3dcbswa?svg=true)](https://ci.appveyor.com/project/Skycoder42/qtmvvm) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/3b15e17fae4949668b12f54f4090d397)](https://www.codacy.com/app/Skycoder42/QtMvvm) +[![AUR](https://img.shields.io/aur/version/qt5-mvvmcore.svg)](https://aur.archlinux.org/pkgbase/qt5-mvvm/) + +![Demo Animation Widgets](./doc/images/sample_basic_widgets.gif) +![Demo Animation Quick](./doc/images/sample_basic_quick.gif) + +For more images, check the [Images page](https://skycoder42.github.io/QtMvvm/TODO) ## Features -The main feature of QtMvvm is the seperation between ui and logic. With this library, you can create a core library, containing your application logic, as well as ui controllers, and create multiple ui projects on top of it. This way you can for example provide both, a widgets and a qt quick based application, or create different uis for different devices. The key features are: +The main feature of QtMvvm is the seperation between ui and logic. With this library, you can create a core library, containing your application logic, as well as ui controllers (called "ViewModels"), and create multiple ui projects on top of it. This way you can for example provide both, a widgets and a qt quick based application, or create different uis for different devices, without having to code anything twice. -- The Control class to represent an ui element in your core app. Allows you to expose properties, signals and slots to the ui and control the application flow from your core app -- The IPresenter as interface to be implemented on each ui project. This is the part that presents uis based on controls -- The CoreApp to control the application startup independently of the QApplication used -- Functions to show messageboxes (info, warning, error, etc.) from your core app - - Supports even input dialogs, with default edits as well as the option to add custom inputs +The key features are: -### QtMvvm Settings -Another feature is the QtMvvmSettings module. This extends the QtMvvm projects by adding ui independent settings. it is provided as seperate module to extend the basic QtMvvm. +- Create ViewModels in the core application to prepare data for presentation without binding to any concret GUI +- Functions to show messageboxes (info, warning, error, etc.) from your core app + - Asynchronous, with result handling + - Supports input dialogs and file dialogs out of the box + - custom dialog types can be created +- Methods to create Two-Way Bindings from C++ and QML +- Macros and a ServiceRegistry to make Dependency Injection possible for Services and ViewModels +- A generic Presenter Interface so you can create your own custom GUI implementations + - Widgets and Qt Quick Controls 2 are supported out of the box +- Generic Edit-View factories to create simple edits for any kind of data from the core code +- Supports an extensive "Settings GUI" via a simple XML format -- Create a control and an xml file (or custom formats) describing the settings ui in your core app -- Automatically creates a ui for widgets or quick based on the xml - - Supports search as well as a reset functionality - - Utilizes the same edits as the input dialogs - ### QtMvvm Datasync -The QtMvvmDatasync module helps you to integrate [QtDataSync](https://github.com/Skycoder42/QtDataSync) (An easy and reliable synchronization library) into your projects. It adds uis to: - -- Monitor the connection status - - Current status - - Synchronization progress - - Displays errors (if present) -- Enable/disable synchronization -- Trigger synchronization - - Including Resync -- Exchange User data (identity, encryption key, etc.) - - Export/Import via files - - UPD-based local network exchange - -## Modules -- **QtMvvm** - The QtMvvm library itself - - [`de.skycoder42.qtmvvm.core`](https://github.com/Skycoder42/QtMvvmCore) - The core part of qtmvvm - - [`de.skycoder42.qtmvvm.widgets`](https://github.com/Skycoder42/QtMvvmWidgets) - The basic frontend for QtWidgets - - [`de.skycoder42.qtmvvm.quick`](https://github.com/Skycoder42/QtMvvmQuick) - The basic frontend for QtQuick Controls 2 -- **QtMvvmSettings** - A module adding an easy way to create settings dialogs - - [`de.skycoder42.qtmvvm.settings.core`](https://github.com/Skycoder42/QtMvvmSettingsCore) - The core part of the settings module (the logic) - - [`de.skycoder42.qtmvvm.settings.widgets`](https://github.com/Skycoder42/QtMvvmSettingsWidgets) - The ui implementation for QtWidgets - - [`de.skycoder42.qtmvvm.settings.quick`](https://github.com/Skycoder42/QtMvvmSettingsQuick) - The ui implementation for QtQuick Controls 2 -- **QtMvvmDatasync** - A module adding control and monitor uis for the QtDataSync library - - [`de.skycoder42.qtmvvm.datasync.core`](https://github.com/Skycoder42/QtMvvmDatasyncCore) - The core part of the datasync module (the logic) - - [`de.skycoder42.qtmvvm.datasync.widgets`](https://github.com/Skycoder42/QtMvvmDatasyncWidgets) - The ui implementation for QtWidgets - - [`de.skycoder42.qtmvvm.datasync.quick`](https://github.com/Skycoder42/QtMvvmDatasyncQuick) - The ui implementation for QtQuick Controls 2 - -## Requirements -QtMvvm heavily relies on [qpm](https://www.qpm.io/). Check [GitHub - Installing](https://github.com/Cutehacks/qpm/blob/master/README.md#installing) to learn how to install qpm. You can of course use the project without qpm, but in that case you will have to collect all the dependencies by yourself. If you are unfamiliar with qpm, no worries, it's really easy to use. - -## Gettings started -The following chapters will explain how to create a QtMvvm Project and how to correctly implement applications with it +The QtMvvmDatasync modules help you to integrate [QtDataSync](https://github.com/Skycoder42/QtDataSync) (An easy and reliable synchronization library) into your projects. It adds ViewModels and Views to: + +- Monitor and control the connection and synchronization status +- Manage your account and account devices +- Easy file based import and exports, as well as a fronted for the local Network-Exchange + +### The Mvvm Pattern +If you don't know the Mvvm pattern already, you can read up on the links below. It's basically a clever seperation +of logic (the models), presentation logic (the viewmodels) and the actual GUI (the views) that is very useful when +creating applications that need to support different uis for the same data. + +![[The MVVM Pattern](https://msdn.microsoft.com/en-us/library/hh848246.aspx)](https://i-msdn.sec.s-msft.com/dynimg/IC564167.png) + +Good links to get started: + +- https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel +- https://msdn.microsoft.com/en-us/library/hh848246.aspx + +## Download/Installation +1. Package Managers: The library is available via: + - **Arch-Linux:** AUR-Repository: [`qt5-qtmvvm`](https://aur.archlinux.org/pkgbase/qt5-mvvm/) + - **Ubuntu:** Launchpad-PPA: [ppa:skycoder42/qt-modules](https://launchpad.net/~skycoder42/+archive/ubuntu/qt-modules), package `libqt5mvvm[1/-dev]` + - **MacOs:** + - Tap: [`brew tap Skycoder42/qt-modules`](https://github.com/Skycoder42/homebrew-qt-modules) + - Package: `qtmvvm` + - **IMPORTANT:** Due to limitations of homebrew, you must run `source /usr/local/opt/qtmvvm/bashrc.sh` before you can use the module. Some goes for the `qtdatasync` dependency. +2. Simply add my repository to your Qt MaintenanceTool (Image-based How-To here: [Add custom repository](https://github.com/Skycoder42/QtModules/blob/master/README.md#add-my-repositories-to-qt-maintenancetool)): + 1. Open the MaintenanceTool, located in your Qt install directory (e.g. `~/Qt/MaintenanceTool`) + 2. Select `Add or remove components` and click on the `Settings` button + 3. Go to `Repositories`, scroll to the bottom, select `User defined repositories` and press `Add` + 4. In the right column (selected by default), type: + - On Linux: https://install.skycoder42.de/qtmodules/linux_x64 + - On Windows: https://install.skycoder42.de/qtmodules/windows_x86 + - On Mac: https://install.skycoder42.de/qtmodules/mac_x64 + 5. Press `Ok`, make shure `Add or remove components` is still selected, and continue the install (`Next >`) + 6. A new entry appears under all supported Qt Versions (e.g. `Qt > Qt 5.10 > Skycoder42 Qt modules`) + 7. You can install either all of my modules, or select the one you need: `Qt Mvvm` + 8. Continue the setup and thats it! you can now use the module for all of your installed Kits for that Qt Version +3. Download the compiled modules from the release page. **Note:** You will have to add the correct ones yourself and may need to adjust some paths to fit your installation! In addition to that, you will have to download the modules this one depends on as well. See Section "Requirements" below. +4. Build it yourself! **Note:** This requires all build an runtime dependencies to be available (See Section "Requirements" below). If you don't have/need cmake, you can ignore the related warnings. To automatically build and install to your Qt installation, run: + - `qmake` + - `make qmake_all` + - `make` (If you want the tests/examples/etc. run `make all`) + - Optional steps: + - `make doxygen` to generate the documentation + - `make lrelease` to generate the translations + - `make install` + +### Requirements +The library only has a few dependencies. The main modules only depends on qtbase and qtquick respectively. However, the Datasync extensions need QtDataSync of course. + +- Custom Qt modules: + - [QtDataSync](https://github.com/Skycoder42/QtDataSync) (DataSync extensions only) +- Additional stuff for building it yourself: + - perl + - [qpmx](https://github.com/Skycoder42/qpmx) + - [qpm](https://github.com/Cutehacks/qpm) + - Please note that you need the QtDataSync for building the datasync modules, but if you don't have it, the build wont fail. The datasync modules will simply be skipped. + +### Modules +- **QtMvvmCore:** The core part of QtMvvm +- **QtMvvmWidgets:** The basic frontend for QtWidgets +- **QtMvvmQuick:** The basic frontend for QtQuick Controls 2 +- **QtMvvmDataSyncCore:** The core part of the QtMvvm DataSync extension (requires QtDataSync) +- **QtMvvmDataSyncWidgets:** The frontend extensions of QtMvvm DataSync for QtWidgets (requires QtDataSync) +- **QtMvvmDataSyncQuick:** The frontend extensions of QtMvvm DataSync for QtQuick Controls 2 (requires QtDataSync) + +## Usage +The following chapters will explain how to create a QtMvvm Project and how to correctly implement applications with it. A Mvvm Project always consists of one core project, with the application logic, and one or more gui projects with the View implementations. In the following section it is explained how to use QtMvvm without going into the depths. For more details you can check the sample projects. If you want to go deeper on how the Framework works and what detailed steps are needed, check out the [Documentation](https://skycoder42.github.io/QtMvvm/) of the following classes: + +- CoreApp +- ViewModel +- ServiceRegistry +- IPresenter +- TODO add more + +The easiest way to create a QtMvvm Project is to use the provided project template. If you did not install via a package manager or the repository, follow the steps below to add the wizard. ### Add the custom wizard -Tho create a new QtMvvm project, you can use a custom wizard for QtCreator. You will have to add it to your computer once. To do this, you will have to copy the contents of the [`ProjectTemplate`](ProjectTemplate) folder to a location known by QtCreator (Pro Tip: Use [Kinolien's Gitzip](https://kinolien.github.io/gitzip/) to download that directory only). The locations can be found here: [Locating Wizards](https://doc.qt.io/qtcreator/creator-project-wizards.html#locating-wizards). If you are, for example, working on linux, create a new folder called `QtMvvm` inside of `$HOME/.config/QtProject/qtcreator/templates/wizards` and copy the contents there. After restarting QtCreator, the project template should appear in the `Applications` section of the new-dialog as `QtMvvm Application Project`. +If you did install the module as module you can skip this part. To create a new QtMvvm project, you can use a custom wizard for QtCreator. You will have to add it to your computer once. To do this, you will have to copy the contents of the [`ProjectTemplate`](ProjectTemplate) folder to a location known by QtCreator (Pro Tip: Use [Kinolien's Gitzip](https://kinolien.github.io/gitzip/) to download that directory only). The locations can be found here: [Locating Wizards](https://doc.qt.io/qtcreator/creator-project-wizards.html#locating-wizards). If you are, for example, working on linux, create a new folder called `QtMvvm` inside of `$HOME/.config/QtProject/qtcreator/templates/wizards` and copy the contents there. After restarting QtCreator, the project template should appear in the `Applications` section of the new-dialog as `QtMvvm Application Project`. ### Create and initialize the QtMvvm Project -Follow the setup to create the project. You can select the GUI-frontends you want to use, as well as additional features. After the project has been created, qpm dependencies need to be installed (Sadly, this cannot be done by the wizard). To install them, simply run `qpm install` inside of every sub-project: +Follow the setup to create the project. You can select the GUI-frontends you want to use, as well as additional features. After that you get a basic project skeleton with a simple CoreApp and a ViewModel, as well as the corresponding views. -```sh -cd MyProjectCore -qpm install -cd .. - -cd MyProjectWidgets -qpm install -cd .. - -cd MyProjectQuick -qpm install -cd .. -``` +For more Details on these classes, check the [Documentation](https://skycoder42.github.io/QtMvvm/). -Once that's done, the project should build and run successfully. (**Note:** Don't be irritated by the long build time. The qpm dependencies must be built initially, for all 3 projects. But since those don't change, only the first build/rebuilds take longer) +### Adding new ViewModels and Views +The most important part is to know how to add new ViewModels and Views. -### Adding new controls -Finally, you may need to add custom controls. Currently, there is no custom wizard for that, but I may try to create one as well. Creating a new control can be done by the following steps: +#### Create the ViewModel +- Add a new c++ class to your core project. Let it inherit from `QtMvvm::ViewModel` +- Make shure the Constructor has the following signature: `Q_INVOKABLE MyClass(QObject *parent);` +- See [`examples/mvvmcore/SampleCore/sampleviewmodel.h`](examples/mvvmcore/SampleCore/sampleviewmodel.h) for an example ViewModel -#### Create the control -- Add a new c++ class to your core project. Let it inherit from `Control` and your done with the core part. You can show the control by creating one and calling `Control::show` -- See [`MvvmExample/MvvmExampleCore/maincontrol.h`](MvvmExample/MvvmExampleCore/maincontrol.h) for an example control - -#### Create the widgets ui +#### Create the View for QtWidgets - Create a new widget class in your widgets gui project. -- In order to use it as widget for a control, you should name it accordingly: If your control is named `MyCustomControl`, the widgets name must start with `MyCustom` as well (e.g. `MyCustomWindow`, `MyCustomDialog`, etc.) -- Modify the constructor to look like this: `Q_INVOKABLE MyCustomWindow(Control *mControl, QWidget *parent = nullptr);` (with the name adjusted to your class) -- Create a member variable with your control type, e.g. `MyCustomControl *control;` -- In the constructors implementation, cast the `mControl` to your control class and assign it to `control` -- As final step, you need to register the widget. This can be done by adding the line `WidgetPresenter::registerWidget();` to your `main.cpp`, before calling `QApplication::exec` -- See [`MvvmExample/MvvmExampleWidgets/mainwindow.h`](MvvmExample/MvvmExampleWidgets/mainwindow.h) for an example widget +- In order to use it as widget for a viewmodel, you should name it accordingly: If your viewmodel is named `MyCustomViewModel`, the widgets name must start with `MyCustom` as well (e.g. `MyCustomWindow`, `MyCustomDialog`, `MyCustomView`, etc.) +- Modify the constructor to look like this: `Q_INVOKABLE MyCustomWindow(QtMvvm::ViewModel *viewModel, QWidget *parent = nullptr);` +- Create a member variable with your viewmodel type, e.g. `MyCustomViewModel *_viewModel;` +- In the constructors implementation, cast the `viewModel` to your viewmodel class and assign it to `_viewModel` +- As final step, you need to register the widget. This can be done by adding the line `WidgetPresenter::registerView();` to your `main.cpp`, before calling `QApplication::exec` +- See [`examples/mvvmwidgets/SampleWidgets/sampleview.h`](examples/mvvmwidgets/SampleWidgets/sampleview.h) for an example widget #### Create the quick ui -- *Optional:* Register the control for qml. This way autocomplete will work. Add the line `qmlRegisterUncreatableType("com.example.mvvmexample", 1, 0, "MyCustomControl", "Controls cannot be created!");` to your main cpp before creating the engine. +- *Optional:* Register the viewmodel for qml. This way autocomplete will work. Add the line `qmlRegisterUncreatableType("com.example.mvvmexample", 1, 0, "MyCustomViewModel", "ViewModels cannot be created!");` to your main cpp before creating the engine. - Create a new qml file inside of the folder `:/qml/views` (This is required to automatically find the view). -- In order to use it as view for a control, you should name it accordingly: If your control is named `MyCustomControl`, the views name must start with `MyCustom` as well (e.g. `MyCustomView`, `MyCustomActivity`, etc.) +- In order to use it as view for a viewmodel, you should name it accordingly: If your viewmodel is named `MyCustomViewModel`, the views name must start with `MyCustom` as well (e.g. `MyCustomView`, `MyCustomActivity`, etc.) - Add the following imports to the qml file: ```qml -import QtQuick 2.8 -import QtQuick.Controls 2.1 -import de.skycoder42.quickextras 1.0 -import de.skycoder42.qtmvvm.quick 1.0 +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import de.skycoder42.QtMvvm.Core 1.0 +import de.skycoder42.QtMvvm.Quick 1.0 import com.example.mvvmexample 1.0 //adjust to the module defined in your main.cpp ``` -- Add a property named control to the root element: `property MyCustomControl control: null` (If you did not register the control, use `var` instead of `MyCustomControl` as property type) +- Add a property named viewmodel to the root element: `property MyCustomViewModel viewmodel: null` (If you did not register the viewmodel, use `var` instead of `MyCustomViewModel` as property type) - Add a presenter progress element to the root element. This way lazy loading views will show a progress: `PresenterProgress {}` -- See [`MvvmExample/MvvmExampleQuick/MainView.qml`](MvvmExample/MvvmExampleQuick/MainView.qml) for an example widget +- See [`examples/mvvmquick/SampleQuick/SampleView.qml`](examples/mvvmquick/SampleQuick/SampleView.qml) for an example view ## Understanding how QtMvvm works -The general idea is the following: You create controls in your core project, which represent uis. They typically contain all the properties relevant for the ui, methods (slots) that can be called (e.g. on a button click) and signals to inform the ui about changes and other events. The controls have show and close methods, just like widgets. Thus, you can use the as ui "placeholders". Of course, They only contain the ui logic, not the actual uis. +The general idea is the following: You create viewmodels in your core project, which represent uis. They typically contain all the properties relevant for the ui, methods (slots) that can be called (e.g. on a button click) and signals to inform the ui about changes and other events. Thus, you can use the as ui "placeholders". Of course, They only contain the ui logic, not the actual uis. -The CoreApp is what's reponsible for managing those controls. showing or closing a control, as well as messages (alert dialogs) are all controlled by the coreapp. The coreapp uses a so called presenter to create the actual uis. The presenters are located in the ui projects and they are the most complicated part. Their main task is to find ui implementations for controls, and manage the life cycle as well as the presentation of those real uis. The presenters are where the decision is made, how a specific ui should be shown. +The CoreApp is what's reponsible for managing those viewmodels. Showing a viewmodel, as well as messages (alert dialogs) are all controlled by the coreapp. The coreapp uses a so called presenters to create the actual uis. The presenters are located in the ui projects and they are the most complicated part. Their main task is to find ui implementations for viewmodel (called views), and manage the life cycle as well as the presentation of those real views. The presenters are where the decision is made, how a specific view should be shown. -The uis are whatever you need to create actual uis. This depends on the presenter used, since the presenter selects the uis. Each ui type has their own way to create those uis, but the all have in commmon, that the uis themselves do not control the application. When a control gets shown, a new ui is created and the control passed to it. Once the control was closed (via code or via the gui itself), the gui gets deleted again. Uis are temporary and should only use the control to interact with other -parts of the applications. +The views are whatever you need to create actual uis. This depends on the presenter used, since the presenter selects the views. Each ui type has their own way to create those views, but they all have in commmon that the views themselves do not control the application. When a viewmodel gets shown, a new view is created and the viewmodel passed to it. Once the views was closed, the view and the viewmodel get deleted again. Views and ViewModels are temporary and should only use the viewmodel to interact with other parts of the application. ### A side note on presenters -To create a presenter, the `IPresenter` must be implemented. Presenters can become quite complicated, but they are the thing you need to modify if you want to present views in a different way from the ones supported. Currently, the presenters can do the following: +To create a presenter, the `QtMvvm::IPresenter` must be implemented and provided via the `ServiceRegistry`. Presenters can become quite complicated, but they are the thing you need to modify if you want to present views in a different way from the ones supported. Currently, the presenters can do the following: -- Widgets Presenter (Presenter of `de.skycoder42.qtmvvm.widgets`) - - Present controls as widgets (or windows, dialogs, depending on type and parent) - - Parent-Aware: If your control has a parent and shows a ui, the childs ui will be a child of the parents ui as well +- Widgets Presenter + - Present viewmodels as widgets (or windows, dialogs, depending on type and parent) + - Parent-Aware: If your viewmodel has a parent and shows a ui, the childs ui will be a child of the parents ui as well - Automatically detect QDockWidgets and place them as dock inside a parents QMainWindow - Automatically detect QMdiSubWindows and place them in a parents QMdiArea - Allows windows to implement `IPresentingWidget`. This way a window can handle the presentation of it's children without modifying the presenter - Allows to register custom input widgets for input dialogs (and other parts, like the settings. See `InputWidgetFactory`) -- Quick Presenter (Presenter of `de.skycoder42.qtmvvm.quick`) +- Quick Presenter - Consists of a c++ part and a qml part - The c++ part is responsible for finding views for controls and creating them, but **not** for the actual presentation. This is done by the qml presenter - Allows to register custom input views for input dialogs (and other parts, like the settings. See `InputViewFactory`) - - The qml presenter can be any qml type. It must provide the correct methods (as seen in `AppBase`) and register itself with `QuickPresenter.qmlPresenter = root` - - The `AppBase` or `App` qml types automatically register themselves as presenter and perform the presentations - - Supports Items as new fullscreen pages inside a stack view + - The qml presenter can be any qml type. It must provide the correct methods (as seen in `QtMvvmApp`) and register itself with `Component.onCompleted: QuickPresenter.qmlPresenter = self` + - The `QtMvvmApp` qml types automatically register themselves as presenter and perform the presentations + - Supports Items as new fullscreen pages inside a stack view, as drawer or as tabs - Supports Popups as modal dialogs - -## Installation -All those modules are available as qpm packages. To install any of them: - -1. Install qpm (See [GitHub - Installing](https://github.com/Cutehacks/qpm/blob/master/README.md#installing), for **windows** see below) -2. In your projects root directory, run `qpm install de.skycoder42.qtmvvm...` (The package you need) -3. Include qpm to your project by adding `include(vendor/vendor.pri)` to your `.pro` file - -Check their [GitHub - Usage for App Developers](https://github.com/Cutehacks/qpm/blob/master/README.md#usage-for-app-developers) to learn more about qpm. - -**Important for Windows users:** QPM Version *0.10.0* (the one you can download on the website) is currently broken on windows! It's already fixed in master, but not released yet. Until a newer versions gets released, you can download the latest dev build from here: -- https://storage.googleapis.com/www.qpm.io/download/latest/windows_amd64/qpm.exe -- https://storage.googleapis.com/www.qpm.io/download/latest/windows_386/qpm.exe - + - ## Icons In many of the UI projects default icons are used for the views (if no icon theme is present). They are taken from: diff --git a/doc/binding.dox b/doc/binding.dox new file mode 100644 index 0000000..1f2ae7b --- /dev/null +++ b/doc/binding.dox @@ -0,0 +1,70 @@ +/*! +@class QtMvvm::Binding + +This class is returned by the bind() function as a handle to a binding. It can be used to check +if the binding is valid and can remove a binding again. + +@note You don't need to keep the Binding as reference. The binding will stay even if you don't +hold any references to it. This class serves as a passive watcher over a binding. + +@sa QtMvvm::bind +*/ + +/*! +@property QtMvvm::Binding::valid + +@default{`false`} + +A binding is considered valid is if has successfully been set up and has not been unbound yet. +A valid binding means view and viewModel are valid can the properties could be found. + +@accessors{ + @readAc{isValid()} + @finalAc +} + +@sa Binding::unbind, bind() +*/ + +/*! +@fn QtMvvm::bind(QObject*, const char*, QObject*, const char*, Binding::BindingDirection, const char*, const char*) + +@param viewModel The object in the role of a viewmodel +@param viewModelProperty The property of the viewmodel to use in the binding +@param view The object in the role of a view +@param viewProperty The property of the view to use in the binding +@param type The type/direction of binding to create +@param viewModelChangeSignal An alternative signal to be used instead of the viewModelProperty +notify signal to detect property changes +@param viewChangeSignal An alternative signal to be used instead of the viewProperty notify +signal to detect property changes +@returns A handle to the newly created binding, or an invalid handle on errors + +The methods creates a binding similar to the QML bindings, but it is possible to create two-way +bindings with this method as well. A binding means that whenever a property changes, the property +it is bound to is updated to the changed value as well. + +@code{.cpp} +//sample usage +QtMvvm::bind(this->viewModel, "name", // A viewmodel that has a property named "name" + ui->nameLabel, "text"); // A view (for example a QLabel) that has a property named "text" +//any changes to "name" will update "text" and the other way around +@endcode + +To control the directions, i.e. which properties should trigger an update on changes and which +only get updated, use the `type` parameter. + +@attention Unlike the QML variant, this binding **does not handle binding loops**. THis means if +you use a two way binding, make shure change signals are only emitted when the value does actually +change. Otherwise your application is caught up in a binding loop and will eventually crash. Using +two seperate one way bindings will not prevent this. + +If the properties you want to use don't have a change signal or you don't want to use that signal, +you can use the `viewModelChangeSignal` and `viewChangeSignal` to overwrite the signal to be used. +Please note that you must specify the full signal signature. For example, the signatur of the +signal `void foo(const QString &bar);` has the signature `foo(QString)`. See QMetaMethod for more +details. If you leave the parameter out or set it to `nullptr` (or `{}`, an invalid meta method) +the notify signals of the properties are used. + +@sa Binding +*/ diff --git a/doc/coreapp.dox b/doc/coreapp.dox new file mode 100644 index 0000000..8ba8815 --- /dev/null +++ b/doc/coreapp.dox @@ -0,0 +1,49 @@ +/*! +@class QtMvvm::CoreApp + +Every QtMvvm Application needs a core application that performs the application setup and +manages the ViewModels in order to present them. If you want to use QtMvvm, you need to perform +the following steps: + +1. Create a custom App class in your core project and extend QtMvvm::CoreApp +2. Implement the CoreApp::startApp method (and possibly other virtual methods) +3. In your *GUI* Project, register the App by using the #QTMVVM_REGISTER_CORE_APP macro + +Simple example for a custom CoreApp: +@code{.cpp} +class MyApp : public QtMvvm::CoreApp +{ + Q_OBJECT + +public: + explicit MyApp(QObject *parent = nullptr) : + CoreApp(parent) + {} + +protected: + int startApp(const QStringList &arguments) override { + show(); + return EXIT_SUCCESS; + } +}; + +//optional: overwrite the coreApp macro: +#undef coreApp +#define coreApp static_cast(QtMvvm::CoreApp::instance()) +@endcode + +Then in the GUI project add to, for example, the `main.cpp`: +@code{.cpp} +#include "myapp.h" + +QTMVVM_REGISTER_CORE_APP(MyApp) + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); //registerApp() is called from here (and thus performRegistrations()) + + //... + + return app.exec(); // bootApp() is called as soon as the eventloop starts (and thus startApp()) +} +@endcode +*/ diff --git a/doc/doc.pro b/doc/doc.pro index daacfaa..fa8b33f 100644 --- a/doc/doc.pro +++ b/doc/doc.pro @@ -7,7 +7,7 @@ OTHER_FILES += Doxyfile \ *.dox \ snippets/*.cpp \ images/* - + system($$QMAKE_MKDIR $$shell_quote($$shell_path($$OUT_PWD/qtmvvm))) docTarget.target = doxygen diff --git a/doc/images/sample_basic_quick.gif b/doc/images/sample_basic_quick.gif new file mode 100644 index 0000000..b400dbb Binary files /dev/null and b/doc/images/sample_basic_quick.gif differ diff --git a/doc/images/sample_basic_widgets.gif b/doc/images/sample_basic_widgets.gif new file mode 100644 index 0000000..f2d5eeb Binary files /dev/null and b/doc/images/sample_basic_widgets.gif differ diff --git a/examples/mvvmquick/SampleQuick/TabView.qml b/examples/mvvmquick/SampleQuick/TabView.qml index 014de1d..f0736d6 100644 --- a/examples/mvvmquick/SampleQuick/TabView.qml +++ b/examples/mvvmquick/SampleQuick/TabView.qml @@ -1,5 +1,6 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 +import QtQuick.Controls.Material 2.3 import QtQuick.Layouts 1.3 import de.skycoder42.QtMvvm.Core 1.0 import de.skycoder42.QtMvvm.Quick 1.0 @@ -35,6 +36,7 @@ Page { Layout.columnSpan: 2 Layout.fillWidth: true onCurrentIndexChanged: swipe.setCurrentIndex(currentIndex) + Material.background: Material.primary TabButton { text: "+" diff --git a/examples/mvvmquick/SampleQuick/main.cpp b/examples/mvvmquick/SampleQuick/main.cpp index eb19b75..157fbdf 100644 --- a/examples/mvvmquick/SampleQuick/main.cpp +++ b/examples/mvvmquick/SampleQuick/main.cpp @@ -23,6 +23,7 @@ int main(int argc, char *argv[]) qputenv("QML2_IMPORT_PATH", QML_PATH); #endif + QIcon::setThemeSearchPaths({}); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); qDebug() << QQuickStyle::availableStyles() << QQuickStyle::name(); diff --git a/src/mvvmcore/binding.h b/src/mvvmcore/binding.h index 9f79174..325e45b 100644 --- a/src/mvvmcore/binding.h +++ b/src/mvvmcore/binding.h @@ -10,43 +10,53 @@ namespace QtMvvm { class BindingPrivate; +//! A handle to a multidirectional binding between properties class Q_MVVMCORE_EXPORT Binding { Q_GADGET - Q_PROPERTY(bool valid READ isValid CONSTANT FINAL) + //! Specifies whether the binding is a valid and active binding + Q_PROPERTY(bool valid READ isValid FINAL) public: + //! Flags to control how to bind the properties enum BindingDirectionFlag { - SingleInit = 0x01, - OneWayToView = (0x02 | SingleInit), - OneWayToViewModel = 0x04, - TwoWay = (OneWayToView | OneWayToViewModel) + SingleInit = 0x01, //!< Only initialize the view by settings it to the viewmodels value on creation + OneWayToView = (0x02 | SingleInit), //!< All changes done to the viewmodel are propagated to the view + OneWayToViewModel = 0x04, //!< All changes done to the view are propagated to the viewmodel + TwoWay = (OneWayToView | OneWayToViewModel) //!< All changes on both sides are propagated to the other one. }; Q_DECLARE_FLAGS(BindingDirection, BindingDirectionFlag) Q_FLAG(BindingDirection) Binding(); + //! @private Binding(QPointer d_ptr); ~Binding(); + //! @readAcFn{Binding::valid} bool isValid() const; - void unbind(); + + //! Remove the binding by disconnecting all change signals + Q_INVOKABLE void unbind(); private: QPointer d; }; +//! Create a multidirectional binding between properties Q_MVVMCORE_EXPORT Binding bind(QObject *viewModel, const char *viewModelProperty, QObject *view, const char *viewProperty, Binding::BindingDirection type = Binding::TwoWay, const char *viewModelChangeSignal = nullptr, const char *viewChangeSignal = nullptr); +//! @copydoc QtMvvm::bind(QObject*, const char*, QObject*, const char*, Binding::BindingDirection, const char*, const char*) Q_MVVMCORE_EXPORT Binding bind(QObject *viewModel, const QMetaProperty &viewModelProperty, QObject *view, const QMetaProperty &viewProperty, Binding::BindingDirection type = Binding::TwoWay, const char *viewModelChangeSignal = nullptr, const char *viewChangeSignal = nullptr); +//! @copydoc QtMvvm::bind(QObject*, const char*, QObject*, const char*, Binding::BindingDirection, const char*, const char*) Q_MVVMCORE_EXPORT Binding bind(QObject *viewModel, const QMetaProperty &viewModelProperty, QObject *view, const QMetaProperty &viewProperty, Binding::BindingDirection type = Binding::TwoWay, diff --git a/src/mvvmcore/coreapp.h b/src/mvvmcore/coreapp.h index 6a8815d..7940062 100644 --- a/src/mvvmcore/coreapp.h +++ b/src/mvvmcore/coreapp.h @@ -1,6 +1,8 @@ #ifndef QTMVVM_COREAPP_H #define QTMVVM_COREAPP_H +#include + #include #include #include @@ -14,37 +16,52 @@ class QCommandLineParser; namespace QtMvvm { class CoreAppPrivate; +//! A logicaly application object to drive the mvvm application from the core part class Q_MVVMCORE_EXPORT CoreApp : public QObject { Q_OBJECT public: + //! Default Constructor explicit CoreApp(QObject *parent = nullptr); ~CoreApp(); + //! Returns the current CoreApp instance static CoreApp *instance(); + //! Disables the automatic startup of the CoreApp static void disableAutoBoot(); + //! Registers this instance as the CoreApp instance void registerApp(); + //! Show a new ViewModel by its type template static inline void show(const QVariantHash ¶ms = {}); + //! Show a new ViewModel by its name static void show(const char *viewModelName, const QVariantHash ¶ms = {}); + //! Show a new ViewModel by its metaobject static void show(const QMetaObject *viewMetaObject, const QVariantHash ¶ms = {}); + //! Show a basic dialog static MessageResult *showDialog(const MessageConfig &config); public Q_SLOTS: + //! Boots up the app and starts the mvvm presenting void bootApp(); Q_SIGNALS: + //! Is emitted once the app has successfully started void appStarted(); protected: + //! Is called as part of the app registration, before any view setups virtual void performRegistrations(); + //! Is called as part of the app start/boot, after all the view setups to start the app virtual int startApp(const QStringList &arguments) = 0; + //! Is called when the app is about to quit virtual void closeApp(); + //! Can be used to automatically handle help and version commandline arguments bool autoParse(QCommandLineParser &parser, const QStringList &arguments); private: @@ -63,13 +80,17 @@ inline void CoreApp::show(const QVariantHash ¶ms) } +//! Registers you custom CoreApp class as CoreApp to be used #define QTMVVM_REGISTER_CORE_APP(T) \ static void _setup_ ## T ## _hook() { \ + static_assert(std::is_base_of::value, "QTMVVM_REGISTER_CORE_APP must be used with a class that extends QtMvvm::CoreApp"); \ auto app = new T(nullptr); \ app->registerApp(); \ } \ Q_COREAPP_STARTUP_FUNCTION(_setup_ ## T ## _hook) +//! A define as shortcut to the CoreApp. Can be redefined to cast to your custom class type #define coreApp QtMvvm::CoreApp::instance() +//!@file coreapp.h The Header that defines the QtMvvm::CoreApp class #endif // QTMVVM_COREAPP_H diff --git a/src/mvvmcore/qtmvvmcore_global.h b/src/mvvmcore/qtmvvmcore_global.h index 818ac80..9242149 100644 --- a/src/mvvmcore/qtmvvmcore_global.h +++ b/src/mvvmcore/qtmvvmcore_global.h @@ -11,8 +11,10 @@ # define Q_MVVMCORE_EXPORT Q_DECL_IMPORT #endif +//! The primary namespace of the QtMvvm library namespace QtMvvm { +//! Registers QVariant converters from QObject to an interface type registered with Q_DECLARE_INTERFACE template inline void registerInterfaceConverter() { QMetaType::registerConverter([](QObject *o) {