From 6e465f9dbe48836c7bcc44107a887673d3e2952e Mon Sep 17 00:00:00 2001 From: James Turner Date: Sat, 31 Aug 2019 23:40:21 +0100 Subject: [PATCH] FGQCanvas: Multi-window support (for RPi4) To support EGLFS on the RPi4 dual outputs, enable multiple windows within a single process. --- utils/fgqcanvas/WindowData.cpp | 86 +++++++++++++++ utils/fgqcanvas/WindowData.h | 46 ++++++++ utils/fgqcanvas/applicationcontroller.cpp | 101 +++++++++++++----- utils/fgqcanvas/applicationcontroller.h | 16 +-- utils/fgqcanvas/canvasconnection.cpp | 13 +++ utils/fgqcanvas/canvasconnection.h | 10 ++ utils/fgqcanvas/canvaspainteddisplay.cpp | 1 - utils/fgqcanvas/configs/example_config.json | 32 ++++++ .../configs/multi_window_config.json | 38 +++++++ utils/fgqcanvas/fgcanvas.pro | 5 +- utils/fgqcanvas/fgqcanvas_resources.qrc | 2 +- utils/fgqcanvas/main.cpp | 56 +--------- .../qml/{mainMenu.qml => Window.qml} | 29 ++++- 13 files changed, 341 insertions(+), 94 deletions(-) create mode 100644 utils/fgqcanvas/WindowData.cpp create mode 100644 utils/fgqcanvas/WindowData.h create mode 100644 utils/fgqcanvas/configs/example_config.json create mode 100644 utils/fgqcanvas/configs/multi_window_config.json rename utils/fgqcanvas/qml/{mainMenu.qml => Window.qml} (61%) diff --git a/utils/fgqcanvas/WindowData.cpp b/utils/fgqcanvas/WindowData.cpp new file mode 100644 index 000000000..516a4e9a0 --- /dev/null +++ b/utils/fgqcanvas/WindowData.cpp @@ -0,0 +1,86 @@ +#include "WindowData.h" + +#include +#include +#include + +#include "jsonutils.h" + +WindowData::WindowData(QObject *parent) : QObject(parent) +{ + +} + +QJsonObject WindowData::saveState() const +{ + QJsonObject json; + json["rect"] = rectToJsonArray(m_windowRect); + if (!m_screenName.isEmpty()) { + json["screen"] = m_screenName; + } + if (!m_title.isEmpty()) { + json["title"] = m_title; + } + // support frameless option here? + json["state"] = static_cast(m_state); + return json; +} + +bool WindowData::restoreState(QJsonObject state) +{ + m_windowRect = jsonArrayToRect(state.value("rect").toArray()); + emit windowRectChanged(m_windowRect); + + if (state.contains("screen")) { + m_screenName = state.value("screen").toString(); + } else { + m_screenName.clear(); + } + + if (state.contains("title")) { + m_title = state.value("title").toString(); + } + + if (state.contains("state")) { + m_state = static_cast(state.value("state").toInt()); + } + + return true; +} + +QRect WindowData::windowRect() const +{ + return m_windowRect; +} + +QScreen *WindowData::screen() const +{ + if (m_screenName.isEmpty()) + return nullptr; + + QStringList screenNames; + Q_FOREACH(auto s, qApp->screens()) { + if (s->name() == m_screenName) { + return s; + } + screenNames.append(s->name()); + } + + qWarning() << "couldn't find a screen with name:" << m_screenName; + qWarning() << "Available screens:" << screenNames.join(", "); + return nullptr; +} + +void WindowData::setWindowState(Qt::WindowState ws) +{ + m_state = ws; +} + +void WindowData::setWindowRect(QRect windowRect) +{ + if (m_windowRect == windowRect) + return; + + m_windowRect = windowRect; + emit windowRectChanged(m_windowRect); +} diff --git a/utils/fgqcanvas/WindowData.h b/utils/fgqcanvas/WindowData.h new file mode 100644 index 000000000..ff5b6472a --- /dev/null +++ b/utils/fgqcanvas/WindowData.h @@ -0,0 +1,46 @@ +#ifndef WINDOWDATA_H +#define WINDOWDATA_H + +#include +#include +#include + +class QScreen; + +class WindowData : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QRect windowRect READ windowRect WRITE setWindowRect NOTIFY windowRectChanged) +public: + explicit WindowData(QObject *parent = nullptr); + + QJsonObject saveState() const; + bool restoreState(QJsonObject state); + + QRect windowRect() const; + QScreen* screen() const; + + Qt::WindowState windowState() const + { return m_state; } + + void setWindowState(Qt::WindowState ws); + + QString title() const + { return m_title; } +signals: + + void windowRectChanged(QRect windowRect); + +public slots: + + void setWindowRect(QRect windowRect); + +private: + QRect m_windowRect; + Qt::WindowState m_state = Qt::WindowNoState; + QString m_screenName; + QString m_title; +}; + +#endif // WINDOWDATA_H diff --git a/utils/fgqcanvas/applicationcontroller.cpp b/utils/fgqcanvas/applicationcontroller.cpp index fbbce2f2e..ce8dc4c3a 100644 --- a/utils/fgqcanvas/applicationcontroller.cpp +++ b/utils/fgqcanvas/applicationcontroller.cpp @@ -36,9 +36,12 @@ #include #include #include +#include +#include #include "jsonutils.h" #include "canvasconnection.h" +#include "WindowData.h" ApplicationController::ApplicationController(QObject *parent) : QObject(parent) @@ -72,18 +75,6 @@ ApplicationController::~ApplicationController() delete m_netAccess; } -void ApplicationController::setWindow(QWindow *window) -{ - m_window = window; -} - -void ApplicationController::restoreWindowState() -{ - if (!m_window) - return; - m_window->setWindowState(m_windowState); -} - void ApplicationController::loadFromFile(QString path) { if (!QFile::exists(path)) { @@ -104,6 +95,42 @@ void ApplicationController::setDaemonMode() m_daemonMode = true; } +void ApplicationController::createWindows() +{ + if (m_windowList.empty()) { + defineDefaultWindow(); + } + + for (int index = 0; index < m_windowList.size(); ++index) { + auto wd = m_windowList.at(index); + QQuickView* qqv = new QQuickView; + qqv->rootContext()->setContextProperty("_application", this); + qqv->rootContext()->setContextProperty("_windowNumber", index); + qqv->setResizeMode(QQuickView::SizeRootObjectToView); + qqv->setSource(QUrl{"qrc:///qml/Window.qml"}); + qqv->setTitle(wd->title()); + + if (m_daemonMode) { + qqv->setScreen(wd->screen()); + qqv->setGeometry(wd->windowRect()); + qqv->setWindowState(wd->windowState()); + } else { + // interactive mode, restore window size etc + + } + + qqv->show(); + } +} + +void ApplicationController::defineDefaultWindow() +{ + auto w = new WindowData(this); + w->setWindowRect(QRect{0, 0, 1024, 768}); + m_windowList.append(w); + emit windowListChanged(); +} + void ApplicationController::save(QString configName) { QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); @@ -364,6 +391,11 @@ QQmlListProperty ApplicationController::activeCanvases() return QQmlListProperty(this, m_activeCanvases); } +QQmlListProperty ApplicationController::windowList() +{ + return QQmlListProperty(this, m_windowList); +} + QNetworkAccessManager *ApplicationController::netAccess() const { return m_netAccess; @@ -371,6 +403,9 @@ QNetworkAccessManager *ApplicationController::netAccess() const bool ApplicationController::showUI() const { + if (m_daemonMode) + return false; + if (m_blockUIIdle) return true; @@ -490,10 +525,12 @@ QByteArray ApplicationController::saveState(QString name) const } json["canvases"] = canvases; - if (m_window) { - json["window-rect"] = rectToJsonArray(m_window->geometry()); - json["window-state"] = static_cast(m_window->windowState()); + + QJsonArray windows; + Q_FOREACH (auto w, m_windowList) { + windows.append(w->saveState()); } + json["windows"] = windows; // background color? @@ -509,18 +546,29 @@ void ApplicationController::restoreState(QByteArray bytes) QJsonDocument jsonDoc = QJsonDocument::fromJson(bytes); QJsonObject json = jsonDoc.object(); - if (m_window) { - QRect r = jsonArrayToRect(json.value("window-rect").toArray()); - if (r.isValid()) { - m_window->setGeometry(r); - } - - // we have to cache the state becuase it seems to be ignored - // before show is called(); - m_windowState = static_cast(json.value("window-state").toInt()); - m_window->setWindowState(m_windowState); + // clear windows + Q_FOREACH(auto w, m_windowList) { + w->deleteLater(); + } + m_windowList.clear(); + + for (auto w : json.value("windows").toArray()) { + auto wd = new WindowData(this); + m_windowList.append(wd); + wd->restoreState(w.toObject()); + } + + if (m_windowList.isEmpty()) { + // check for previous single-window data + auto w = new WindowData(this); + if (json.contains("window-rect")) { + w->setWindowRect(jsonArrayToRect(json.value("window-rect").toArray())); + } + if (json.contains("window-state")) { + w->setWindowState(static_cast(json.value("window-state").toInt())); + } + m_windowList.append(w); } - // background color for (auto c : json.value("canvases").toArray()) { auto cc = new CanvasConnection(this); @@ -533,6 +581,7 @@ void ApplicationController::restoreState(QByteArray bytes) cc->reconnect(); } + emit windowListChanged(); emit activeCanvasesChanged(); } diff --git a/utils/fgqcanvas/applicationcontroller.h b/utils/fgqcanvas/applicationcontroller.h index 052d8920a..307bcee71 100644 --- a/utils/fgqcanvas/applicationcontroller.h +++ b/utils/fgqcanvas/applicationcontroller.h @@ -27,6 +27,7 @@ class CanvasConnection; class QWindow; class QTimer; +class WindowData; class ApplicationController : public QObject { @@ -42,6 +43,7 @@ class ApplicationController : public QObject Q_PROPERTY(QQmlListProperty activeCanvases READ activeCanvases NOTIFY activeCanvasesChanged) + Q_PROPERTY(QQmlListProperty windowList READ windowList NOTIFY windowListChanged) Q_ENUMS(Status) @@ -54,15 +56,12 @@ class ApplicationController : public QObject Q_PROPERTY(bool showGettingStarted READ showGettingStarted WRITE setShowGettingStarted NOTIFY showGettingStartedChanged) public: explicit ApplicationController(QObject *parent = nullptr); - ~ApplicationController(); - - void setWindow(QWindow* window); - - void restoreWindowState(); + ~ApplicationController() override; void loadFromFile(QString path); void setDaemonMode(); + void createWindows(); Q_INVOKABLE void query(); Q_INVOKABLE void cancelQuery(); @@ -86,6 +85,7 @@ public: QVariantList canvases() const; QQmlListProperty activeCanvases(); + QQmlListProperty windowList(); QNetworkAccessManager* netAccess() const; @@ -129,6 +129,7 @@ signals: void portChanged(unsigned int port); void activeCanvasesChanged(); + void windowListChanged(); void canvasListChanged(); void statusChanged(Status status); @@ -179,6 +180,8 @@ private: QByteArray createSnapshot(QString name) const; + void defineDefaultWindow(); + QString m_host; unsigned int m_port; QVariantList m_canvases; @@ -189,8 +192,7 @@ private: QNetworkReply* m_query = nullptr; QVariantList m_snapshots; - QWindow* m_window = nullptr; - Qt::WindowState m_windowState = Qt::WindowNoState; + QList m_windowList; bool m_daemonMode = false; bool m_showUI = true; diff --git a/utils/fgqcanvas/canvasconnection.cpp b/utils/fgqcanvas/canvasconnection.cpp index 04656495d..41903e14b 100644 --- a/utils/fgqcanvas/canvasconnection.cpp +++ b/utils/fgqcanvas/canvasconnection.cpp @@ -77,6 +77,7 @@ QJsonObject CanvasConnection::saveState() const json["url"] = m_webSocketUrl.toString(); json["path"] = QString::fromUtf8(m_rootPropertyPath); json["rect"] = rectToJsonArray(m_destRect.toRect()); + json["window"] = m_windowIndex; return json; } @@ -86,6 +87,10 @@ bool CanvasConnection::restoreState(QJsonObject state) m_rootPropertyPath = state.value("path").toString().toUtf8(); m_destRect = jsonArrayToRect(state.value("rect").toArray()); + if (state.contains("window")) { + m_windowIndex = state.value("window").toInt(); + } + emit geometryChanged(); emit rootPathChanged(); emit webSocketUrlChanged(); @@ -167,6 +172,14 @@ QSizeF CanvasConnection::size() const return m_destRect.size(); } +void CanvasConnection::setWindowIndex(int index) +{ + if (m_windowIndex != index) { + m_windowIndex = index; + emit geometryChanged(); + } +} + LocalProp *CanvasConnection::propertyRoot() const { return m_localPropertyRoot.get(); diff --git a/utils/fgqcanvas/canvasconnection.h b/utils/fgqcanvas/canvasconnection.h index 3bc9a5b22..c05a6b1f0 100644 --- a/utils/fgqcanvas/canvasconnection.h +++ b/utils/fgqcanvas/canvasconnection.h @@ -46,6 +46,8 @@ class CanvasConnection : public QObject Q_PROPERTY(QPointF origin READ origin WRITE setOrigin NOTIFY geometryChanged) Q_PROPERTY(QSizeF size READ size WRITE setSize NOTIFY geometryChanged) + Q_PROPERTY(int windowIndex READ windowIndex WRITE setWindowIndex NOTIFY geometryChanged) + Q_PROPERTY(QUrl webSocketUrl READ webSocketUrl NOTIFY webSocketUrlChanged) Q_PROPERTY(QString rootPath READ rootPath NOTIFY rootPathChanged) public: @@ -84,6 +86,13 @@ public: QSizeF size() const; + int windowIndex() const + { + return m_windowIndex; + } + + void setWindowIndex(int index); + LocalProp* propertyRoot() const; QUrl webSocketUrl() const @@ -132,6 +141,7 @@ private: QUrl m_webSocketUrl; QByteArray m_rootPropertyPath; QRectF m_destRect; + int m_windowIndex = 0; QWebSocket m_webSocket; QNetworkAccessManager* m_netAccess = nullptr; diff --git a/utils/fgqcanvas/canvaspainteddisplay.cpp b/utils/fgqcanvas/canvaspainteddisplay.cpp index e37c2bf54..c14b8be25 100644 --- a/utils/fgqcanvas/canvaspainteddisplay.cpp +++ b/utils/fgqcanvas/canvaspainteddisplay.cpp @@ -108,7 +108,6 @@ void CanvasPaintedDisplay::onConnectionStatusChanged() void CanvasPaintedDisplay::buildElements() { - qDebug() << Q_FUNC_INFO; m_rootElement = new FGCanvasGroup(nullptr, m_connection->propertyRoot()); // this is important to elements can discover their connection // by walking their parent chain diff --git a/utils/fgqcanvas/configs/example_config.json b/utils/fgqcanvas/configs/example_config.json new file mode 100644 index 000000000..13d8bbda6 --- /dev/null +++ b/utils/fgqcanvas/configs/example_config.json @@ -0,0 +1,32 @@ +{ + "canvases": [ + { + "path": "/canvas/by-index/texture[4]", + "rect": [ + 300, + 253, + 852, + 745 + ], + "url": "ws://localhost:8080/PropertyTreeMirror/canvas/by-index/texture[4]" + }, + { + "path": "/canvas/by-index/texture[7]", + "rect": [ + 1171, + 259, + 747, + 711 + ], + "url": "ws://localhost:8080/PropertyTreeMirror/canvas/by-index/texture[7]" + } + ], + "configName": "738_captain", + "window-rect": [ + 1001, + 2188, + 1920, + 1052 + ], + "window-state": 2 +} diff --git a/utils/fgqcanvas/configs/multi_window_config.json b/utils/fgqcanvas/configs/multi_window_config.json new file mode 100644 index 000000000..9ebb9973d --- /dev/null +++ b/utils/fgqcanvas/configs/multi_window_config.json @@ -0,0 +1,38 @@ +{ + "canvases": [ + { + "path": "/canvas/by-index/texture[4]", + "rect": [ + 300, + 253, + 852, + 745 + ], + "url": "ws://localhost:8080/PropertyTreeMirror/canvas/by-index/texture[4]" + }, + { + "path": "/canvas/by-index/texture[7]", + "rect": [ + 1171, + 259, + 747, + 711 + ], + "window":1, + "url": "ws://localhost:8080/PropertyTreeMirror/canvas/by-index/texture[7]" + } + ], + "configName": "738_captain", + "windows": [ + { + "title": "First Window", + "rect":[100, 100, 500, 300] + }, + + { + "title": "Another Window", + "rect":[150, 400, 500, 300], + "screen":"Colour LCD" + } + ] +} diff --git a/utils/fgqcanvas/fgcanvas.pro b/utils/fgqcanvas/fgcanvas.pro index 11f89b61d..edcdf4e5f 100644 --- a/utils/fgqcanvas/fgcanvas.pro +++ b/utils/fgqcanvas/fgcanvas.pro @@ -6,6 +6,7 @@ TARGET = fgqcanvas TEMPLATE = app SOURCES += main.cpp\ + WindowData.cpp \ fgcanvasgroup.cpp \ fgcanvaselement.cpp \ fgcanvaspaintcontext.cpp \ @@ -25,6 +26,7 @@ SOURCES += main.cpp\ HEADERS += \ + WindowData.h \ fgcanvasgroup.h \ fgcanvaselement.h \ fgcanvaspaintcontext.h \ @@ -48,7 +50,8 @@ RESOURCES += \ OTHER_FILES += \ qml/* \ - doc/* + doc/* \ + config/* #Q_XCODE_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM #Q_XCODE_DEVELOPMENT_TEAM.value = "James Turner" diff --git a/utils/fgqcanvas/fgqcanvas_resources.qrc b/utils/fgqcanvas/fgqcanvas_resources.qrc index 0908feaf1..4241c4ff7 100644 --- a/utils/fgqcanvas/fgqcanvas_resources.qrc +++ b/utils/fgqcanvas/fgqcanvas_resources.qrc @@ -1,6 +1,6 @@ - qml/mainMenu.qml + qml/Window.qml qml/Button.qml qml/InputLine.qml qml/checkerboard.png diff --git a/utils/fgqcanvas/main.cpp b/utils/fgqcanvas/main.cpp index c17e1b6e1..8275f7521 100644 --- a/utils/fgqcanvas/main.cpp +++ b/utils/fgqcanvas/main.cpp @@ -27,6 +27,7 @@ #include "canvasdisplay.h" #include "canvasconnection.h" #include "canvaspainteddisplay.h" +#include "WindowData.h" int main(int argc, char *argv[]) { @@ -38,12 +39,6 @@ int main(int argc, char *argv[]) QCommandLineParser parser; parser.addPositionalArgument("config", QCoreApplication::translate("main", "JSON configuration to load")); - QCommandLineOption framelessOption(QStringList() << "frameless", - QCoreApplication::translate("main", "Use a frameless window")); - QCommandLineOption screenOption(QStringList() << "screen", - QCoreApplication::translate("main", "Run full-screen on "), "screen"); - parser.addOption(framelessOption); - parser.addOption(screenOption); parser.process(a); ApplicationController appController; @@ -52,48 +47,10 @@ int main(int argc, char *argv[]) qmlRegisterType("FlightGear", 1, 0, "CanvasDisplay"); qmlRegisterType("FlightGear", 1, 0, "PaintedCanvasDisplay"); + qmlRegisterUncreatableType("FlightGear", 1, 0, "WindowData", "Don't create me"); qmlRegisterUncreatableType("FlightGear", 1, 0, "CanvasConnection", "Don't create me"); qmlRegisterUncreatableType("FlightGear", 1, 0, "Application", "Can't create"); - QQuickView quickView; - appController.setWindow(&quickView); - if (parser.isSet(framelessOption)) { - quickView.setFlag(Qt::FramelessWindowHint, true); - } - - bool restoreWindowState = true; - if (parser.isSet(screenOption)) { - QString screenName = parser.value(screenOption); - QStringList allScreenNames; - - QScreen* found = nullptr; - Q_FOREACH(QScreen* s, a.screens()) { - allScreenNames << s->name(); - if (s->name() == screenName) { - found = s; - break; - } - } - - if (!found) { - qFatal("Unable to find screen: %s: screens are: %s", screenName.toLatin1().data(), - allScreenNames.join(",").toLatin1().data()); - } - - quickView.setScreen(found); - quickView.setGeometry(found->geometry()); - quickView.setFlag(Qt::FramelessWindowHint, true); - restoreWindowState = false; - - qInfo() << "Requested to run on screen:" << screenName << "with geometry" << found->geometry(); - } else { - // windows mode, default geeometry - quickView.setWidth(1024); - quickView.setHeight(768); - } - - quickView.rootContext()->setContextProperty("_application", &appController); - const QStringList args = parser.positionalArguments(); if (!args.empty()) { @@ -101,15 +58,8 @@ int main(int argc, char *argv[]) appController.loadFromFile(args.front()); } - quickView.setResizeMode(QQuickView::SizeRootObjectToView); - quickView.setSource(QUrl("qrc:///qml/mainMenu.qml")); - quickView.show(); - - if (restoreWindowState) { - appController.restoreWindowState(); - } + appController.createWindows(); int result = a.exec(); - return result; } diff --git a/utils/fgqcanvas/qml/mainMenu.qml b/utils/fgqcanvas/qml/Window.qml similarity index 61% rename from utils/fgqcanvas/qml/mainMenu.qml rename to utils/fgqcanvas/qml/Window.qml index 21659dbe5..9a3ab7606 100644 --- a/utils/fgqcanvas/qml/mainMenu.qml +++ b/utils/fgqcanvas/qml/Window.qml @@ -1,13 +1,15 @@ import QtQuick 2.0 -import FlightGear 1.0 as FG +import QtQuick.Window 2.4 as QQ2W Rectangle { width: 1024 height: 768 color: "black" - property double __uiOpacity: _application.showUI ? 1.0 : 0.0 + // only show the UI on the main window + property double __uiOpacity: (isMainWindow && _application.showUI) ? 1.0 : 0.0 property bool __uiVisible: true + readonly property bool isMainWindow: (_windowNumber === 0) Behavior on __uiOpacity { SequentialAnimation { @@ -27,9 +29,25 @@ Rectangle { Repeater { model: _application.activeCanvases - delegate: CanvasFrame { - id: display - canvas: modelData + + // we use a loader to only create canvases on the correct window + // by driving the 'active' property + delegate: Loader { + id: canvasLoader + sourceComponent: canvasFrame + active: modelData.windowIndex === _windowNumber + + Binding { + target: canvasLoader.item + property: "canvas" + value: model.modelData + } + } + } + + Component { + id: canvasFrame + CanvasFrame { showUi: __uiVisible } } @@ -58,6 +76,7 @@ Rectangle { } GetStarted { + visible: isMainWindow anchors.centerIn: parent }