1
0
Fork 0

FGQCanvas: Multi-window support (for RPi4)

To support EGLFS on the RPi4 dual outputs, enable multiple windows
within a single process.
This commit is contained in:
James Turner 2019-08-31 23:40:21 +01:00
parent 331ef3232f
commit 6e465f9dbe
13 changed files with 341 additions and 94 deletions

View file

@ -0,0 +1,86 @@
#include "WindowData.h"
#include <QScreen>
#include <QGuiApplication>
#include <QDebug>
#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<int>(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<Qt::WindowState>(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);
}

View file

@ -0,0 +1,46 @@
#ifndef WINDOWDATA_H
#define WINDOWDATA_H
#include <QObject>
#include <QJsonObject>
#include <QRect>
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

View file

@ -36,9 +36,12 @@
#include <QTimer>
#include <QGuiApplication>
#include <QSettings>
#include <QQuickView>
#include <QQmlContext>
#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<CanvasConnection> ApplicationController::activeCanvases()
return QQmlListProperty<CanvasConnection>(this, m_activeCanvases);
}
QQmlListProperty<WindowData> ApplicationController::windowList()
{
return QQmlListProperty<WindowData>(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<int>(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);
// 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());
}
// we have to cache the state becuase it seems to be ignored
// before show is called();
m_windowState = static_cast<Qt::WindowState>(json.value("window-state").toInt());
m_window->setWindowState(m_windowState);
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<Qt::WindowState>(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();
}

View file

@ -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<CanvasConnection> activeCanvases READ activeCanvases NOTIFY activeCanvasesChanged)
Q_PROPERTY(QQmlListProperty<WindowData> 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<CanvasConnection> activeCanvases();
QQmlListProperty<WindowData> 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<WindowData*> m_windowList;
bool m_daemonMode = false;
bool m_showUI = true;

View file

@ -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();

View file

@ -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;

View file

@ -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

View file

@ -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
}

View file

@ -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"
}
]
}

View file

@ -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"

View file

@ -1,6 +1,6 @@
<RCC>
<qresource prefix="/">
<file>qml/mainMenu.qml</file>
<file>qml/Window.qml</file>
<file>qml/Button.qml</file>
<file>qml/InputLine.qml</file>
<file alias="images/checkerboard">qml/checkerboard.png</file>

View file

@ -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 <scren>"), "screen");
parser.addOption(framelessOption);
parser.addOption(screenOption);
parser.process(a);
ApplicationController appController;
@ -52,48 +47,10 @@ int main(int argc, char *argv[])
qmlRegisterType<CanvasDisplay>("FlightGear", 1, 0, "CanvasDisplay");
qmlRegisterType<CanvasPaintedDisplay>("FlightGear", 1, 0, "PaintedCanvasDisplay");
qmlRegisterUncreatableType<WindowData>("FlightGear", 1, 0, "WindowData", "Don't create me");
qmlRegisterUncreatableType<CanvasConnection>("FlightGear", 1, 0, "CanvasConnection", "Don't create me");
qmlRegisterUncreatableType<ApplicationController>("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;
}

View file

@ -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
}