diff --git a/CMakeLists.txt b/CMakeLists.txt index 46b0b4ac..a3843076 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ option(NO_FILE_ITEM_ACTION_PLUGIN "whether building the file item action plugin option(NO_MODEL "whether building models should be skipped, implies NO_TRAY" OFF) option(NO_WIDGETS "whether building widgets should be skipped, implies NO_TRAY" OFF) option(NO_PLASMOID "whether building the Plasmoid for the Plasma desktop should be skipped" "${PLASMOID_DISABLED_BY_DEFAULT}") +option(QUICK_GUI "enables/disables building the experimental Qt Quick GUI (disabled by default)" OFF) # allow using non-default configuration set(CONFIGURATION_PACKAGE_SUFFIX "" CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities") diff --git a/tray/CMakeLists.txt b/tray/CMakeLists.txt index 9b1192eb..30a3c505 100644 --- a/tray/CMakeLists.txt +++ b/tray/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR) # metadata set(META_PROJECT_TYPE application) set(META_APP_NAME "Syncthing Tray") +set(META_HAS_QUICK_GUI ON) +set(META_USE_QQC2 ON) # use testfiles directory from syncthingconnector set(META_SRCDIR_REFS "${CMAKE_CURRENT_SOURCE_DIR}\n${CMAKE_CURRENT_SOURCE_DIR}/../syncthingconnector") @@ -138,6 +140,11 @@ endif () # apply basic configuration include(BasicConfig) +if (QUICK_GUI) + find_package(${PACKAGE_NAMESPACE_PREFIX}qtquickforkawesome${CONFIGURATION_PACKAGE_SUFFIX_QTFORKAWESOME} 0.1.0 REQUIRED) + use_qt_quick_fork_awesome() +endif () + # add an option to unify left- and right-click context menus useful on Mac OS if (APPLE) set(UNIFY_TRAY_MENUS_BY_DEFAULT ON) @@ -173,8 +180,14 @@ Name=Restart Syncthing (local instance)\n\ Exec=${SYNCTHINGCTL_TARGET_NAME} restart") set(DESKTOP_FILE_ADDITIONAL_ENTRIES "${DESKTOP_FILE_ADDITIONAL_ENTRIES}${ACTIONS}\n") -# include modules to apply configuration +# configure Qt GUI GUI include(QtGuiConfig) +if (QUICK_GUI) + list(APPEND ADDITIONAL_QT_MODULES Svg) + list(APPEND ADDITIONAL_QT_REPOS svg) +endif () + +# include further modules to apply configuration include(QtConfig) include(WindowsResources) include(AppTarget) @@ -182,6 +195,17 @@ include(ShellCompletion) include(Doxygen) include(ConfigHeader) +# add Qml module +if (QUICK_GUI) + qt_add_qml_module(${META_TARGET_NAME} + URI "Main" + VERSION 1.0 + NO_PLUGIN + QML_FILES gui/qml/Main.qml + RESOURCE_PREFIX "/qt/qml/" + ) +endif () + # create desktop file using previously defined meta data add_desktop_file() diff --git a/tray/application/main.cpp b/tray/application/main.cpp index 205cbdce..b1c04181 100644 --- a/tray/application/main.cpp +++ b/tray/application/main.cpp @@ -1,3 +1,7 @@ +#ifdef GUI_QTQUICK +#define QT_UTILITIES_GUI_QTQUICK +#endif + #include "./singleinstance.h" #include "../gui/trayicon.h" @@ -37,6 +41,21 @@ #include #include +#ifdef GUI_QTQUICK +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#endif + #include #ifdef Q_OS_ANDROID @@ -211,6 +230,9 @@ static int runApplication(int argc, const char *const *argv) #endif parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), +#ifdef GUI_QTQUICK + &qtConfigArgs.qtQuickGuiArg(), +#endif #ifdef SYNCTHINGTRAY_USE_LIBSYNCTHING &cliArg, &syncthingArg, #endif @@ -227,6 +249,47 @@ static int runApplication(int argc, const char *const *argv) return EXIT_SUCCESS; } +#ifdef GUI_QTQUICK + if (qtConfigArgs.qtQuickGuiArg().isPresent()) { + qputenv("QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT", "1"); + SET_QT_APPLICATION_INFO; + auto app = QApplication(argc, const_cast(argv)); + auto &settings = Settings::values(); + Settings::restore(); + settings.qt.disableNotices(); + settings.qt.apply(); + qtConfigArgs.applySettings(true); + qtConfigArgs.applySettingsForQuickGui(); + + auto engine = QQmlApplicationEngine(); + auto renderer = QtForkAwesome::Renderer(); + auto context = engine.rootContext(); + auto connection = Data::SyncthingConnection(); + auto dirModel = Data::SyncthingDirectoryModel(connection); + auto devModel = Data::SyncthingDeviceModel(connection); + auto changesModel = Data::SyncthingRecentChangesModel(connection); + networkAccessManager().setParent(&app); + connection.connect(settings.connection.primary); + context->setContextProperty(QStringLiteral("connection"), &connection); + context->setContextProperty(QStringLiteral("dirModel"), &dirModel); + context->setContextProperty(QStringLiteral("devModel"), &devModel); + context->setContextProperty(QStringLiteral("changesModel"), &changesModel); + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreated, &app, + [](QObject *obj, const QUrl &objUrl) { + if (!obj) { + std::cerr << "Unable to load " << objUrl.toString().toStdString() << '\n'; + QCoreApplication::exit(EXIT_FAILURE); + } + }, + Qt::QueuedConnection); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); + engine.addImageProvider(QStringLiteral("fa"), new QtForkAwesome::QuickImageProvider(renderer)); + engine.loadFromModule("Main", "Main"); + return app.exec(); + } +#endif + // quit unless Qt Widgets GUI should be shown if (!qtConfigArgs.qtWidgetsGuiArg().isPresent()) { return EXIT_SUCCESS; diff --git a/tray/gui/qml/Main.qml b/tray/gui/qml/Main.qml new file mode 100644 index 00000000..db9a0d21 --- /dev/null +++ b/tray/gui/qml/Main.qml @@ -0,0 +1,206 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +ApplicationWindow { + id: window + visible: true + width: 700 + height: 500 + header: ToolBar { + RowLayout { + anchors.fill: parent + anchors.leftMargin: drawer.effectiveWidth + Label { + text: pageStack.currentPage.title + elide: Label.ElideRight + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + Layout.fillWidth: true + } + } + } + + readonly property bool inPortrait: window.width < window.height + readonly property int spacing: 7 + readonly property string faUrlBase: "image://fa/" + + Dialog { + id: aboutDialog + modal: true + focus: true + parent: null + anchors.centerIn: parent + standardButtons: Dialog.Ok + width: 300 + title: qsTr("About") + contentItem: ColumnLayout { + Image { + readonly property double size: 128 + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: size + Layout.preferredHeight: size + source: "qrc:/icons/hicolor/scalable/app/syncthingtray.svg" + sourceSize.width: size + sourceSize.height: size + } + Label { + text: Qt.application.name + Layout.alignment: Qt.AlignHCenter + } + Label { + text: "Version " + Qt.application.version + Layout.alignment: Qt.AlignHCenter + } + Label { + text: "Developed by " + Qt.application.organization + Layout.alignment: Qt.AlignHCenter + } + } + } + + Drawer { + id: drawer + width: Math.min(0.66 * window.width, 200) + height: window.height + interactive: inPortrait || window.width < 600 + modal: interactive + position: initialPosition + visible: !interactive + + readonly property double initialPosition: interactive ? 0 : 1 + readonly property int effectiveWidth: !interactive ? width : 0 + + ListView { + id: drawerListView + anchors.fill: parent + footer: ItemDelegate { + width: parent.width + text: Qt.application.version + icon.source: window.faUrlBase + "info-circle" + onClicked: aboutDialog.visible = true + } + footerPositioning: ListView.OverlayFooter + model: ListModel { + ListElement { + name: qsTr("Folders") + iconName: "folder" + } + ListElement { + name: qsTr("Devices") + iconName: "sitemap" + } + ListElement { + name: qsTr("Recent changes") + iconName: "history" + } + ListElement { + name: qsTr("Syncthing") + iconName: "syncthing" + } + ListElement { + name: qsTr("App settings") + iconName: "cog" + } + } + delegate: ItemDelegate { + text: name + icon.source: window.faUrlBase + iconName + width: parent.width + onClicked: { + drawerListView.currentIndex = index + drawer.position = drawer.initialPosition + } + } + ScrollIndicator.vertical: ScrollIndicator { } + } + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.leftMargin: drawer.effectiveWidth + + StackLayout { + id: pageStack + anchors.fill: parent + currentIndex: drawerListView.currentIndex + + readonly property Page currentPage: children[currentIndex] + + Page { + id: dirsPage + title: qsTr("Folder overview") + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + anchors.fill: parent + model: DelegateModel { + model: dirModel + delegate: ItemDelegate { + width: parent.width + text: name + } + } + ScrollIndicator.vertical: ScrollIndicator { } + } + } + + Page { + id: devsPage + title: qsTr("Device overview") + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + anchors.fill: parent + model: DelegateModel { + model: devModel + delegate: ItemDelegate { + width: parent.width + text: name + } + } + ScrollIndicator.vertical: ScrollIndicator { } + } + } + + Page { + id: changesPage + title: qsTr("Recent changes") + Layout.fillWidth: true + Layout.fillHeight: true + ListView { + anchors.fill: parent + model: DelegateModel { + model: changesModel + delegate: ItemDelegate { + width: parent.width + text: path + } + } + ScrollIndicator.vertical: ScrollIndicator { } + } + } + + Page { + id: syncthingPage + title: qsTr("Syncthing") + Layout.fillWidth: true + Layout.fillHeight: true + Label { + text: "TODO: webview" + } + } + + Page { + id: settingsPage + title: qsTr("App settings") + Layout.fillWidth: true + Layout.fillHeight: true + Label { + text: "TODO: settings UI" + } + } + } + } +}