first commit

This commit is contained in:
Your Name
2022-10-20 20:29:11 +08:00
commit 4d531f8044
3238 changed files with 1387862 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
# AUTOMOC/AUTOUIC on generated files policy.
if(POLICY CMP0071)
cmake_policy(SET CMP0071 NEW)
endif()
include(GNUInstallDirs)
project(FGQCanvas)
find_package(Qt5 5.4 COMPONENTS Widgets WebSockets Gui Quick)
if (NOT Qt5WebSockets_FOUND OR NOT Qt5Quick_FOUND)
message(WARNING "FGQCanvas utility requested, but QtWebSockets not found")
message(STATUS "Check you have the development package for Qt5 WebSockets installed")
return()
endif()
set(SOURCES
main.cpp
localprop.cpp
localprop.h
fgcanvaselement.cpp
fgcanvaselement.h
fgcanvasgroup.cpp
fgcanvasgroup.h
fgcanvaspaintcontext.cpp
fgcanvaspaintcontext.h
fgcanvaspath.cpp
fgcanvaspath.h
fgcanvastext.cpp
fgcanvastext.h
fgqcanvasimage.cpp
fgqcanvasimage.h
fgqcanvasmap.cpp
fgqcanvasmap.h
canvastreemodel.cpp
canvastreemodel.h
fgqcanvasfontcache.cpp
fgqcanvasfontcache.h
fgqcanvasimageloader.cpp
fgqcanvasimageloader.h
elementdatamodel.cpp
elementdatamodel.h
canvasitem.cpp
canvasitem.h
canvasconnection.cpp
canvasconnection.h
applicationcontroller.cpp
applicationcontroller.h
canvasdisplay.cpp
canvasdisplay.h
canvaspainteddisplay.cpp
canvaspainteddisplay.h
jsonutils.cpp
jsonutils.h
WindowData.cpp
WindowData.h
)
qt5_add_resources(qrc_sources fgqcanvas_resources.qrc)
#qt5_wrap_ui(uic_sources temporarywidget.ui)
add_executable(fgqcanvas ${SOURCES} ${qrc_sources})
set_property(TARGET fgqcanvas PROPERTY AUTOMOC ON)
target_link_libraries(fgqcanvas Qt5::Core Qt5::Widgets Qt5::WebSockets Qt5::Quick)
target_include_directories(fgqcanvas PRIVATE ${PROJECT_SOURCE_DIR})
# so ui_foo.h files are found
target_include_directories(fgqcanvas PRIVATE ${PROJECT_BINARY_DIR})
target_include_directories(fgqcanvas PRIVATE ${Qt5Gui_PRIVATE_INCLUDE_DIRS})
target_include_directories(fgqcanvas PRIVATE ${Qt5Quick_PRIVATE_INCLUDE_DIRS})
install(TARGETS fgqcanvas RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+45
View File
@@ -0,0 +1,45 @@
# FGQCanvas
A Qt-based remote canvas application for FlightGear. This app can connect to
a FlightGear instance which has the built-in HTTPD server enabled and display
any canvas in real-time.
## Usage
Start FlightGear with the '--httpd' option, passing a port number. This can be
done in the 'additional options' box if using the launcher.
* `--httpd=8080`
Start FGQCanvas and enter the WebSocket url, with a suitable host-name and port.
Provide the path to the Canvas you want to display (this part will become
smarter in the future!)
Examples URLs:
* `ws://localhost:8080/PropertyTreeMirror`
* `ws://mycomputer.local:8001/PropertyTreeMirror`
Example Canvas path:
* `/canvas/by-index/texture[0]/`
## Limitations
* Clipping is still being worked on
* Fonts are not loaded from the host instance yet
* Image loading is still being worked on, no support for remote image loading
yet.
* Performance is mediocre due to proof-of-concept implementation
* No input event support yet
## Future plans
* Finish image, clip and font loading
* Switch to OpenGL rendering
* Support event-input to the Canvas
* Rewrite to use [Skia](http://skia.org)
## Questions / support
Ask on the developer mailing list!
+86
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);
}
+46
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
+637
View File
@@ -0,0 +1,637 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "applicationcontroller.h"
#include <QNetworkDiskCache>
#include <QStandardPaths>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QDebug>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QDataStream>
#include <QWindow>
#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)
, m_status(Idle)
{
m_netAccess = new QNetworkAccessManager;
QSettings settings;
m_host = settings.value("last-host", "localhost").toString();
m_port = settings.value("last-port", 8080).toUInt();
QNetworkDiskCache* cache = new QNetworkDiskCache;
cache->setCacheDirectory(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
m_netAccess->setCache(cache); // takes ownership
setStatus(Idle);
rebuildConfigData();
rebuildSnapshotData();
m_uiIdleTimer = new QTimer(this);
m_uiIdleTimer->setInterval(10 * 1000);
connect(m_uiIdleTimer, &QTimer::timeout, this,
&ApplicationController::onUIIdleTimeout);
m_uiIdleTimer->start();
qApp->installEventFilter(this);
}
ApplicationController::~ApplicationController()
{
delete m_netAccess;
}
void ApplicationController::loadFromFile(QString path)
{
if (!QFile::exists(path)) {
qWarning() << Q_FUNC_INFO << "no such file:" << path;
}
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << Q_FUNC_INFO << "failed to open" << path;
return;
}
restoreState(f.readAll());
}
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));
if (!d.exists()) {
d.mkpath(".");
}
// convert spaces to underscores
QString filesystemCleanName = configName.replace(QRegularExpression("[\\s-\\\"/]"), "_");
QFile f(d.filePath(filesystemCleanName + ".json"));
if (f.exists()) {
qWarning() << "not over-writing" << f.fileName();
return;
}
f.open(QIODevice::WriteOnly | QIODevice::Truncate);
f.write(saveState(configName));
QVariantMap m;
m["path"] = f.fileName();
m["name"] = configName;
m_configs.append(m);
emit configListChanged(m_configs);
}
void ApplicationController::rebuildConfigData()
{
m_configs.clear();
QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
if (!d.exists()) {
emit configListChanged(m_configs);
return;
}
// this requires parsing each config in its entirety just to extract
// the name, which is horrible.
Q_FOREACH (auto entry, d.entryList(QStringList() << "*.json")) {
QString path = d.filePath(entry);
QFile f(path);
f.open(QIODevice::ReadOnly);
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
QVariantMap m;
m["path"] = path;
m["name"] = doc.object().value("configName").toString();
m_configs.append(m);
}
emit configListChanged(m_configs);
}
void ApplicationController::saveSnapshot(QString snapshotName)
{
QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
d.cd("Snapshots");
if (!d.exists()) {
d.mkpath(".");
}
// convert spaces to underscores
QString filesystemCleanName = snapshotName.replace(QRegularExpression("[\\s-\\\"/]"), "_");
QFile f(d.filePath(filesystemCleanName + ".fgcanvassnapshot"));
if (f.exists()) {
qWarning() << "not over-writing" << f.fileName();
return;
}
f.open(QIODevice::WriteOnly | QIODevice::Truncate);
f.write(createSnapshot(snapshotName));
QVariantMap m;
m["path"] = f.fileName();
m["name"] = snapshotName;
m_snapshots.append(m);
emit snapshotListChanged();
}
void ApplicationController::restoreSnapshot(int index)
{
QString path = m_snapshots.at(index).toMap().value("path").toString();
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << Q_FUNC_INFO << "failed to open the file";
return;
}
clearConnections();
{
QDataStream ds(&f);
int version, canvasCount;
QString name;
ds >> version >> name >> canvasCount;
for (int i=0; i < canvasCount; ++i) {
CanvasConnection* cc = new CanvasConnection(this);
cc->restoreSnapshot(ds);
m_activeCanvases.append(cc);
}
}
emit activeCanvasesChanged();
}
void ApplicationController::rebuildSnapshotData()
{
m_snapshots.clear();
QDir d(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
d.cd("Snapshots");
if (!d.exists()) {
emit snapshotListChanged();
return;
}
Q_FOREACH (auto entry, d.entryList(QStringList() << "*.fgcanvassnapshot")) {
QFile f(d.filePath(entry));
f.open(QIODevice::ReadOnly);
{
QDataStream ds(&f);
int version;
QString name;
ds >> version;
QVariantMap m;
m["path"] = f.fileName();
ds >>name;
m["name"] = name;
m_snapshots.append(m);
}
}
emit snapshotListChanged();
}
void ApplicationController::query()
{
if (m_query) {
cancelQuery();
}
if (m_host.isEmpty() || (m_port == 0))
return;
QSettings settings;
settings.setValue("last-host", m_host);
settings.setValue("last-port", m_port);
QUrl queryUrl;
queryUrl.setScheme("http");
queryUrl.setHost(m_host);
queryUrl.setPort(static_cast<int>(m_port));
queryUrl.setPath("/json/canvas/by-index");
queryUrl.setQuery("d=2");
m_query = m_netAccess->get(QNetworkRequest(queryUrl));
connect(m_query, &QNetworkReply::finished,
this, &ApplicationController::onFinishedGetCanvasList);
setStatus(Querying);
}
void ApplicationController::cancelQuery()
{
setStatus(Idle);
if (m_query) {
m_query->abort();
m_query->deleteLater();
}
m_query = nullptr;
m_canvases.clear();
emit canvasListChanged();
}
void ApplicationController::clearQuery()
{
cancelQuery();
}
void ApplicationController::restoreConfig(int index)
{
QString path = m_configs.at(index).toMap().value("path").toString();
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
qWarning() << Q_FUNC_INFO << "failed to open the file";
return;
}
restoreState(f.readAll());
}
void ApplicationController::deleteConfig(int index)
{
QString path = m_configs.at(index).toMap().value("path").toString();
QFile f(path);
if (!f.remove()) {
qWarning() << "failed to remove file";
return;
}
m_configs.removeAt(index);
emit configListChanged(m_configs);
}
void ApplicationController::saveConfigChanges(int index)
{
QString path = m_configs.at(index).toMap().value("path").toString();
QString name = m_configs.at(index).toMap().value("name").toString();
doSaveToFile(path, name);
}
void ApplicationController::doSaveToFile(QString path, QString configName)
{
QFile f(path);
f.open(QIODevice::WriteOnly | QIODevice::Truncate);
f.write(saveState(configName));
}
void ApplicationController::openCanvas(QString path)
{
CanvasConnection* cc = new CanvasConnection(this);
cc->setNetworkAccess(m_netAccess);
m_activeCanvases.append(cc);
cc->setRootPropertyPath(path.toUtf8());
cc->connectWebSocket(m_host.toUtf8(), m_port);
emit activeCanvasesChanged();
}
void ApplicationController::closeCanvas(CanvasConnection *canvas)
{
Q_ASSERT(m_activeCanvases.indexOf(canvas) >= 0);
m_activeCanvases.removeOne(canvas);
canvas->deleteLater();
emit activeCanvasesChanged();
}
QString ApplicationController::host() const
{
return m_host;
}
unsigned int ApplicationController::port() const
{
return m_port;
}
QVariantList ApplicationController::canvases() const
{
return m_canvases;
}
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;
}
bool ApplicationController::showUI() const
{
if (m_daemonMode)
return false;
if (m_blockUIIdle)
return true;
return m_showUI;
}
QString ApplicationController::gettingStartedText() const
{
QFile f(":/doc/gettingStarted.html");
f.open(QIODevice::ReadOnly);
return QString::fromUtf8(f.readAll());
}
bool ApplicationController::showGettingStarted() const
{
if (m_daemonMode) return false;
QSettings settings;
return settings.value("show-getting-started", true).toBool();
}
void ApplicationController::setHost(QString host)
{
if (m_host == host)
return;
m_host = host;
emit hostChanged(m_host);
setStatus(Idle);
}
void ApplicationController::setPort(unsigned int port)
{
if (m_port == port)
return;
m_port = port;
emit portChanged(m_port);
setStatus(Idle);
}
void ApplicationController::setShowGettingStarted(bool show)
{
QSettings settings;
if (settings.value("show-getting-started", true).toBool() == show)
return;
settings.setValue("show-getting-started", show);
emit showGettingStartedChanged(show);
}
QJsonObject jsonPropNodeFindChild(QJsonObject obj, QByteArray name)
{
Q_FOREACH (QJsonValue v, obj.value("children").toArray()) {
QJsonObject vo = v.toObject();
if (vo.value("name").toString() == name) {
return vo;
}
}
return QJsonObject();
}
void ApplicationController::onFinishedGetCanvasList()
{
m_canvases.clear();
QNetworkReply* reply = m_query;
m_query = nullptr;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
setStatus(QueryFailed);
emit canvasListChanged();
return;
}
QJsonDocument json = QJsonDocument::fromJson(reply->readAll());
QJsonArray canvasArray = json.object().value("children").toArray();
Q_FOREACH (QJsonValue canvasValue, canvasArray) {
QJsonObject canvas = canvasValue.toObject();
QString canvasName = jsonPropNodeFindChild(canvas, "name").value("value").toString();
QString propPath = canvas.value("path").toString();
QVariantMap info;
info["name"] = canvasName;
info["path"] = propPath;
m_canvases.append(info);
}
emit canvasListChanged();
setStatus(SuccessfulQuery);
}
void ApplicationController::onUIIdleTimeout()
{
m_showUI = false;
emit showUIChanged();
}
void ApplicationController::setStatus(ApplicationController::Status newStatus)
{
if (newStatus == m_status)
return;
m_status = newStatus;
emit statusChanged(m_status);
}
QByteArray ApplicationController::saveState(QString name) const
{
QJsonObject json;
json["configName"] = name;
QJsonArray canvases;
Q_FOREACH (auto canvas, m_activeCanvases) {
canvases.append(canvas->saveState());
}
json["canvases"] = canvases;
QJsonArray windows;
Q_FOREACH (auto w, m_windowList) {
windows.append(w->saveState());
}
json["windows"] = windows;
// background color?
QJsonDocument doc;
doc.setObject(json);
return doc.toJson();
}
void ApplicationController::restoreState(QByteArray bytes)
{
clearConnections();
QJsonDocument jsonDoc = QJsonDocument::fromJson(bytes);
QJsonObject json = jsonDoc.object();
// 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<Qt::WindowState>(json.value("window-state").toInt()));
}
m_windowList.append(w);
}
for (auto c : json.value("canvases").toArray()) {
auto cc = new CanvasConnection(this);
if (m_daemonMode)
cc->setAutoReconnect();
cc->setNetworkAccess(m_netAccess);
m_activeCanvases.append(cc);
cc->restoreState(c.toObject());
cc->reconnect();
}
emit windowListChanged();
emit activeCanvasesChanged();
}
void ApplicationController::clearConnections()
{
Q_FOREACH(auto c, m_activeCanvases) {
c->deleteLater();
}
m_activeCanvases.clear();
emit activeCanvasesChanged();
}
QByteArray ApplicationController::createSnapshot(QString name) const
{
QByteArray bytes;
const int version = 1;
{
QDataStream ds(&bytes, QIODevice::WriteOnly);
ds << version << name;
ds << m_activeCanvases.size();
Q_FOREACH(auto c, m_activeCanvases) {
c->saveSnapshot(ds);
}
}
return bytes;
}
bool ApplicationController::eventFilter(QObject* obj, QEvent* event)
{
Q_UNUSED(obj);
switch (event->type()) {
case QEvent::MouseButtonPress:
case QEvent::TouchUpdate:
case QEvent::MouseMove:
case QEvent::TouchBegin:
case QEvent::KeyPress:
case QEvent::KeyRelease:
if (!m_showUI) {
m_showUI = true;
emit showUIChanged();
} else {
m_uiIdleTimer->start();
}
break;
default:
break;
}
return false; //process as normal
}
+203
View File
@@ -0,0 +1,203 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef APPLICATIONCONTROLLER_H
#define APPLICATIONCONTROLLER_H
#include <QObject>
#include <QAbstractListModel>
#include <QNetworkAccessManager>
#include <QQmlListProperty>
#include <QVariantList>
class CanvasConnection;
class QWindow;
class QTimer;
class WindowData;
class ApplicationController : public QObject
{
Q_OBJECT
Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged)
Q_PROPERTY(unsigned int port READ port WRITE setPort NOTIFY portChanged)
Q_PROPERTY(QVariantList canvases READ canvases NOTIFY canvasListChanged)
Q_PROPERTY(QVariantList configs READ configs NOTIFY configListChanged)
Q_PROPERTY(QVariantList snapshots READ snapshots NOTIFY snapshotListChanged)
Q_PROPERTY(QQmlListProperty<CanvasConnection> activeCanvases READ activeCanvases NOTIFY activeCanvasesChanged)
Q_PROPERTY(QQmlListProperty<WindowData> windowList READ windowList NOTIFY windowListChanged)
Q_ENUMS(Status)
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
Q_PROPERTY(bool showUI READ showUI NOTIFY showUIChanged)
Q_PROPERTY(bool blockUIIdle READ blockUIIdle WRITE setBlockUIIdle NOTIFY blockUIIdleChanged)
Q_PROPERTY(QString gettingStartedText READ gettingStartedText CONSTANT)
Q_PROPERTY(bool showGettingStarted READ showGettingStarted WRITE setShowGettingStarted NOTIFY showGettingStartedChanged)
public:
explicit ApplicationController(QObject *parent = nullptr);
~ApplicationController() override;
void loadFromFile(QString path);
void setDaemonMode();
void createWindows();
Q_INVOKABLE void query();
Q_INVOKABLE void cancelQuery();
Q_INVOKABLE void clearQuery();
Q_INVOKABLE void save(QString configName);
Q_INVOKABLE void restoreConfig(int index);
Q_INVOKABLE void deleteConfig(int index);
Q_INVOKABLE void saveConfigChanges(int index);
Q_INVOKABLE void openCanvas(QString path);
Q_INVOKABLE void closeCanvas(CanvasConnection* canvas);
Q_INVOKABLE void saveSnapshot(QString snapshotName);
Q_INVOKABLE void restoreSnapshot(int index);
QString host() const;
unsigned int port() const;
QVariantList canvases() const;
QQmlListProperty<CanvasConnection> activeCanvases();
QQmlListProperty<WindowData> windowList();
QNetworkAccessManager* netAccess() const;
enum Status {
Idle,
Querying,
SuccessfulQuery,
QueryFailed
};
Status status() const
{
return m_status;
}
QVariantList configs() const
{
return m_configs;
}
QVariantList snapshots() const
{
return m_snapshots;
}
bool showUI() const;
bool blockUIIdle() const
{
return m_blockUIIdle;
}
QString gettingStartedText() const;
bool showGettingStarted() const;
signals:
void hostChanged(QString host);
void portChanged(unsigned int port);
void activeCanvasesChanged();
void windowListChanged();
void canvasListChanged();
void statusChanged(Status status);
void configListChanged(QVariantList configs);
void snapshotListChanged();
void showUIChanged();
void blockUIIdleChanged(bool blockUIIdle);
void showGettingStartedChanged(bool showGettingStarted);
public slots:
void setHost(QString host);
void setPort(unsigned int port);
void setBlockUIIdle(bool blockUIIdle)
{
if (m_blockUIIdle == blockUIIdle)
return;
m_blockUIIdle = blockUIIdle;
emit blockUIIdleChanged(m_blockUIIdle);
}
void setShowGettingStarted(bool showGettingStarted);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private slots:
void onFinishedGetCanvasList();
void onUIIdleTimeout();
private:
void setStatus(Status newStatus);
void rebuildConfigData();
void rebuildSnapshotData();
void clearConnections();
void doSaveToFile(QString path, QString configName);
QByteArray saveState(QString name) const;
void restoreState(QByteArray bytes);
QByteArray createSnapshot(QString name) const;
void defineDefaultWindow();
QString m_host;
unsigned int m_port;
QVariantList m_canvases;
QList<CanvasConnection*> m_activeCanvases;
QNetworkAccessManager* m_netAccess;
Status m_status;
QVariantList m_configs;
QNetworkReply* m_query = nullptr;
QVariantList m_snapshots;
QList<WindowData*> m_windowList;
bool m_daemonMode = false;
bool m_showUI = true;
bool m_blockUIIdle = false;
QTimer* m_uiIdleTimer;
};
#endif // APPLICATIONCONTROLLER_H
+321
View File
@@ -0,0 +1,321 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "canvasconnection.h"
#include <QUrl>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDataStream>
#include "localprop.h"
#include "fgqcanvasfontcache.h"
#include "fgqcanvasimageloader.h"
#include "jsonutils.h"
CanvasConnection::CanvasConnection(QObject *parent) : QObject(parent)
{
connect(&m_webSocket, &QWebSocket::connected, this, &CanvasConnection::onWebSocketConnected);
connect(&m_webSocket, &QWebSocket::disconnected, this, &CanvasConnection::onWebSocketClosed);
connect(&m_webSocket, &QWebSocket::textMessageReceived,
this, &CanvasConnection::onTextMessageReceived);
m_destRect = QRectF(50, 50, 400, 400);
m_reconnectTimer = new QTimer(this);
m_reconnectTimer->setInterval(1000 * 10);
m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout,
this, &CanvasConnection::reconnect);
}
CanvasConnection::~CanvasConnection()
{
disconnect(&m_webSocket, &QWebSocket::disconnected,
this, &CanvasConnection::onWebSocketClosed);
m_webSocket.close();
}
void CanvasConnection::setNetworkAccess(QNetworkAccessManager *dl)
{
m_netAccess = dl;
}
void CanvasConnection::setRootPropertyPath(QByteArray path)
{
m_rootPropertyPath = path;
emit rootPathChanged();
}
void CanvasConnection::setAutoReconnect()
{
m_autoReconnect = true;
}
QJsonObject CanvasConnection::saveState() const
{
QJsonObject json;
json["url"] = m_webSocketUrl.toString();
json["path"] = QString::fromUtf8(m_rootPropertyPath);
json["rect"] = rectToJsonArray(m_destRect.toRect());
json["window"] = m_windowIndex;
return json;
}
bool CanvasConnection::restoreState(QJsonObject state)
{
m_webSocketUrl = state.value("url").toString();
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();
return true;
}
void CanvasConnection::saveSnapshot(QDataStream &ds) const
{
ds << m_webSocketUrl << m_rootPropertyPath << m_destRect;
m_localPropertyRoot->saveToStream(ds);
}
void CanvasConnection::restoreSnapshot(QDataStream &ds)
{
ds >> m_webSocketUrl >> m_rootPropertyPath >> m_destRect;
m_localPropertyRoot.reset(LocalProp::restoreFromStream(ds, nullptr));
setStatus(Snapshot);
emit geometryChanged();
emit rootPathChanged();
emit webSocketUrlChanged();
emit updated();
}
void CanvasConnection::reconnect()
{
qDebug() << "starting connection attempt to:" << m_webSocketUrl;
m_webSocket.open(m_webSocketUrl);
setStatus(Connecting);
}
void CanvasConnection::showDebugTree()
{
qWarning() << Q_FUNC_INFO << "implement me!";
}
void CanvasConnection::setOrigin(QPointF c)
{
if (m_destRect.topLeft() == c)
return;
m_destRect.moveTopLeft(c);
emit geometryChanged();
}
void CanvasConnection::setSize(QSizeF sz)
{
if (size() == sz)
return;
m_destRect.setSize(sz);
emit geometryChanged();
}
void CanvasConnection::connectWebSocket(QByteArray hostName, int port)
{
QUrl wsUrl;
wsUrl.setScheme("ws");
wsUrl.setHost(hostName);
wsUrl.setPort(port);
wsUrl.setPath("/PropertyTreeMirror" + m_rootPropertyPath);
m_webSocketUrl = wsUrl;
emit webSocketUrlChanged();
m_webSocket.open(wsUrl);
setStatus(Connecting);
}
QPointF CanvasConnection::origin() const
{
return m_destRect.topLeft();
}
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();
}
FGQCanvasImageLoader *CanvasConnection::imageLoader() const
{
if (!m_imageLoader) {
m_imageLoader = new FGQCanvasImageLoader(m_netAccess, const_cast<CanvasConnection*>(this));
m_imageLoader->setHost(m_webSocketUrl.host(),
m_webSocketUrl.port());
}
return m_imageLoader;
}
FGQCanvasFontCache *CanvasConnection::fontCache() const
{
if (!m_fontCache) {
m_fontCache = new FGQCanvasFontCache(m_netAccess, const_cast<CanvasConnection*>(this));
m_fontCache->setHost(m_webSocketUrl.host(),
m_webSocketUrl.port());
}
return m_fontCache;
}
void CanvasConnection::onWebSocketConnected()
{
qDebug() << Q_FUNC_INFO << m_webSocketUrl;
m_localPropertyRoot.reset(new LocalProp{nullptr, NameIndexTuple("")});
setStatus(Connected);
}
void CanvasConnection::onTextMessageReceived(QString message)
{
QJsonDocument json = QJsonDocument::fromJson(message.toUtf8());
if (json.isObject()) {
// process new nodes
QJsonArray created = json.object().value("created").toArray();
Q_FOREACH (QJsonValue v, created) {
QJsonObject newProp = v.toObject();
QByteArray nodePath = newProp.value("path").toString().toUtf8();
if (nodePath.indexOf(m_rootPropertyPath) != 0) {
qWarning() << "not a property path we are mirroring:" << nodePath;
continue;
}
QByteArray localPath = nodePath.mid(m_rootPropertyPath.size() + 1);
LocalProp* newNode = propertyFromPath(localPath);
newNode->setPosition(newProp.value("position").toInt());
// store in the global dict
int propId = newProp.value("id").toInt();
if (idPropertyDict.contains(propId)) {
qWarning() << "duplicate add of:" << nodePath << "old is" << idPropertyDict.value(propId)->path();
} else {
idPropertyDict.insert(propId, newNode);
}
// set initial value
newNode->processChange(newProp.value("value"));
}
// process removes
QJsonArray removed = json.object().value("removed").toArray();
Q_FOREACH (QJsonValue v, removed) {
int propId = v.toInt();
if (!idPropertyDict.contains(propId)) {
continue;
}
auto prop = idPropertyDict.value(propId);
idPropertyDict.remove(propId);
// depending on the order removes are sent, the LocalProp
// may already have been deleted when its parent was removed,
// so check if the QPointer is null
if (!prop.isNull()) {
prop->parent()->removeChild(prop);
}
} // of removes processing
// process changes
QJsonArray changed = json.object().value("changed").toArray();
Q_FOREACH (QJsonValue v, changed) {
QJsonArray change = v.toArray();
if (change.size() != 2) {
qWarning() << "malformed change notification";
continue;
}
int propId = change.at(0).toInt();
if (!idPropertyDict.contains(propId)) {
qWarning() << "ignoring unknown prop ID " << propId;
continue;
}
LocalProp* lp = idPropertyDict.value(propId);
if (lp != nullptr) {
lp->processChange(change.at(1));
}
} // of change processing
}
emit updated();
}
void CanvasConnection::onWebSocketClosed()
{
if ((m_status == Connected) || (m_status == Connected)) {
qDebug() << "saw web-socket closed";
}
m_localPropertyRoot.reset();
idPropertyDict.clear();
setStatus(Closed);
if (m_autoReconnect) {
m_reconnectTimer->start();
}
}
void CanvasConnection::setStatus(CanvasConnection::Status newStatus)
{
if (newStatus == m_status)
return;
m_status = newStatus;
emit statusChanged(m_status);
}
LocalProp *CanvasConnection::propertyFromPath(QByteArray path) const
{
return m_localPropertyRoot->getOrCreateWithPath(path);
}
+159
View File
@@ -0,0 +1,159 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef CANVASCONNECTION_H
#define CANVASCONNECTION_H
#include <memory>
#include <QObject>
#include <QtWebSockets/QWebSocket>
#include <QJsonObject>
#include <QUrl>
#include <QRectF>
#include <QPointer>
#include <QTimer>
class LocalProp;
class QNetworkAccessManager;
class FGQCanvasImageLoader;
class FGQCanvasFontCache;
class QDataStream;
class CanvasConnection : public QObject
{
Q_OBJECT
Q_ENUMS(Status)
Q_PROPERTY(Status status READ status NOTIFY statusChanged)
// QML exposed versions of the destination rect
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:
explicit CanvasConnection(QObject *parent = nullptr);
~CanvasConnection();
void setNetworkAccess(QNetworkAccessManager *dl);
void setRootPropertyPath(QByteArray path);
void setAutoReconnect();
enum Status
{
NotConnected,
Connecting,
Connected,
Closed,
Reconnecting,
Error,
Snapshot // offline mode, data from snapshot
};
Status status() const
{
return m_status;
}
QJsonObject saveState() const;
bool restoreState(QJsonObject state);
void saveSnapshot(QDataStream& ds) const;
void restoreSnapshot(QDataStream &ds);
void connectWebSocket(QByteArray hostName, int port);
QPointF origin() const;
QSizeF size() const;
int windowIndex() const
{
return m_windowIndex;
}
void setWindowIndex(int index);
LocalProp* propertyRoot() const;
QUrl webSocketUrl() const
{
return m_webSocketUrl;
}
QString rootPath() const
{
return QString::fromUtf8(m_rootPropertyPath);
}
FGQCanvasImageLoader* imageLoader() const;
FGQCanvasFontCache* fontCache() const;
public Q_SLOTS:
void reconnect();
// not on iOS / Android - requires widgets
void showDebugTree();
void setOrigin(QPointF center);
void setSize(QSizeF size);
signals:
void statusChanged(Status status);
void geometryChanged();
void rootPathChanged();
void webSocketUrlChanged();
void updated();
private Q_SLOTS:
void onWebSocketConnected();
void onTextMessageReceived(QString message);
void onWebSocketClosed();
private:
void setStatus(Status newStatus);
LocalProp *propertyFromPath(QByteArray path) const;
QUrl m_webSocketUrl;
QByteArray m_rootPropertyPath;
QRectF m_destRect;
int m_windowIndex = 0;
QWebSocket m_webSocket;
QNetworkAccessManager* m_netAccess = nullptr;
QTimer* m_reconnectTimer = nullptr;
bool m_autoReconnect = false;
std::unique_ptr<LocalProp> m_localPropertyRoot;
QHash<int, QPointer<LocalProp>> idPropertyDict;
Status m_status = NotConnected;
mutable FGQCanvasImageLoader* m_imageLoader = nullptr;
mutable FGQCanvasFontCache* m_fontCache = nullptr;
};
#endif // CANVASCONNECTION_H
+139
View File
@@ -0,0 +1,139 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "canvasdisplay.h"
#include <QDebug>
#include <QQuickItem>
#include "canvasconnection.h"
#include "fgcanvasgroup.h"
#include "fgcanvaspaintcontext.h"
#include "canvasitem.h"
#include "localprop.h"
CanvasDisplay::CanvasDisplay(QQuickItem* parent) :
QQuickItem(parent)
{
setTransformOrigin(QQuickItem::TopLeft);
setFlag(ItemHasContents);
}
CanvasDisplay::~CanvasDisplay()
{
delete m_rootItem;
}
void CanvasDisplay::updatePolish()
{
m_rootElement->polish();
}
void CanvasDisplay::geometryChanged(const QRectF &newGeometry, const QRectF &)
{
Q_UNUSED(newGeometry);
recomputeScaling();
}
void CanvasDisplay::setCanvas(CanvasConnection *canvas)
{
if (m_connection == canvas)
return;
if (m_connection) {
disconnect(m_connection, nullptr, this, nullptr);
qDebug() << "deleting items";
delete m_rootItem;
qDebug() << "deleting elements";
m_rootElement.reset();
qDebug() << "done";
}
m_connection = canvas;
emit canvasChanged(m_connection);
if (m_connection) {
connect(m_connection, &QObject::destroyed,
this, &CanvasDisplay::onConnectionDestroyed);
connect(m_connection, &CanvasConnection::statusChanged,
this, &CanvasDisplay::onConnectionStatusChanged);
connect(m_connection, &CanvasConnection::updated,
this, &CanvasDisplay::onConnectionUpdated);
onConnectionStatusChanged();
}
}
void CanvasDisplay::onConnectionDestroyed()
{
m_connection = nullptr;
emit canvasChanged(m_connection);
m_rootElement.reset();
}
void CanvasDisplay::onConnectionStatusChanged()
{
if ((m_connection->status() == CanvasConnection::Connected) ||
(m_connection->status() == CanvasConnection::Snapshot))
{
m_rootElement.reset(new FGCanvasGroup(nullptr, m_connection->propertyRoot()));
// this is important to elements can discover their connection
// by walking their parent chain
m_rootElement->setParent(m_connection);
connect(m_rootElement.get(), &FGCanvasGroup::canvasSizeChanged,
this, &CanvasDisplay::onCanvasSizeChanged);
m_rootItem = m_rootElement->createQuickItem(this);
onCanvasSizeChanged();
if (m_connection->status() == CanvasConnection::Snapshot) {
m_connection->propertyRoot()->recursiveNotifyRestored();
m_rootElement->polish();
update();
}
}
}
void CanvasDisplay::onConnectionUpdated()
{
if (m_rootElement) {
m_rootElement->polish();
update();
}
}
void CanvasDisplay::onCanvasSizeChanged()
{
m_sourceSize = QSizeF(m_connection->propertyRoot()->value("size", 256).toDouble(),
m_connection->propertyRoot()->value("size[1]", 256).toDouble());
setImplicitSize(m_sourceSize.width(), m_sourceSize.height());
recomputeScaling();
}
void CanvasDisplay::recomputeScaling()
{
const double xScaleFactor = width() / m_sourceSize.width();
const double yScaleFactor = height() / m_sourceSize.height();
setScale(std::min(xScaleFactor, yScaleFactor));
}
+74
View File
@@ -0,0 +1,74 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef CANVASDISPLAY_H
#define CANVASDISPLAY_H
#include <memory>
#include <QQuickItem>
class CanvasConnection;
class FGCanvasGroup;
class QQuickItem;
class CanvasDisplay : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(CanvasConnection* canvas READ canvas WRITE setCanvas NOTIFY canvasChanged)
public:
CanvasDisplay(QQuickItem* parent = nullptr);
~CanvasDisplay();
CanvasConnection* canvas() const
{
return m_connection;
}
signals:
void canvasChanged(CanvasConnection* canvas);
public slots:
void setCanvas(CanvasConnection* canvas);
protected:
void updatePolish() override;
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
private slots:
void onConnectionStatusChanged();
void onConnectionUpdated();
void onCanvasSizeChanged();
void onConnectionDestroyed();
private:
void recomputeScaling();
CanvasConnection* m_connection = nullptr;
std::unique_ptr<FGCanvasGroup> m_rootElement;
QQuickItem* m_rootItem = nullptr;
QSizeF m_sourceSize;
};
#endif // CANVASDISPLAY_H
+198
View File
@@ -0,0 +1,198 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "canvasitem.h"
#include <QMatrix4x4>
#include <QSGClipNode>
class LocalTransform : public QQuickTransform
{
Q_OBJECT
public:
LocalTransform(QObject *parent) : QQuickTransform(parent) {}
void setTransform(const QMatrix4x4 &t) {
transform = t;
update();
}
void applyTo(QMatrix4x4 *matrix) const override
{
*matrix *= transform;
}
private:
QMatrix4x4 transform;
};
CanvasItem::CanvasItem(QQuickItem* pr)
: QQuickItem(pr)
, m_localTransform(new LocalTransform(this))
{
setFlag(ItemHasContents);
m_localTransform->prependToItem(this);
}
void CanvasItem::setTransform(const QMatrix4x4 &mat)
{
m_localTransform->setTransform(mat);
}
void CanvasItem::setClip(const QRectF &clip, ReferenceFrame rf)
{
if (m_hasClip && (clip == m_clipRect) && (rf == m_clipReferenceFrame)) {
return;
}
m_hasClip = true;
m_clipRect = clip;
m_clipReferenceFrame = rf;
update();
}
void CanvasItem::setClipReferenceFrameItem(QQuickItem *refItem)
{
m_clipReferenceFrameItem = refItem;
}
void CanvasItem::clearClip()
{
m_hasClip = false;
update();
}
QSGNode *CanvasItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *d)
{
QSGNode* realOldNode = oldNode;
QSGClipNode* oldClip = nullptr;
if (oldNode && (oldNode->type() == QSGNode::ClipNodeType)) {
Q_ASSERT(oldNode->childCount() == 1);
realOldNode = oldNode->childAtIndex(0);
oldClip = static_cast<QSGClipNode*>(oldNode);
}
QSGNode* contentNode = updateRealPaintNode(realOldNode, d);
if (!contentNode) {
return nullptr;
}
QSGNode* clipNode = updateClipNode(oldClip, contentNode);
return clipNode ? clipNode : contentNode;
}
QSGNode *CanvasItem::updateRealPaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *d)
{
if (oldNode) {
return oldNode;
}
return new QSGNode();
}
QRectF checkRectangularClip(QPointF* vertices)
{
// order is TL / BL / TR / BR to match updateRectGeometry
const double top = vertices[0].y();
const double left = vertices[0].x();
const double bottom = vertices[1].y();
const double right = vertices[2].x();
if (vertices[1].x() != left) return {};
if (vertices[2].y() != top) return {};
if ((vertices[3].x() != right) || (vertices[3].y() != bottom))
return {};
return QRectF(vertices[0], vertices[3]);
}
QSGClipNode* CanvasItem::updateClipNode(QSGClipNode* oldClipNode, QSGNode* contentNode)
{
Q_ASSERT(contentNode);
if (!m_hasClip) {
return nullptr;
}
QSGGeometry* clipGeometry = nullptr;
QSGClipNode* clipNode = oldClipNode;
if (!clipNode) {
clipNode = new QSGClipNode();
clipGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4);
clipGeometry->setDrawingMode(GL_TRIANGLE_STRIP);
clipNode->setGeometry(clipGeometry);
clipNode->setFlag(QSGNode::OwnsGeometry);
clipNode->appendChildNode(contentNode);
} else {
if (clipNode->childCount() == 1) {
const auto existingChild = clipNode->childAtIndex(0);
if (existingChild == contentNode) {
qInfo() << "optimise for this case!";
}
}
clipNode->removeAllChildNodes();
clipNode->appendChildNode(contentNode);
clipGeometry = clipNode->geometry();
Q_ASSERT(clipGeometry);
}
QPointF clipVertices[4],
inVertices[4] = {m_clipRect.topLeft(), m_clipRect.bottomLeft(),
m_clipRect.topRight(), m_clipRect.bottomRight()};
QRectF rectClip;
switch (m_clipReferenceFrame) {
case ReferenceFrame::GLOBAL:
case ReferenceFrame::PARENT:
Q_ASSERT(m_clipReferenceFrameItem);
for (int i=0; i<4; ++i) {
clipVertices[i] = mapFromItem(m_clipReferenceFrameItem, inVertices[i]);
}
rectClip = checkRectangularClip(clipVertices);
break;
case ReferenceFrame::LOCAL:
// local ref-frame clip is always rectangular
rectClip = m_clipRect;
for (int i=0; i<4; ++i) {
clipVertices[i] = inVertices[i];
}
break;
}
clipNode->setIsRectangular(!rectClip.isNull());
qInfo() << "\nobj:" << objectName();
if (!rectClip.isNull()) {
qInfo() << "have rectangular clip for:" << m_clipRect << (int) m_clipReferenceFrame << rectClip;
clipNode->setClipRect(rectClip);
} else {
qInfo() << "haved rotated clip" << m_clipRect << (int) m_clipReferenceFrame;
qInfo() << "final local clip points:" << clipVertices[0] << clipVertices[1]
<< clipVertices[2] << clipVertices[3];
}
QSGGeometry::Point2D *v = clipGeometry->vertexDataAsPoint2D();
for (int i=0; i<4; ++i) {
v[i].x = clipVertices[i].x();
v[i].y = clipVertices[i].y();
}
clipGeometry->markVertexDataDirty();
clipNode->markDirty(QSGNode::DirtyGeometry);
return clipNode;
}
#include "canvasitem.moc"
+59
View File
@@ -0,0 +1,59 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef CANVASITEM_H
#define CANVASITEM_H
#include <QQuickItem>
#include "fgcanvaselement.h"
class LocalTransform;
class QSGClipNode;
class CanvasItem : public QQuickItem
{
Q_OBJECT
public:
CanvasItem(QQuickItem* pr = nullptr);
void setTransform(const QMatrix4x4& mat);
void setClip(const QRectF &clip, ReferenceFrame rf);
void setClipReferenceFrameItem(QQuickItem* refItem);
void clearClip();
QSGNode* updatePaintNode(QSGNode *, UpdatePaintNodeData *) override final;
signals:
public slots:
protected:
virtual QSGNode *updateRealPaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *d);
private:
QSGClipNode* updateClipNode(QSGClipNode* oldClipNode, QSGNode* contentNode);
LocalTransform* m_localTransform;
QRectF m_clipRect;
bool m_hasClip = false;
ReferenceFrame m_clipReferenceFrame = ReferenceFrame::GLOBAL;
QQuickItem* m_clipReferenceFrameItem = nullptr;
};
#endif // CANVASITEM_H
+141
View File
@@ -0,0 +1,141 @@
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "canvaspainteddisplay.h"
#include <QDebug>
#include "canvasconnection.h"
#include "fgcanvasgroup.h"
#include "fgcanvaspaintcontext.h"
#include "localprop.h"
CanvasPaintedDisplay::CanvasPaintedDisplay(QQuickItem* parent) :
QQuickPaintedItem(parent)
{
setTransformOrigin(QQuickItem::TopLeft);
setAntialiasing(true);
}
CanvasPaintedDisplay::~CanvasPaintedDisplay()
{
}
void CanvasPaintedDisplay::paint(QPainter *painter)
{
if (!m_rootElement)
return;
const double xScaleFactor = width() / m_sourceSize.width();
const double yScaleFactor = height() / m_sourceSize.height();
const double f = std::min(xScaleFactor, yScaleFactor);
painter->scale(f, f);
FGCanvasPaintContext context(painter);
m_rootElement->paint(&context);
}
void CanvasPaintedDisplay::geometryChanged(const QRectF &newGeometry, const QRectF &)
{
Q_UNUSED(newGeometry);
update();
}
void CanvasPaintedDisplay::setCanvas(CanvasConnection *canvas)
{
if (m_connection == canvas)
return;
if (m_connection) {
disconnect(m_connection, nullptr, this, nullptr);
delete m_rootElement;
}
m_connection = canvas;
emit canvasChanged(m_connection);
if (m_connection) {
connect(m_connection, &QObject::destroyed,
this, &CanvasPaintedDisplay::onConnectionDestroyed);
connect(m_connection, &CanvasConnection::statusChanged,
this, &CanvasPaintedDisplay::onConnectionStatusChanged);
connect(m_connection, &CanvasConnection::updated,
this, &CanvasPaintedDisplay::onConnectionUpdated);
onConnectionStatusChanged();
}
}
void CanvasPaintedDisplay::onConnectionDestroyed()
{
qDebug() << Q_FUNC_INFO << "saw connection destroyed";
m_connection = nullptr;
delete m_rootElement;
emit canvasChanged(m_connection);
}
void CanvasPaintedDisplay::onConnectionStatusChanged()
{
if ((m_connection->status() == CanvasConnection::Connected) ||
(m_connection->status() == CanvasConnection::Snapshot))
{
buildElements();
} else {
if (m_rootElement) {
qDebug() << Q_FUNC_INFO << "clearing root element";
delete m_rootElement;
m_rootElement.clear();
}
update();
}
}
void CanvasPaintedDisplay::buildElements()
{
m_rootElement = new FGCanvasGroup(nullptr, m_connection->propertyRoot());
// this is important to elements can discover their connection
// by walking their parent chain
m_rootElement->setParent(m_connection);
connect(m_rootElement.data(), &FGCanvasGroup::canvasSizeChanged,
this, &CanvasPaintedDisplay::onCanvasSizeChanged);
onCanvasSizeChanged();
m_connection->propertyRoot()->recursiveNotifyRestored();
m_rootElement->polish();
update();
}
void CanvasPaintedDisplay::onConnectionUpdated()
{
if (m_rootElement) {
m_rootElement->polish();
update();
}
}
void CanvasPaintedDisplay::onCanvasSizeChanged()
{
m_sourceSize = QSizeF(m_connection->propertyRoot()->value("size", 256).toDouble(),
m_connection->propertyRoot()->value("size[1]", 256).toDouble());
setImplicitSize(m_sourceSize.width(), m_sourceSize.height());
update();
}
+76
View File
@@ -0,0 +1,76 @@
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef CANVAS_PAINTED_DISPLAY_H
#define CANVAS_PAINTED_DISPLAY_H
#include <memory>
#include <QQuickPaintedItem>
#include <QPointer>
class CanvasConnection;
class FGCanvasGroup;
class QQuickItem;
class CanvasPaintedDisplay : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(CanvasConnection* canvas READ canvas WRITE setCanvas NOTIFY canvasChanged)
public:
CanvasPaintedDisplay(QQuickItem* parent = nullptr);
~CanvasPaintedDisplay();
CanvasConnection* canvas() const
{
return m_connection;
}
void paint(QPainter *painter) override;
signals:
void canvasChanged(CanvasConnection* canvas);
public slots:
void setCanvas(CanvasConnection* canvas);
protected:
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
private slots:
void onConnectionStatusChanged();
void onConnectionUpdated();
void onCanvasSizeChanged();
void onConnectionDestroyed();
private:
void recomputeScaling();
void buildElements();
CanvasConnection* m_connection = nullptr;
QPointer<FGCanvasGroup> m_rootElement;
// QQuickItem* m_rootItem = nullptr;
QSizeF m_sourceSize;
};
#endif // CANVAS_PAINTED_DISPLAY_H
+183
View File
@@ -0,0 +1,183 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "canvastreemodel.h"
#include <QDebug>
#include "localprop.h"
CanvasTreeModel::CanvasTreeModel(FGCanvasGroup* root) :
_root(root)
{
connect(_root, &FGCanvasGroup::childAdded, this, &CanvasTreeModel::onGroupChildAdded);
}
FGCanvasElement* CanvasTreeModel::elementFromIndex(const QModelIndex &index) const
{
if (!index.isValid()) {
return nullptr;
}
FGCanvasElement* e = static_cast<FGCanvasElement*>(index.internalPointer());
return e;
}
int CanvasTreeModel::rowCount(const QModelIndex &parent) const
{
FGCanvasElement* e = static_cast<FGCanvasElement*>(parent.internalPointer());
if (!e) {
return _root->childCount();
}
FGCanvasGroup* group = qobject_cast<FGCanvasGroup*>(e);
if (group) {
return group->childCount();
}
return 0;
}
int CanvasTreeModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant CanvasTreeModel::data(const QModelIndex &index, int role) const
{
FGCanvasElement* e = static_cast<FGCanvasElement*>(index.internalPointer());
if (!e) {
return QVariant();
}
switch (role) {
case Qt::DisplayRole:
return e->property()->value("id", QVariant("<noid>"));
case Qt::CheckStateRole:
return e->property()->value("visible", true).toBool() ? Qt::Checked : Qt::Unchecked;
default:
break;
}
return QVariant();
}
bool CanvasTreeModel::hasChildren(const QModelIndex &parent) const
{
FGCanvasElement* e;
if (parent.isValid()) {
e = static_cast<FGCanvasElement*>(parent.internalPointer());
} else {
e = _root;
}
FGCanvasGroup* group = qobject_cast<FGCanvasGroup*>(e);
if (group) {
return group->hasChilden();
}
return false;
}
QModelIndex CanvasTreeModel::index(int row, int column, const QModelIndex &parent) const
{
FGCanvasGroup* group;
if (parent.isValid()) {
group = qobject_cast<FGCanvasGroup*>(static_cast<FGCanvasElement*>(parent.internalPointer()));
} else {
group = _root;
}
if (!group) {
return QModelIndex(); // invalid
}
if ((row < 0) || (row >= (int) group->childCount())) {
return QModelIndex(); // invalid
}
return createIndex(row, column, group->childAt(row));
}
QModelIndex CanvasTreeModel::parent(const QModelIndex &child) const
{
FGCanvasElement* e = static_cast<FGCanvasElement*>(child.internalPointer());
if (!child.isValid() || !e) {
return QModelIndex();
}
return indexForGroup(const_cast<FGCanvasGroup*>(e->parentGroup()));
}
Qt::ItemFlags CanvasTreeModel::flags(const QModelIndex &index) const
{
return QAbstractItemModel::flags(index) | Qt::ItemIsUserCheckable | Qt::ItemIsEditable;
}
bool CanvasTreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
FGCanvasElement* e = static_cast<FGCanvasElement*>(index.internalPointer());
if (!e) {
return false;
}
qDebug() << Q_FUNC_INFO;
if (role == Qt::CheckStateRole) {
e->property()->changeValue("visible", (value.toInt() == Qt::Checked));
emit dataChanged(index, index, QVector<int>() << Qt::CheckStateRole);
return true;
}
return false;
}
QModelIndex CanvasTreeModel::indexForGroup(FGCanvasGroup* group) const
{
if (!group) {
return QModelIndex();
}
if (group->parentGroup()) {
int prIndex = group->parentGroup()->indexOfChild(group);
return createIndex(prIndex, 0, group);
} else {
return createIndex(0, 0, group);
}
}
void CanvasTreeModel::onGroupChildAdded()
{
FGCanvasGroup* group = qobject_cast<FGCanvasGroup*>(sender());
int newChild = group->childCount() - 1;
FGCanvasGroup* childGroup = qobject_cast<FGCanvasGroup*>(group->childAt(newChild));
if (childGroup) {
connect(childGroup, &FGCanvasGroup::childAdded, this, &CanvasTreeModel::onGroupChildAdded);
}
beginInsertRows(indexForGroup(group),
newChild, newChild);
endInsertRows();
}
void CanvasTreeModel::onGroupChildRemoved(int index)
{
FGCanvasGroup* group = qobject_cast<FGCanvasGroup*>(sender());
beginRemoveRows(indexForGroup(group), index, index);
}
+56
View File
@@ -0,0 +1,56 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef CANVASTREEMODEL_H
#define CANVASTREEMODEL_H
#include <QAbstractItemModel>
#include "fgcanvasgroup.h"
class CanvasTreeModel : public QAbstractItemModel
{
public:
CanvasTreeModel(FGCanvasGroup* root);
FGCanvasElement* elementFromIndex(const QModelIndex& index) const;
protected:
virtual int rowCount(const QModelIndex &parent) const override;
virtual int columnCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
virtual bool hasChildren(const QModelIndex &parent) const override;
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const override;
virtual QModelIndex parent(const QModelIndex &child) const override;
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override;
private:
QModelIndex indexForGroup(FGCanvasGroup *group) const;
void onGroupChildAdded();
void onGroupChildRemoved(int index);
FGCanvasGroup* _root;
};
#endif // CANVASTREEMODEL_H
@@ -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
}
@@ -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"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
<h2>FlightGear Remote Canvas</h2>
<p>Remote-canvas connects to a running FlightGear instance via a WebSocket
connection. FlightGear must have been started with the <tt>--httpd</tt> option.
Enter the IP address or hostname of the computer running FlightGear, and
choose the 'Query' option to select a canvas.
</p>
<p>Several canvases can be added, moved and resized. Be patient when
connecting, especially over WiFi. A configuration of canvases and their sizes
can be saved and loaded, useful for dedicated setups.</p>
<p>Press and hold on a canvas to bring up a menu for it - this allows a
canvas to be closed, or to reconnect to the FlightGear instance</p>
+93
View File
@@ -0,0 +1,93 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "elementdatamodel.h"
#include "fgcanvaselement.h"
#include "localprop.h"
ElementDataModel::ElementDataModel(QObject* pr)
: QAbstractTableModel(pr)
, m_element(nullptr)
{
}
void ElementDataModel::setElement(FGCanvasElement *e)
{
beginResetModel();
m_element = e;
computeKeys();
endResetModel();
}
int ElementDataModel::rowCount(const QModelIndex &parent) const
{
return m_keys.size();
}
int ElementDataModel::columnCount(const QModelIndex &parent) const
{
return 2;
}
QVariant ElementDataModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || (index.row() >= m_keys.size())) {
return QVariant();
}
QByteArray key = m_keys.at(index.row());
if (role == Qt::DisplayRole) {
if (index.column() == 0) {
return key;
}
if (key == "position") {
return m_element->property()->position();
}
return m_element->property()->value(key.constData(), QVariant());
}
return QVariant();
}
void ElementDataModel::computeKeys()
{
m_keys.clear();
if (m_element == nullptr) {
return;
}
LocalProp *prop = m_element->property();
QByteArrayList directProps = QByteArrayList() << "fill" << "stroke" <<
"background" <<
"text" <<
"clip" << "file" << "src"
"font" << "character-size" <<
"z-index" << "visible";
Q_FOREACH (QByteArray b, directProps) {
if (prop->hasChild(b)) {
m_keys.append(b);
}
}
m_keys.append("position");
}
+47
View File
@@ -0,0 +1,47 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef ELEMENTDATAMODEL_H
#define ELEMENTDATAMODEL_H
#include <QAbstractTableModel>
class FGCanvasElement;
class ElementDataModel : public QAbstractTableModel
{
Q_OBJECT
public:
ElementDataModel(QObject* pr);
void setElement(FGCanvasElement* e);
virtual int rowCount(const QModelIndex &parent) const override;
virtual int columnCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
private:
void computeKeys();
FGCanvasElement* m_element;
QList<QByteArray> m_keys;
};
#endif // ELEMENTDATAMODEL_H
+62
View File
@@ -0,0 +1,62 @@
QT += core gui widgets gui-private quick websockets quick-private
CONFIG += c++11
TARGET = fgqcanvas
TEMPLATE = app
SOURCES += main.cpp\
WindowData.cpp \
fgcanvasgroup.cpp \
fgcanvaselement.cpp \
fgcanvaspaintcontext.cpp \
localprop.cpp \
fgcanvaspath.cpp \
fgcanvastext.cpp \
fgqcanvasmap.cpp \
fgqcanvasimage.cpp \
fgqcanvasfontcache.cpp \
fgqcanvasimageloader.cpp \
canvasitem.cpp \
canvasconnection.cpp \
applicationcontroller.cpp \
canvasdisplay.cpp \
canvaspainteddisplay.cpp \
jsonutils.cpp
HEADERS += \
WindowData.h \
fgcanvasgroup.h \
fgcanvaselement.h \
fgcanvaspaintcontext.h \
localprop.h \
fgcanvaspath.h \
fgcanvastext.h \
fgqcanvasmap.h \
fgqcanvasimage.h \
canvasconnection.h \
applicationcontroller.h \
canvasdisplay.h \
canvasitem.h \
fgqcanvasfontcache.h \
fgqcanvasimageloader.h \
canvaspainteddisplay.h \
jsonutils.h
RESOURCES += \
fgqcanvas_resources.qrc
OTHER_FILES += \
qml/* \
doc/* \
config/*
#Q_XCODE_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM
#Q_XCODE_DEVELOPMENT_TEAM.value = "James Turner"
#QMAKE_MAC_XCODE_SETTINGS += Q_XCODE_DEVELOPMENT_TEAM
ios {
QMAKE_INFO_PLIST = ios/Info.plist
}
+544
View File
@@ -0,0 +1,544 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgcanvaselement.h"
#include "localprop.h"
#include "fgcanvaspaintcontext.h"
#include "fgcanvasgroup.h"
#include "canvasitem.h"
#include "canvasconnection.h"
#include <QDebug>
#include <QPainter>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QMatrix4x4>
QTransform qTransformFromCanvas(LocalProp* prop)
{
double m[6] = { 1.0, 0.0, 0.0, 1.0, 0.0, 0.0 }; // identity matrix
for (unsigned int i =0; i< 6; ++i) {
LocalProp* mProp = prop->getOrCreateChildWithNameAndIndex(NameIndexTuple("m", i));
if (!mProp->value().isNull()) {
m[i] = mProp->value().toDouble();
}
}
return QTransform(m[0], m[1], 0.0,
m[2], m[3], 0.0,
m[4], m[5], 1.0);
}
bool FGCanvasElement::isStyleProperty(QByteArray name)
{
if ((name == "font") || (name == "line-height") || (name == "alignment")
|| (name == "character-size") || (name == "fill") || (name == "background")
|| (name == "fill-opacity"))
{
return true;
}
return false;
}
LocalProp *FGCanvasElement::property() const
{
return const_cast<LocalProp*>(_propertyRoot);
}
void FGCanvasElement::setHighlighted(bool hilighted)
{
_highlighted = hilighted;
}
bool FGCanvasElement::isHighlighted() const
{
return _highlighted;
}
CanvasItem *FGCanvasElement::createQuickItem(QQuickItem *parent)
{
Q_UNUSED(parent)
return nullptr;
}
CanvasItem *FGCanvasElement::quickItem() const
{
return nullptr;
}
FGCanvasElement::FGCanvasElement(FGCanvasGroup* pr, LocalProp* prop) :
QObject(pr),
_propertyRoot(prop),
_parent(pr)
{
connect(prop->getOrCreateWithPath("visible", true), &LocalProp::valueChanged,
this, &FGCanvasElement::onVisibleChanged);
connect(prop, &LocalProp::childAdded, this, &FGCanvasElement::onChildAdded);
connect(prop, &LocalProp::childRemoved, this, &FGCanvasElement::onChildRemoved);
connect(prop, &LocalProp::destroyed, this, &FGCanvasElement::onPropDestroyed);
if (pr) {
pr->markChildZIndicesDirty();
}
requestPolish();
}
void FGCanvasElement::onPropDestroyed()
{
doDestroy();
if (_parent) {
const_cast<FGCanvasGroup*>(_parent)->removeChild(this);
}
deleteLater();
}
void FGCanvasElement::requestPolish()
{
_polishRequired = true;
}
void FGCanvasElement::polish()
{
bool vis = isVisible();
auto qq = quickItem();
if (qq && (qq->isVisible() != vis)) {
qq->setVisible(vis);
}
if (!vis) {
return;
}
if (_clipDirty) {
_clipDirty = false;
if (qq) {
if (_hasClip) {
if (_clipFrame == ReferenceFrame::GLOBAL) {
qq->setClipReferenceFrameItem(rootGroup()->quickItem());
} else if (_clipFrame == ReferenceFrame::PARENT) {
qq->setClipReferenceFrameItem(parentGroup()->quickItem());
}
qq->setObjectName(_propertyRoot->path());
qq->setClip(_clipRect, _clipFrame);
} else {
qq->clearClip();
}
}
}
if (qq) {
qq->setTransform(combinedTransform());
}
if (_styleDirty) {
_fillColor = parseColorValue(getCascadedStyle("fill"));
const auto opacity = getCascadedStyle("fill-opacity");
if (!opacity.isNull()) {
_fillColor.setAlphaF(opacity.toReal());
}
_styleDirty = false;
}
doPolish();
_polishRequired = false;
}
void FGCanvasElement::dumpElement()
{
}
void FGCanvasElement::paint(FGCanvasPaintContext *context) const
{
if (!isVisible()) {
return;
}
QPainter* p = context->painter();
p->save();
QTransform combined = combinedTransform();
if (_hasClip)
{
QTransform t = p->transform();
// clip is defined in the global coordinate system
if (_clipFrame == ReferenceFrame::GLOBAL) {
// this rpelaces the transform entirely
p->setTransform(context->globalCoordinateTransform());
} else if (_clipFrame == ReferenceFrame::LOCAL) {
p->setTransform(combined, true /* combine */);
} else if (_clipFrame == ReferenceFrame::PARENT) {
// incoming transform is already our parent
} else {
qWarning() << "Unhandled clip type:" << static_cast<int>(_clipFrame) << "at" << property()->path();
}
#if defined(DEBUG_PAINTING)
p->setPen(Qt::yellow);
p->setBrush(QBrush(Qt::yellow, Qt::DiagCrossPattern));
p->drawRect(_clipRect);
#endif
p->setClipping(true);
p->setClipRect(_clipRect);
p->setTransform(t); // restore the previous transformation
}
p->setTransform(combined, true /* combine */);
if (!_fillColor.isValid()) {
p->setBrush(Qt::NoBrush);
} else {
p->setBrush(_fillColor);
}
doPaint(context);
if (_hasClip) {
p->setClipping(false);
}
p->restore();
}
void FGCanvasElement::doPaint(FGCanvasPaintContext* context) const
{
Q_UNUSED(context);
}
void FGCanvasElement::doPolish()
{
}
QTransform FGCanvasElement::combinedTransform() const
{
if (_transformsDirty) {
_combinedTransform.reset();
for (LocalProp* tfProp : _propertyRoot->childrenWithName("tf")) {
_combinedTransform *= qTransformFromCanvas(tfProp);
}
#if 0
QPointF offset(_propertyRoot->value("center-offset-x", 0.0).toFloat(),
_propertyRoot->value("center-offset-y", 0.0).toFloat());
_combinedTransform.translate(offset.x(), offset.y());
#endif
_transformsDirty = false;
}
return _combinedTransform;
}
bool FGCanvasElement::isVisible() const
{
return _visible;
}
int FGCanvasElement::zIndex() const
{
return _zIndex;
}
const FGCanvasGroup *FGCanvasElement::parentGroup() const
{
return _parent;
}
const FGCanvasGroup *FGCanvasElement::rootGroup() const
{
if (!_parent) {
return qobject_cast<const FGCanvasGroup*>(this);
}
return _parent->rootGroup();
}
CanvasConnection *FGCanvasElement::connection() const
{
if (_parent)
return _parent->connection();
return qobject_cast<CanvasConnection*>(parent());
}
bool FGCanvasElement::onChildAdded(LocalProp *prop)
{
const QByteArray nm = prop->name();
if (nm == "tf") {
connect(prop, &LocalProp::childAdded, this, &FGCanvasElement::onChildAdded);
return true;
} else if (nm == "visible") {
return true;
} else if (nm == "tf-rot-index") {
// ignored, this is noise from the Nasal SVG parser
return true;
} else if (nm.startsWith("center-offset-")) {
// ignored, this is noise from the Nasal SVG parser
return true;
} else if (nm == "center") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::onCenterChanged);
return true;
} else if (nm == "m") {
if ((prop->parent()->name() == "tf") && (prop->parent()->parent() == _propertyRoot)) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markTransformsDirty);
return true;
} else {
qWarning() << "saw confusing 'm' property" << prop->path();
}
} else if (nm == "m-geo") {
// ignore for now, we do geo projection server-side
return true;
} else if (nm == "id") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markSVGIDDirty);
return true;
} else if (nm == "update") {
// disable updates optionally?
return true;
} else if ((nm == "clip") || (nm == "clip-frame")) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markClipDirty);
return true;
}
if (isStyleProperty(nm)) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markStyleDirty);
return true;
}
if (nm == "symbol-type") {
// ignored for now
return true;
}
if (nm == "layer-type") {
connect(prop, &LocalProp::valueChanged, [this](QVariant value)
{qDebug() << "layer-type:" << value.toByteArray() << "on" << _propertyRoot->path(); });
return true;
} else if (nm == "z-index") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markZIndexDirty);
return true;
}
return false;
}
bool FGCanvasElement::onChildRemoved(LocalProp *prop)
{
const QByteArray nm = prop->name();
if ((nm == "tf") || (nm == "m")) {
markTransformsDirty();
return true;
}
return false;
}
void FGCanvasElement::doDestroy()
{
}
QColor FGCanvasElement::fillColor() const
{
return _fillColor;
}
void FGCanvasElement::onCenterChanged(QVariant value)
{
LocalProp* senderProp = static_cast<LocalProp*>(sender());
const unsigned int centerTerm = senderProp->index();
if (centerTerm == 0) {
_center.setX(value.toReal());
} else {
_center.setY(value.toReal());
}
requestPolish();
}
void FGCanvasElement::markTransformsDirty()
{
_transformsDirty = true;
requestPolish();
}
void FGCanvasElement::markClipDirty()
{
_clipDirty = true;
parseCSSClip(_propertyRoot->value("clip", QVariant()).toByteArray());
_clipFrame = static_cast<ReferenceFrame>(_propertyRoot->value("clip-frame", 0).toInt());
requestPolish();
}
double FGCanvasElement::parseCSSValue(QByteArray value) const
{
value = value.trimmed();
// deal with %, px suffixes
if (value.indexOf('%') >= 0) {
qWarning() << Q_FUNC_INFO << "extend parsing to deal with:" << value;
}
if (value.endsWith("px")) {
value.truncate(value.length() - 2);
}
bool ok = false;
qreal v = value.toDouble(&ok);
if (!ok) {
qWarning() << "failed to parse:" << value;
}
return v;
}
QColor FGCanvasElement::parseColorValue(QVariant value) const
{
QString colorString = value.toString();
if (colorString.isEmpty() || (colorString == QStringLiteral("none"))) {
return QColor(); // return an invalid color
}
int alpha = 255;
int red = 0;
int green = 0;
int blue = 0;
bool good = false;
if (colorString.startsWith('#')) {
// web style
if (colorString.length() == 9) {
QRegularExpression re("#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})");
QRegularExpressionMatch match = re.match(colorString);
if (match.hasMatch()) {
red = match.captured(1).toInt(nullptr, 16);
green = match.captured(2).toInt(nullptr, 16);
blue = match.captured(3).toInt(nullptr, 16);
alpha = match.captured(4).toInt(nullptr, 16);
good = true;
}
} else if (colorString.length() == 7) {
// long form, RGB
QRegularExpression re("#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})");
QRegularExpressionMatch match = re.match(colorString);
if (match.hasMatch()) {
red = match.captured(1).toInt(nullptr, 16);
green = match.captured(2).toInt(nullptr, 16);
blue = match.captured(3).toInt(nullptr, 16);
good = true;
}
}
} else if (colorString.startsWith("rgb")) {
// try rgb(ddd, ddd, ddd) syntax
QRegularExpression re("rgb\\((\\d*),(\\d*),(\\d*)\\)");
QRegularExpressionMatch match = re.match(value.toString());
if (match.hasMatch()) {
red = match.captured(1).toInt();
green = match.captured(2).toInt();
blue = match.captured(3).toInt();
good = true;
}
QRegularExpression re2("rgba\\((\\d*),(\\d*),(\\d*),(\\d*)\\)");
match = re2.match(value.toString());
if (match.hasMatch()) {
red = match.captured(1).toInt();
green = match.captured(2).toInt();
blue = match.captured(3).toInt();
alpha = match.captured(4).toInt() * 255;
good = true;
}
}
if (good) {
return QColor(red, green, blue, alpha);
}
qWarning() << _propertyRoot->path() << "failed to parse color:" << colorString;
return Qt::magenta; // default horrible colour
}
void FGCanvasElement::markStyleDirty()
{
_styleDirty = true;
requestPolish();
// group will cascade
}
QVariant FGCanvasElement::getCascadedStyle(const char *name, QVariant defaultValue) const
{
LocalProp* style = _propertyRoot->childWithNameAndIndex(NameIndexTuple(name, 0));
if (style) {
return style->value();
}
if (_parent) {
return _parent->getCascadedStyle(name);
}
return defaultValue;
}
void FGCanvasElement::markZIndexDirty(QVariant value)
{
_zIndex = value.toInt();
_parent->markChildZIndicesDirty();
}
void FGCanvasElement::markSVGIDDirty(QVariant value)
{
_svgElementId = value.toByteArray();
}
void FGCanvasElement::onVisibleChanged(QVariant value)
{
_visible = value.toBool();
requestPolish();
}
void FGCanvasElement::parseCSSClip(QByteArray value)
{
if (value.isEmpty()) {
_hasClip = false;
return;
}
// https://www.w3.org/wiki/CSS/Properties/clip for the stupid order here
if (value.startsWith("rect(")) {
int closingParen = value.indexOf(')');
value = value.mid(5, closingParen - 5); // trim front portion
}
QByteArrayList clipRectDesc = value.split(',');
const int parts = clipRectDesc.size();
if (parts != 4) {
qWarning() << "implement parsing for non-standard clip" << value;
return;
}
const qreal top = parseCSSValue(clipRectDesc.at(0));
const qreal right = parseCSSValue(clipRectDesc.at(1));
const qreal bottom = parseCSSValue(clipRectDesc.at(2));
const qreal left = parseCSSValue(clipRectDesc.at(3));
_clipRect = QRectF(left, top, right - left, bottom - top);
// qDebug() << "final clip rect:" << _clipRect << "from" << value;
_hasClip = true;
requestPolish();
}
+148
View File
@@ -0,0 +1,148 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGCANVASELEMENT_H
#define FGCANVASELEMENT_H
#include <QObject>
#include <QTransform>
#include <QColor>
#include <QVariant>
#include <vector>
class LocalProp;
class FGCanvasPaintContext;
class FGCanvasGroup;
class CanvasItem;
class QQuickItem;
class CanvasConnection;
/**
* Coordinate reference frame (eg. "clip" property)
*/
enum class ReferenceFrame
{
GLOBAL, ///< Global coordinates
PARENT, ///< Coordinates relative to parent coordinate frame
LOCAL ///< Coordinates relative to local coordinates (parent
/// coordinates with local transformations applied)
};
class FGCanvasElement : public QObject
{
Q_OBJECT
public:
explicit FGCanvasElement(FGCanvasGroup* pr, LocalProp* prop);
void paint(FGCanvasPaintContext* context) const;
QTransform combinedTransform() const;
bool isVisible() const;
int zIndex() const;
const FGCanvasGroup* parentGroup() const;
const FGCanvasGroup* rootGroup() const;
CanvasConnection* connection() const;
static bool isStyleProperty(QByteArray name);
LocalProp* property() const;
void setHighlighted(bool hilighted);
bool isHighlighted() const;
virtual CanvasItem* quickItem() const;
virtual CanvasItem* createQuickItem(QQuickItem* parent);
void requestPolish();
void polish();
virtual void dumpElement() = 0;
protected:
virtual void doPaint(FGCanvasPaintContext* context) const;
virtual void doPolish();
virtual bool onChildAdded(LocalProp* prop);
virtual bool onChildRemoved(LocalProp* prop);
virtual void doDestroy();
const LocalProp* _propertyRoot;
const FGCanvasGroup* _parent;
QColor fillColor() const;
QColor parseColorValue(QVariant value) const;
virtual void markStyleDirty();
QVariant getCascadedStyle(const char* name, QVariant defaultValue = QVariant()) const;
private slots:
void onPropDestroyed();
private:
void onCenterChanged(QVariant value);
void markTransformsDirty();
void markZIndexDirty(QVariant value);
void onVisibleChanged(QVariant value);
void markClipDirty();
void markSVGIDDirty(QVariant value);
private:
friend class FGCanvasGroup;
bool _polishRequired = false;
bool _visible = true;
bool _highlighted = false;
mutable bool _transformsDirty = true;
mutable bool _styleDirty = true;
mutable QTransform _combinedTransform;
QPointF _center;
mutable QColor _fillColor;
int _zIndex = 0;
QByteArray _svgElementId;
mutable bool _clipDirty = true;
mutable bool _hasClip = false;
mutable QRectF _clipRect;
mutable ReferenceFrame _clipFrame = ReferenceFrame::GLOBAL;
void parseCSSClip(QByteArray value);
double parseCSSValue(QByteArray value) const;
};
using FGCanvasElementVec = std::vector<FGCanvasElement*>;
#endif // FGCANVASELEMENT_H
+247
View File
@@ -0,0 +1,247 @@
#include "fgcanvasgroup.h"
#include <QDebug>
#include "canvasitem.h"
#include "localprop.h"
#include "fgcanvaspaintcontext.h"
#include "fgcanvaspath.h"
#include "fgcanvastext.h"
#include "fgqcanvasmap.h"
#include "fgqcanvasimage.h"
class ChildOrderingFunction
{
public:
bool operator()(const FGCanvasElement* a, FGCanvasElement* b)
{
if (a->zIndex() == b->zIndex()) {
// use prop node positions in the parent
return a->property()->position() < b->property()->position();
}
return a->zIndex() < b->zIndex();
}
};
FGCanvasGroup::FGCanvasGroup(FGCanvasGroup* pr, LocalProp* prop) :
FGCanvasElement(pr, prop)
{
}
const FGCanvasElementVec &FGCanvasGroup::children() const
{
return _children;
}
void FGCanvasGroup::markChildZIndicesDirty() const
{
_zIndicesDirty = true;
}
bool FGCanvasGroup::hasChilden() const
{
return !_children.empty();
}
unsigned int FGCanvasGroup::childCount() const
{
return _children.size();
}
FGCanvasElement *FGCanvasGroup::childAt(unsigned int index) const
{
return _children.at(index);
}
unsigned int FGCanvasGroup::indexOfChild(const FGCanvasElement *e) const
{
auto it = std::find(_children.begin(), _children.end(), e);
if (it == _children.end()) {
qWarning() << Q_FUNC_INFO << "not found";
return 0;
}
return std::distance(_children.begin(), it);
}
CanvasItem *FGCanvasGroup::createQuickItem(QQuickItem *parent)
{
_quick = new CanvasItem(parent);
for (auto e : _children) {
e->createQuickItem(_quick);
}
return _quick;
}
void FGCanvasGroup::doPaint(FGCanvasPaintContext *context) const
{
for (FGCanvasElement* element : _children) {
element->paint(context);
}
}
void FGCanvasGroup::doPolish()
{
if (_cachedSymbolDirty) {
qDebug() << _propertyRoot->path() << "should use symbol cache:" << _propertyRoot->value("symbol-type", QVariant()).toByteArray();
_cachedSymbolDirty = false;
}
if (_zIndicesDirty) {
std::sort(_children.begin(), _children.end(), ChildOrderingFunction());
_zIndicesDirty = false;
resetChildQuickItemZValues();
}
for (FGCanvasElement* element : _children) {
element->polish();
}
}
void FGCanvasGroup::resetChildQuickItemZValues()
{
int counter = 0;
for (auto e : _children) {
auto qq = e->quickItem();
if (qq) {
qq->setZ(counter++);
}
}
}
bool FGCanvasGroup::onChildAdded(LocalProp *prop)
{
const bool isRootGroup = (_parent == nullptr);
int newChildCount = 0;
if (FGCanvasElement::onChildAdded(prop)) {
return true;
}
const QByteArray nm = prop->name();
if (nm == "group") {
_children.push_back(new FGCanvasGroup(this, prop));
newChildCount++;
} else if (nm == "path") {
_children.push_back(new FGCanvasPath(this, prop));
newChildCount++;
} else if (nm == "text") {
_children.push_back(new FGCanvasText(this, prop));
newChildCount++;
} else if (nm == "image") {
_children.push_back(new FGQCanvasImage(this, prop));
newChildCount++;
} else if (nm == "map") {
_children.push_back(new FGQCanvasMap(this, prop));
newChildCount++;
} else if (nm == "symbol-type") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasGroup::markCachedSymbolDirty);
return true;
}
if (isRootGroup) {
// ignore all of these, handled by the enclosing canvas view
if (nm == "size") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasGroup::canvasSizeChanged);
return true;
}
if ((nm == "view") || nm.startsWith("status") || (nm == "name") || (nm == "mipmapping") || (nm == "placement")) {
return true;
}
}
if (newChildCount > 0) {
markChildZIndicesDirty();
if (_quick) {
_children.back()->createQuickItem(_quick);
}
emit childAdded();
return true;
}
qDebug() << "saw unknown group child" << prop->name();
return false;
}
bool FGCanvasGroup::onChildRemoved(LocalProp *prop)
{
if (FGCanvasElement::onChildRemoved(prop)) {
return true;
}
const QByteArray nm = prop->name();
if ((nm == "group") || (nm == "image") || (nm == "path") || (nm == "text") || (nm == "map")) {
int removedChildIndex = indexOfChildWithProp(prop);
if (removedChildIndex >= 0) {
auto it = _children.begin() + removedChildIndex;
delete *it;
_children.erase(it);
emit childRemoved(removedChildIndex);
}
return true;
}
return false;
}
void FGCanvasGroup::removeChild(FGCanvasElement *child)
{
auto it = std::find(_children.begin(), _children.end(), child);
if (it != _children.end()) {
int index = std::distance(_children.begin(), it);
_children.erase(it);
emit childRemoved(index);
}
}
void FGCanvasGroup::dumpElement()
{
qDebug() << "Group at" << _propertyRoot->path();
for (auto c : _children) {
c->dumpElement();
}
qDebug() << "End-group at" << _propertyRoot->path();
}
int FGCanvasGroup::indexOfChildWithProp(LocalProp* prop) const
{
auto it = std::find_if(_children.begin(), _children.end(), [prop](FGCanvasElement* child)
{
return (child->property() == prop);
});
if (it == _children.end()) {
return -1;
}
return std::distance(_children.begin(), it);
}
void FGCanvasGroup::markStyleDirty()
{
for (FGCanvasElement* element : _children) {
element->markStyleDirty();
}
}
void FGCanvasGroup::doDestroy()
{
delete _quick;
FGCanvasElementVec children = std::move(_children);
_children.clear();
for (auto c : children) {
delete c;
}
}
void FGCanvasGroup::markCachedSymbolDirty()
{
_cachedSymbolDirty = true;
}
+61
View File
@@ -0,0 +1,61 @@
#ifndef FGCANVASGROUP_H
#define FGCANVASGROUP_H
#include "fgcanvaselement.h"
class FGCanvasGroup : public FGCanvasElement
{
Q_OBJECT
public:
explicit FGCanvasGroup(FGCanvasGroup* pr, LocalProp* prop);
const FGCanvasElementVec& children() const;
void markChildZIndicesDirty() const;
bool hasChilden() const;
unsigned int childCount() const;
FGCanvasElement* childAt(unsigned int index) const;
unsigned int indexOfChild(const FGCanvasElement* e) const;
CanvasItem* createQuickItem(QQuickItem *parent) override;
CanvasItem* quickItem() const override
{ return _quick; }
void removeChild(FGCanvasElement* child);
void dumpElement() override;
signals:
void childAdded();
void childRemoved(int index);
void canvasSizeChanged();
protected:
virtual void doPaint(FGCanvasPaintContext* context) const override;
void doPolish() override;
bool onChildAdded(LocalProp *prop) override;
bool onChildRemoved(LocalProp *prop) override;
virtual void markStyleDirty() override;
void doDestroy() override;
private:
void markCachedSymbolDirty();
int indexOfChildWithProp(LocalProp *prop) const;
void resetChildQuickItemZValues();
private:
mutable FGCanvasElementVec _children;
mutable bool _zIndicesDirty = false;
mutable bool _cachedSymbolDirty = false;
CanvasItem* _quick = nullptr;
};
#endif // FGCANVASGROUP_H
+24
View File
@@ -0,0 +1,24 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgcanvaspaintcontext.h"
FGCanvasPaintContext::FGCanvasPaintContext(QPainter* painter) :
_painter(painter)
{
_globalCoordsTransform = painter->transform();
}
+41
View File
@@ -0,0 +1,41 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGCANVASPAINTCONTEXT_H
#define FGCANVASPAINTCONTEXT_H
#include <QPainter>
class FGCanvasPaintContext
{
public:
FGCanvasPaintContext(QPainter* painter);
QPainter* painter() const
{ return _painter; }
QTransform globalCoordinateTransform() const
{
return _globalCoordsTransform;
}
private:
QPainter* _painter;
QTransform _globalCoordsTransform;
};
#endif // FGCANVASPAINTCONTEXT_H
+995
View File
@@ -0,0 +1,995 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgcanvaspath.h"
#include <cctype>
#include <QByteArrayList>
#include <QPainter>
#include <QDebug>
#include <QtMath>
#include <QPen>
#include "fgcanvaspaintcontext.h"
#include "localprop.h"
#include "canvasitem.h"
#include "private/qtriangulator_p.h" // private QtGui header
#include "private/qtriangulatingstroker_p.h" // private QtGui header
#include "private/qvectorpath_p.h" // private QtGui header
#include <QSGGeometry>
#include <QSGGeometryNode>
#include <QSGFlatColorMaterial>
class PathQuickItem : public CanvasItem
{
Q_OBJECT
Q_PROPERTY(QColor fillColor READ fillColor WRITE setFillColor NOTIFY fillColorChanged)
Q_PROPERTY(QPen stroke READ stroke WRITE setStroke NOTIFY strokeChanged)
public:
PathQuickItem(QQuickItem* parent)
: CanvasItem(parent)
{
setFlag(ItemHasContents);
}
void setPath(QPainterPath pp)
{
m_path = pp;
QRectF pathBounds = pp.boundingRect();
setImplicitSize(pathBounds.width(), pathBounds.height());
update(); // request a paint node update
}
QSGNode* updateRealPaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData *) override
{
if (m_path.isEmpty()) {
return nullptr;
}
delete oldNode;
QSGGeometryNode* fillGeom = nullptr;
QSGGeometryNode* strokeGeom = nullptr;
if (m_fillColor.isValid()) {
// TODO: compute LOD for qTriangulate based on world transform
QTransform transform;
QTriangleSet triangles = qTriangulate(m_path, transform);
int indexType = GL_UNSIGNED_SHORT;
if (triangles.indices.type() == QVertexIndexVector::UnsignedShort) {
// default is fine
} else if (triangles.indices.type() == QVertexIndexVector::UnsignedInt) {
indexType = GL_UNSIGNED_INT;
} else {
qFatal("Unsupported triangle index type");
}
QSGGeometry* sgGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),
triangles.vertices.size() >> 1,
triangles.indices.size(),
indexType);
sgGeom->setIndexDataPattern(QSGGeometry::StaticPattern);
sgGeom->setDrawingMode(GL_TRIANGLES);
//
QSGGeometry::Point2D *points = sgGeom->vertexDataAsPoint2D();
for (int v=0; v < triangles.vertices.size(); ) {
const float vx = triangles.vertices.at(v++);
const float vy = triangles.vertices.at(v++);
(points++)->set(vx, vy);
}
if (triangles.indices.type() == QVertexIndexVector::UnsignedShort) {
quint16* indices = sgGeom->indexDataAsUShort();
::memcpy(indices, triangles.indices.data(), sizeof(unsigned short) * triangles.indices.size());
} else {
quint32* indices = sgGeom->indexDataAsUInt();
::memcpy(indices, triangles.indices.data(), sizeof(quint32) * triangles.indices.size());
}
// create the node now, pretty trivial
fillGeom = new QSGGeometryNode;
fillGeom->setGeometry(sgGeom);
fillGeom->setFlag(QSGNode::OwnsGeometry);
QSGFlatColorMaterial* mat = new QSGFlatColorMaterial();
mat->setColor(m_fillColor);
fillGeom->setMaterial(mat);
fillGeom->setFlag(QSGNode::OwnsMaterial);
}
if (m_stroke.style() != Qt::NoPen) {
const QVectorPath& vp = qtVectorPathForPath(m_path);
QRectF clipBounds;
QTriangulatingStroker ts;
QPainter::RenderHints renderHints;
if (m_stroke.style() == Qt::SolidLine) {
ts.process(vp, m_stroke, clipBounds, renderHints);
#if 0
inline int vertexCount() const { return m_vertices.size(); }
inline const float *vertices() const { return m_vertices.data(); }
#endif
} else {
QDashedStrokeProcessor dasher;
dasher.process(vp, m_stroke, clipBounds, renderHints);
QVectorPath dashStroke(dasher.points(),
dasher.elementCount(),
dasher.elementTypes(),
renderHints);
ts.process(dashStroke, m_stroke, clipBounds, renderHints);
}
QSGGeometry* sgGeom = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),
ts.vertexCount() >> 1);
sgGeom->setVertexDataPattern(QSGGeometry::StaticPattern);
sgGeom->setDrawingMode(GL_TRIANGLE_STRIP);
QSGGeometry::Point2D *points = sgGeom->vertexDataAsPoint2D();
const float* vPtr = ts.vertices();
for (int v=0; v < ts.vertexCount(); v += 2) {
const float vx = *vPtr++;
const float vy = *vPtr++;
(points++)->set(vx, vy);
}
// create the node now, pretty trivial
strokeGeom = new QSGGeometryNode;
strokeGeom->setGeometry(sgGeom);
strokeGeom->setFlag(QSGNode::OwnsGeometry);
QSGFlatColorMaterial* mat = new QSGFlatColorMaterial();
mat->setColor(m_stroke.color());
strokeGeom->setMaterial(mat);
strokeGeom->setFlag(QSGNode::OwnsMaterial);
}
if (fillGeom && strokeGeom) {
QSGNode* groupNode = new QSGNode;
groupNode->appendChildNode(fillGeom);
groupNode->appendChildNode(strokeGeom);
return groupNode;
} else if (fillGeom) {
return fillGeom;
}
return strokeGeom;
}
QColor fillColor() const
{
return m_fillColor;
}
QPen stroke() const
{
return m_stroke;
}
public slots:
void setFillColor(QColor fillColor)
{
if (m_fillColor == fillColor)
return;
m_fillColor = fillColor;
emit fillColorChanged(fillColor);
update();
}
void setStroke(QPen stroke)
{
if (m_stroke == stroke)
return;
m_stroke = stroke;
emit strokeChanged(stroke);
update();
}
signals:
void fillColorChanged(QColor fillColor);
void strokeChanged(QPen stroke);
protected:
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override
{
QQuickItem::geometryChanged(newGeometry, oldGeometry);
update();
}
QRectF boundingRect() const override
{
if ((width() == 0.0) || (height() == 0.0)) {
return QRectF(0.0, 0.0, implicitWidth(), implicitHeight());
}
return QQuickItem::boundingRect();
}
private:
QPainterPath m_path;
QColor m_fillColor;
QPen m_stroke;
};
static void pathArcSegment(QPainterPath &path,
qreal xc, qreal yc,
qreal th0, qreal th1,
qreal rx, qreal ry, qreal xAxisRotation)
{
qreal sinTh, cosTh;
qreal a00, a01, a10, a11;
qreal x1, y1, x2, y2, x3, y3;
qreal t;
qreal thHalf;
sinTh = qSin(xAxisRotation * (M_PI / 180.0));
cosTh = qCos(xAxisRotation * (M_PI / 180.0));
a00 = cosTh * rx;
a01 = -sinTh * ry;
a10 = sinTh * rx;
a11 = cosTh * ry;
thHalf = 0.5 * (th1 - th0);
t = (8.0 / 3.0) * qSin(thHalf * 0.5) * qSin(thHalf * 0.5) / qSin(thHalf);
x1 = xc + qCos(th0) - t * qSin(th0);
y1 = yc + qSin(th0) + t * qCos(th0);
x3 = xc + qCos(th1);
y3 = yc + qSin(th1);
x2 = x3 + t * qSin(th1);
y2 = y3 - t * qCos(th1);
path.cubicTo(a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3);
}
// the arc handling code underneath is from XSVG (BSD license)
/*
* Copyright 2002 USC/Information Sciences Institute
*
* Permission to use, copy, modify, distribute, and sell this software
* and its documentation for any purpose is hereby granted without
* fee, provided that the above copyright notice appear in all copies
* and that both that copyright notice and this permission notice
* appear in supporting documentation, and that the name of
* Information Sciences Institute not be used in advertising or
* publicity pertaining to distribution of the software without
* specific, written prior permission. Information Sciences Institute
* makes no representations about the suitability of this software for
* any purpose. It is provided "as is" without express or implied
* warranty.
*
* INFORMATION SCIENCES INSTITUTE DISCLAIMS ALL WARRANTIES WITH REGARD
* TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL INFORMATION SCIENCES
* INSTITUTE BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
* OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
static void pathArc(QPainterPath &path,
qreal rx,
qreal ry,
qreal x_axis_rotation,
int large_arc_flag,
int sweep_flag,
qreal x,
qreal y,
qreal curx, qreal cury)
{
qreal sin_th, cos_th;
qreal a00, a01, a10, a11;
qreal x0, y0, x1, y1, xc, yc;
qreal d, sfactor, sfactor_sq;
qreal th0, th1, th_arc;
int i, n_segs;
qreal dx, dy, dx1, dy1, Pr1, Pr2, Px, Py, check;
rx = qAbs(rx);
ry = qAbs(ry);
sin_th = qSin(x_axis_rotation * (M_PI / 180.0));
cos_th = qCos(x_axis_rotation * (M_PI / 180.0));
dx = (curx - x) / 2.0;
dy = (cury - y) / 2.0;
dx1 = cos_th * dx + sin_th * dy;
dy1 = -sin_th * dx + cos_th * dy;
Pr1 = rx * rx;
Pr2 = ry * ry;
Px = dx1 * dx1;
Py = dy1 * dy1;
/* Spec : check if radii are large enough */
check = Px / Pr1 + Py / Pr2;
if (check > 1) {
rx = rx * qSqrt(check);
ry = ry * qSqrt(check);
}
a00 = cos_th / rx;
a01 = sin_th / rx;
a10 = -sin_th / ry;
a11 = cos_th / ry;
x0 = a00 * curx + a01 * cury;
y0 = a10 * curx + a11 * cury;
x1 = a00 * x + a01 * y;
y1 = a10 * x + a11 * y;
/* (x0, y0) is current point in transformed coordinate space.
(x1, y1) is new point in transformed coordinate space.
The arc fits a unit-radius circle in this space.
*/
d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0);
sfactor_sq = 1.0 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
sfactor = qSqrt(sfactor_sq);
if (sweep_flag == large_arc_flag) sfactor = -sfactor;
xc = 0.5 * (x0 + x1) - sfactor * (y1 - y0);
yc = 0.5 * (y0 + y1) + sfactor * (x1 - x0);
/* (xc, yc) is center of the circle. */
th0 = qAtan2(y0 - yc, x0 - xc);
th1 = qAtan2(y1 - yc, x1 - xc);
th_arc = th1 - th0;
if (th_arc < 0 && sweep_flag)
th_arc += 2 * M_PI;
else if (th_arc > 0 && !sweep_flag)
th_arc -= 2 * M_PI;
n_segs = qCeil(qAbs(th_arc / (M_PI * 0.5 + 0.001)));
for (i = 0; i < n_segs; i++) {
pathArcSegment(path, xc, yc,
th0 + i * th_arc / n_segs,
th0 + (i + 1) * th_arc / n_segs,
rx, ry, x_axis_rotation);
}
}
///////////////////////////////////////////////////////////////////////////////
FGCanvasPath::FGCanvasPath(FGCanvasGroup* pr, LocalProp* prop) :
FGCanvasElement(pr, prop)
{
}
void FGCanvasPath::dumpElement()
{
qDebug() << "Path: at " << _propertyRoot->path();
}
void FGCanvasPath::doPaint(FGCanvasPaintContext *context) const
{
context->painter()->setPen(_stroke);
switch (_paintType) {
case Rect:
context->painter()->drawRect(_rect);
break;
case RoundRect:
context->painter()->drawRoundRect(_rect, _roundRectRadius.width(), _roundRectRadius.height());
break;
case Path:
context->painter()->drawPath(_painterPath);
break;
}
if (isHighlighted()) {
context->painter()->setPen(QPen(Qt::red, 1));
context->painter()->setBrush(Qt::NoBrush);
switch (_paintType) {
case Rect:
case RoundRect:
context->painter()->drawRect(_rect);
break;
case Path:
context->painter()->drawRect(_painterPath.boundingRect());
break;
}
}
}
void FGCanvasPath::doPolish()
{
if (_pathDirty) {
rebuildPath();
if (_quickPath) {
_quickPath->setPath(_painterPath);
}
_pathDirty = false;
}
if (_penDirty) {
rebuildPen();
if (_quickPath) {
_quickPath->setStroke(_stroke);
}
_penDirty = false;
}
if (_quickPath) {
_quickPath->setFillColor(fillColor());
}
}
void FGCanvasPath::markStyleDirty()
{
_penDirty = true;
}
CanvasItem *FGCanvasPath::createQuickItem(QQuickItem *parent)
{
_quickPath = new PathQuickItem(parent);
_quickPath->setPath(_painterPath);
_quickPath->setStroke(_stroke);
_quickPath->setAntialiasing(true);
return _quickPath;
}
CanvasItem *FGCanvasPath::quickItem() const
{
return _quickPath;
}
void FGCanvasPath::doDestroy()
{
delete _quickPath;
}
void FGCanvasPath::markPathDirty()
{
_pathDirty = true;
requestPolish();
}
void FGCanvasPath::markStrokeDirty()
{
_penDirty = true;
requestPolish();
}
bool FGCanvasPath::onChildAdded(LocalProp *prop)
{
if (FGCanvasElement::onChildAdded(prop)) {
return true;
}
if ((prop->name() == "cmd") || (prop->name() == "coord") || (prop->name() == "svg")) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasPath::markPathDirty);
return true;
}
if (prop->name() == "rect") {
_isRect = true;
connect(prop, &LocalProp::childAdded, this, &FGCanvasPath::onChildAdded);
return true;
}
// handle rect property changes
if (prop->parent()->name() == "rect") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasPath::markPathDirty);
return true;
}
if (prop->name().startsWith("border-")) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasPath::markPathDirty);
return true;
}
if ((prop->name() == "cmd-geo") || (prop->name() == "coord-geo")) {
// ignore for now, we let the server-side transform down to cartesian.
// if we move that work to client side we could skip sending the cmd/coord data
return true;
}
if (prop->name().startsWith("stroke")) {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasPath::markStrokeDirty);
return true;
}
qWarning() << "path saw unrecognized child:" << prop->name() << prop->index();
return false;
}
bool FGCanvasPath::onChildRemoved(LocalProp* prop)
{
if (FGCanvasElement::onChildRemoved(prop)) {
return true;
}
const auto name = prop->name();
if (name == "rect") {
_isRect = false;
markPathDirty();
return true;
}
if ((name == "cmd") || (name == "coord") || (name == "svg")) {
markPathDirty();
return true;
}
if ((name == "cmd-geo") || (name == "coord-geo")) {
// ignored
return true;
}
if (name.startsWith("stroke")) {
markStrokeDirty();
return true;
}
return false;
}
typedef enum
{
PathClose = ( 0 << 1),
PathMoveTo = ( 1 << 1),
PathLineTo = ( 2 << 1),
PathHLineTo = ( 3 << 1),
PathVLineTo = ( 4 << 1),
PathQuadTo = ( 5 << 1),
PathCubicTo = ( 6 << 1),
PathSmoothQuadTo = ( 7 << 1),
PathSmoothCubicTo = ( 8 << 1),
PathShortCCWArc = ( 9 << 1),
PathShortCWArc = (10 << 1),
PathLongCCWArc = (11 << 1),
PathLongCWArc = (12 << 1)
} PathCommands;
static const quint8 CoordsPerCommand[] = {
0, /* VG_CLOSE_PATH */
2, /* VG_MOVE_TO */
2, /* VG_LINE_TO */
1, /* VG_HLINE_TO */
1, /* VG_VLINE_TO */
4, /* VG_QUAD_TO */
6, /* VG_CUBIC_TO */
2, /* VG_SQUAD_TO */
4, /* VG_SCUBIC_TO */
5, /* VG_SCCWARC_TO */
5, /* VG_SCWARC_TO */
5, /* VG_LCCWARC_TO */
5 /* VG_LCWARC_TO */
};
void FGCanvasPath::rebuildPath() const
{
std::vector<float> coords;
std::vector<int> commands;
if (_isRect) {
rebuildFromRect(commands, coords);
} else if (_propertyRoot->hasChild("svg")) {
if (!rebuildFromSVGData(commands, coords)) {
qWarning() << "failed to parse SVG path data" << _propertyRoot->value("svg", QVariant());
}
} else {
for (QVariant v : _propertyRoot->valuesOfChildren("coord")) {
coords.push_back(v.toFloat());
}
for (QVariant v : _propertyRoot->valuesOfChildren("cmd")) {
commands.push_back(v.toInt());
}
}
rebuildPathFromCommands(commands, coords);
}
QByteArrayList splitSVGPathData(QByteArray d)
{
QByteArrayList result;
size_t pos = 0;
std::string strData(d.data());
const char* seperators = "\n\r\t ,";
size_t startPos = strData.find_first_not_of(seperators, 0);
for(;;)
{
pos = strData.find_first_of(seperators, startPos);
if (pos == std::string::npos) {
result.push_back(QByteArray::fromStdString(strData.substr(startPos)));
break;
}
result.push_back(QByteArray::fromStdString(strData.substr(startPos, pos - startPos)));
startPos = strData.find_first_not_of(seperators, pos);
if (startPos == std::string::npos) {
break;
}
}
return result;
}
bool hasComplexBorderRadius(const LocalProp* prop)
{
for (auto childProp : prop->children()) {
QByteArray name = childProp->name();
if (!name.startsWith("border-") || !name.endsWith("-radius")) {
continue;
}
if (name != "border-radius") {
return true;
}
} // of child prop iteration
return false;
}
bool FGCanvasPath::rebuildFromRect(std::vector<int>& commands, std::vector<float>& coords) const
{
LocalProp* rectProp = _propertyRoot->getWithPath("rect");
if (hasComplexBorderRadius(_propertyRoot)) {
// build a full path
qWarning() << Q_FUNC_INFO << "implement me";
_paintType = Path;
} else {
float top = rectProp->value("top", 0.0).toFloat();
float left = rectProp->value("left", 0.0).toFloat();
float width = rectProp->value("width", 0.0).toFloat();
float height = rectProp->value("height", 0.0).toFloat();
if (rectProp->hasChild("right")) {
width = rectProp->value("right", 0.0).toFloat() - left;
}
if (rectProp->hasChild("bottom")) {
height = rectProp->value("bottom", 0.0).toFloat() - top;
}
_rect = QRectF(left, top, width, height);
if (_propertyRoot->hasChild("border-radius")) {
// round-rect
float xR = _propertyRoot->value("border-radius", 0.0).toFloat();
float yR = xR;
if (_propertyRoot->hasChild("border-radius[1]")) {
yR = _propertyRoot->value("border-radius[1]", 0.0).toFloat();
}
_roundRectRadius = QSizeF(xR, yR);
_paintType = RoundRect;
} else {
// simple rect
_paintType = Rect;
}
}
return true;
}
bool FGCanvasPath::rebuildFromSVGData(std::vector<int>& commands, std::vector<float>& coords) const
{
QByteArrayList tokens = splitSVGPathData(_propertyRoot->value("svg", QVariant()).toByteArray());
PathCommands currentCommand = PathClose;
bool isRelative = false;
int numCoordsTokens = 0;
const int totalTokens = tokens.size();
for (int index = 0; index < totalTokens; /* no increment */) {
const QByteArray& tk = tokens.at(index);
if ((tk.length() == 1) && std::isalpha(tk.at(0))) {
// new command token
const char svgCommand = std::toupper(tk.at(0));
isRelative = std::islower(tk.at(0));
switch (svgCommand) {
case 'Z':
currentCommand = PathClose;
numCoordsTokens = 0;
break;
case 'M':
currentCommand = PathMoveTo;
numCoordsTokens = 2;
break;
case 'L':
currentCommand = PathLineTo;
numCoordsTokens = 2;
break;
case 'H':
currentCommand = PathHLineTo;
numCoordsTokens = 1;
break;
case 'V':
currentCommand = PathVLineTo;
numCoordsTokens = 1;
break;
case 'C':
currentCommand = PathCubicTo;
numCoordsTokens = 6;
break;
case 'S':
currentCommand = PathSmoothCubicTo;
numCoordsTokens = 4;
break;
case 'Q':
currentCommand = PathQuadTo;
numCoordsTokens = 4;
break;
case 'T':
currentCommand = PathSmoothQuadTo;
numCoordsTokens = 2;
break;
case 'A':
currentCommand = PathShortCWArc;
numCoordsTokens = 0; // handled specially below
break;
default:
qWarning() << "unrecognized SVG command" << svgCommand;
return false;
}
++index;
}
switch (currentCommand) {
case PathMoveTo:
commands.push_back(PathMoveTo | (isRelative ? 1 : 0));
currentCommand = PathLineTo;
break;
case PathClose:
case PathLineTo:
case PathHLineTo:
case PathVLineTo:
case PathQuadTo:
case PathCubicTo:
case PathSmoothQuadTo:
case PathSmoothCubicTo:
commands.push_back(currentCommand | (isRelative ? 1 : 0));
break;
case PathShortCWArc:
case PathShortCCWArc:
case PathLongCWArc:
case PathLongCCWArc:
{
// decode the actual arc type
coords.push_back(tokens.at(index++).toFloat()); // rx
coords.push_back(tokens.at(index++).toFloat()); // ry
coords.push_back(tokens.at(index++).toFloat()); // x-axis rotation
const bool isLargeArc = (tokens.at(index++).toInt() != 0); // large-angle
const bool isCCW = (tokens.at(index++).toInt() != 0); // sweep-flag
if (isLargeArc) {
commands.push_back(isCCW ? PathLongCCWArc : PathLongCWArc);
} else {
commands.push_back(isCCW ? PathShortCCWArc : PathShortCWArc);
}
if (isRelative) {
commands.back() |= 1;
}
coords.push_back(tokens.at(index++).toFloat());
coords.push_back(tokens.at(index++).toFloat());
break;
}
default:
qWarning() << "invalid path command";
return false;
} // of current command switch
// copy over tokens according to the active command.
if (index + numCoordsTokens > totalTokens) {
qWarning() << "insufficent remaining tokens for SVG command" << currentCommand;
qWarning() << index << numCoordsTokens << totalTokens;
return false;
}
for (int c = 0; c < numCoordsTokens; ++c) {
coords.push_back(tokens.at(index + c).toFloat());
}
index += numCoordsTokens;
} // of tokens iteration
return true;
}
void FGCanvasPath::rebuildPathFromCommands(const std::vector<int>& commands, const std::vector<float>& coords) const
{
QPainterPath newPath;
const float* coord = coords.data();
QPointF lastControlPoint; // for smooth cubics / quadric
size_t currentCoord = 0;
for (int cmd : commands) {
bool isRelative = cmd & 0x1;
const int op = cmd & ~0x1;
const int cmdIndex = op >> 1;
const qreal baseX = isRelative ? newPath.currentPosition().x() : 0.0f;
const qreal baseY = isRelative ? newPath.currentPosition().y() : 0.0f;
if ((currentCoord + CoordsPerCommand[cmdIndex]) > coords.size()) {
qWarning() << "insufficient path data" << currentCoord << cmdIndex << CoordsPerCommand[cmdIndex] << coords.size();
break;
}
switch (op) {
case PathClose:
newPath.closeSubpath();
break;
case PathMoveTo:
newPath.moveTo(coord[0] + baseX, coord[1] + baseY);
break;
case PathLineTo:
newPath.lineTo(coord[0] + baseX, coord[1] + baseY);
break;
case PathHLineTo:
newPath.lineTo(coord[0] + baseX, newPath.currentPosition().y());
break;
case PathVLineTo:
newPath.lineTo(newPath.currentPosition().x(), coord[0] + baseY);
break;
case PathQuadTo:
newPath.quadTo(coord[0] + baseX, coord[1] + baseY,
coord[2] + baseX, coord[3] + baseY);
lastControlPoint = QPointF(coord[0] + baseX, coord[1] + baseY);
break;
case PathCubicTo:
newPath.cubicTo(coord[0] + baseX, coord[1] + baseY,
coord[2] + baseX, coord[3] + baseY,
coord[4] + baseX, coord[5] + baseY);
lastControlPoint = QPointF(coord[2] + baseX, coord[3] + baseY);
break;
case PathSmoothQuadTo: {
QPointF smoothControlPoint = (newPath.currentPosition() - lastControlPoint) * 2.0;
newPath.quadTo(smoothControlPoint.x(), smoothControlPoint.y(),
coord[0] + baseX, coord[1] + baseY);
lastControlPoint = smoothControlPoint;
break;
}
case PathSmoothCubicTo: {
QPointF smoothControlPoint = (newPath.currentPosition() - lastControlPoint) * 2.0;
newPath.cubicTo(smoothControlPoint.x(), smoothControlPoint.y(),
coord[0] + baseX, coord[1] + baseY,
coord[2] + baseX, coord[3] + baseY);
lastControlPoint = QPointF(coord[0] + baseX, coord[1] + baseY);
break;
}
#if 0
qreal rx,
qreal ry,
qreal x_axis_rotation,
int large_arc_flag,
int sweep_flag,
qreal x,
qreal y,
qreal curx, qreal cury)
#endif
case PathLongCCWArc:
case PathLongCWArc:
pathArc(newPath, coord[0], coord[1], coord[2],
true, (op == PathLongCCWArc),
coord[3] + baseX, coord[4] + baseY,
newPath.currentPosition().x(), newPath.currentPosition().y());
break;
case PathShortCCWArc:
case PathShortCWArc:
pathArc(newPath, coord[0], coord[1], coord[2],
false, (op == PathShortCCWArc),
coord[3] + baseX, coord[4] + baseY,
newPath.currentPosition().x(), newPath.currentPosition().y());
break;
default:
qWarning() << "uhandled path command type:" << cmdIndex;
}
if ((op < PathQuadTo) || (op > PathSmoothCubicTo)) {
lastControlPoint = newPath.currentPosition();
}
coord += CoordsPerCommand[cmdIndex];
currentCoord += CoordsPerCommand[cmdIndex];
} // of commands iteration
// qDebug() << _propertyRoot->path() << "path" << newPath;
_painterPath = newPath;
}
static Qt::PenCapStyle qtCapFromCanvas(QString s)
{
if (s.isEmpty() || (s == "butt")) {
return Qt::FlatCap;
} else if (s == "round") {
return Qt::RoundCap;
} else if (s == "square") {
return Qt::SquareCap;
} else {
qDebug() << Q_FUNC_INFO << s;
}
return Qt::FlatCap;
}
static Qt::PenJoinStyle qtJoinFromCanvas(QString s)
{
if (s.isEmpty() || (s == "miter")) {
return Qt::MiterJoin;
} else if (s == "round") {
return Qt::RoundJoin;
} else {
qDebug() << Q_FUNC_INFO << s;
}
return Qt::MiterJoin;
}
static QVector<qreal> qtPenDashesFromCanvas(QString s, double penWidth)
{
QVector<qreal> result;
Q_FOREACH(QString v, s.split(',')) {
result.push_back(v.toFloat() / penWidth);
}
// https://developer.mozilla.org/en/docs/Web/SVG/Attribute/stroke-dasharray
// odd number = double it
if ((result.size() % 2) == 1) {
result += result;
}
return result;
}
void FGCanvasPath::rebuildPen() const
{
QPen p;
QVariant strokeColor = getCascadedStyle("stroke");
p.setColor(parseColorValue(strokeColor));
p.setWidthF(getCascadedStyle("stroke-width", 1.0).toFloat());
p.setCapStyle(qtCapFromCanvas(_propertyRoot->value("stroke-linecap", QString()).toString()));
p.setJoinStyle(qtJoinFromCanvas(_propertyRoot->value("stroke-linejoin", QString()).toString()));
QString dashArray = _propertyRoot->value("stroke-dasharray", QVariant()).toString();
if (!dashArray.isEmpty() && (dashArray != "none")) {
p.setDashPattern(qtPenDashesFromCanvas(dashArray, p.widthF()));
}
_stroke = p;
}
#include "fgcanvaspath.moc"
+80
View File
@@ -0,0 +1,80 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGCANVASPATH_H
#define FGCANVASPATH_H
#include "fgcanvaselement.h"
#include <QPainterPath>
#include <QPen>
class PathQuickItem;
class FGCanvasPath : public FGCanvasElement
{
public:
FGCanvasPath(FGCanvasGroup* pr, LocalProp* prop);
void dumpElement() override;
protected:
virtual void doPaint(FGCanvasPaintContext* context) const override;
void doPolish() override;
virtual void markStyleDirty() override;
CanvasItem* createQuickItem(QQuickItem *parent) override;
CanvasItem* quickItem() const override;
void doDestroy() override;
private:
void markPathDirty();
void markStrokeDirty();
private:
bool onChildAdded(LocalProp *prop) override;
bool onChildRemoved(LocalProp* prop) override;
void rebuildPath() const;
void rebuildPen() const;
void rebuildPathFromCommands(const std::vector<int>& commands, const std::vector<float>& coords) const;
bool rebuildFromSVGData(std::vector<int>& commands, std::vector<float>& coords) const;
bool rebuildFromRect(std::vector<int> &commands, std::vector<float> &coords) const;
private:
enum PaintType
{
Path,
Rect,
RoundRect
};
mutable bool _pathDirty = true;
mutable QPainterPath _painterPath;
mutable bool _penDirty = true;
mutable QPen _stroke;
bool _isRect = false;
mutable PaintType _paintType = Path;
mutable QRectF _rect;
mutable QSizeF _roundRectRadius;
PathQuickItem* _quickPath = nullptr;
};
#endif // FGCANVASPATH_H
+392
View File
@@ -0,0 +1,392 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgcanvastext.h"
#include <QPainter>
#include <QDebug>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QTextLayout>
#include <private/qquicktextnode_p.h>
#include "fgcanvaspaintcontext.h"
#include "localprop.h"
#include "fgqcanvasfontcache.h"
#include "canvasitem.h"
#include "canvasconnection.h"
class TextCanvasItem : public CanvasItem
{
Q_OBJECT
public:
TextCanvasItem(QQuickItem* parent)
: CanvasItem(parent)
{
setFlag(ItemHasContents);
}
void setText(QString t)
{
if (t == m_text) {
return;
}
m_text = t;
updateTextLayout();
update();
}
void setColor(QColor c)
{
m_color = c;
update();
}
void setAlignment(Qt::Alignment textAlign)
{
m_alignment = textAlign;
updateTextLayout();
update();
}
void setFont(QFont font)
{
m_font = font;
updateTextLayout();
update();
}
QSGNode* updateRealPaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData *data) override
{
if (!m_textNode) {
m_textNode = new QQuickTextNode(this);
}
m_textNode->deleteContent();
QPointF pos = posForAlignment();
m_textNode->addTextLayout(pos,
&m_layout,
m_color,
QQuickText::Normal);
return m_textNode;
}
protected:
QPointF posForAlignment()
{
float x = 0.0f;
float y = 0.0f;
const float itemWidth = width();
const float itemHeight = height();
const float textWidth = m_layout.boundingRect().width();
const float textHeight = m_layout.boundingRect().height();
switch (m_alignment & Qt::AlignHorizontal_Mask) {
case Qt::AlignLeft:
case Qt::AlignJustify:
break;
case Qt::AlignRight:
x = itemWidth - textWidth;
break;
case Qt::AlignHCenter:
x = (itemWidth - textWidth) / 2;
break;
}
switch (m_alignment & Qt::AlignVertical_Mask) {
case Qt::AlignTop:
break;
case Qt::AlignBottom:
y = itemHeight - textHeight;
break;
case Qt::AlignVCenter:
y = (itemHeight - textHeight) / 2;
break;
case Qt::AlignBaseline:
y = -m_baselineOffset;
break;
}
return QPointF(x,y);
}
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override
{
QQuickItem::geometryChanged(newGeometry, oldGeometry);
update();
}
QRectF boundingRect() const override
{
if ((width() == 0.0) || (height() == 0.0)) {
return m_layout.boundingRect();
}
return QQuickItem::boundingRect();
}
void updateTextLayout()
{
QFontMetricsF fm(m_font);
m_baselineOffset = fm.ascent(); // for aligning to first base line
m_layout.setText(m_text);
QTextOption textOpt(m_alignment);
m_layout.setTextOption(textOpt);
m_layout.setFont(m_font);
m_layout.beginLayout();
float leading = fm.leading();
int lineCount = 0;
float y = 0.0;
QTextLine line = m_layout.createLine();
while (line.isValid()) {
int nextBreak = m_text.indexOf('\n', line.textStart());
int columnsToNextBreak = nextBreak >= 0 ? nextBreak : INT_MAX;
line.setNumColumns(columnsToNextBreak);
line.setPosition(QPointF(0.0, y));
++lineCount;
y += leading + line.height();
line = m_layout.createLine();
}
m_layout.endLayout();
}
private:
QQuickTextNode* m_textNode = nullptr;
QColor m_color;
QFont m_font;
QString m_text;
Qt::Alignment m_alignment = Qt::AlignCenter;
QTextLayout m_layout;
float m_baselineOffset;
};
FGCanvasText::FGCanvasText(FGCanvasGroup* pr, LocalProp* prop) :
FGCanvasElement(pr, prop),
_metrics(QFont())
{
}
CanvasItem *FGCanvasText::createQuickItem(QQuickItem *parent)
{
_quickItem = new TextCanvasItem(parent);
markFontDirty(); // so it gets set on the new item
return _quickItem;
}
CanvasItem *FGCanvasText::quickItem() const
{
return _quickItem;
}
void FGCanvasText::dumpElement()
{
qDebug() << "Text:" << _text << " at " << _propertyRoot->path();
}
void FGCanvasText::doPaint(FGCanvasPaintContext *context) const
{
context->painter()->setFont(_font);
QColor c = fillColor();
if (!c.isValid()) {
c = Qt::white;
}
context->painter()->setPen(c);
context->painter()->setBrush(Qt::NoBrush);
QRectF rect(0, 0, 1000, 1000);
if (_alignment & Qt::AlignBottom) {
rect.moveBottom(0.0);
} else if (_alignment & Qt::AlignVCenter) {
rect.moveCenter(QPointF(rect.center().x(), 0.0));
} else if (_alignment & Qt::AlignBaseline) {
// this is really annoying. Point-based drawing would align
// with the baseline automatically, but with no line-wrapping
// we need to work out the offset from the top of the box to
// the base line. font metrics time!
rect.moveTop(-_metrics.ascent());
}
if (_alignment & Qt::AlignRight) {
rect.moveRight(0.0);
} else if (_alignment & Qt::AlignHCenter) {
rect.moveCenter(QPointF(0.0, rect.center().y()));
}
context->painter()->drawText(rect, _alignment, _text);
// context->painter()->setPen(Qt::cyan);
// context->painter()->drawRect(rect);
}
void FGCanvasText::doPolish()
{
if (_fontDirty) {
rebuildFont();
_fontDirty = false;
}
}
void FGCanvasText::markStyleDirty()
{
markFontDirty();
}
void FGCanvasText::doDestroy()
{
delete _quickItem;
}
bool FGCanvasText::onChildAdded(LocalProp *prop)
{
if (FGCanvasElement::onChildAdded(prop)) {
return true;
}
if (prop->name() == "text") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasText::onTextChanged);
return true;
}
if (prop->name() == "draw-mode") {
connect(prop, &LocalProp::valueChanged, this, &FGCanvasText::setDrawMode);
return true;
}
if (prop->name() == "character-aspect-ratio") {
return true;
}
qDebug() << "text saw child:" << prop->name() << prop->index();
return false;
}
void FGCanvasText::onTextChanged(QVariant var)
{
_text = var.toString();
if (_quickItem) {
_quickItem->setText(var.toString());
}
}
void FGCanvasText::setDrawMode(QVariant var)
{
int mode = var.toInt();
if (mode != 1) {
qDebug() << _propertyRoot->path() << "draw mode is now" << mode;
}
}
void FGCanvasText::rebuildAlignment(QVariant var) const
{
QByteArray alignString = var.toByteArray();
if (alignString.isEmpty()) {
_alignment = Qt::AlignBaseline | Qt::AlignLeft;
return;
}
if (alignString == "center") {
_alignment = Qt::AlignCenter;
if (_quickItem) {
_quickItem->setAlignment(_alignment);
}
return;
}
Qt::Alignment newAlignment;
if (alignString.startsWith("left-")) {
newAlignment |= Qt::AlignLeft;
} else if (alignString.startsWith("center-")) {
newAlignment |= Qt::AlignHCenter;
} else if (alignString.startsWith("right-")) {
newAlignment |= Qt::AlignRight;
}
if (alignString.endsWith("-baseline")) {
newAlignment |= Qt::AlignBaseline;
} else if (alignString.endsWith("-top")) {
newAlignment |= Qt::AlignTop;
} else if (alignString.endsWith("-bottom")) {
newAlignment |= Qt::AlignBottom;
} else if (alignString.endsWith("-center")) {
newAlignment |= Qt::AlignVCenter;
}
if (newAlignment == 0) {
qWarning() << "implement me" << alignString;
}
_alignment = newAlignment;
if (_quickItem) {
_quickItem->setAlignment(_alignment);
}
}
void FGCanvasText::markFontDirty()
{
_fontDirty = true;
}
void FGCanvasText::onFontLoaded(QByteArray name)
{
QByteArray fontName = getCascadedStyle("font", QString()).toByteArray();
if (name != fontName) {
return; // not our font
}
auto fontCache = connection()->fontCache();
disconnect(fontCache, &FGQCanvasFontCache::fontLoaded, this, &FGCanvasText::onFontLoaded);
markFontDirty();
}
void FGCanvasText::rebuildFont() const
{
QByteArray fontName = getCascadedStyle("font", QString()).toByteArray();
bool ok;
auto fontCache = connection()->fontCache();
QFont f = fontCache->fontForName(fontName, &ok);
if (!ok) {
// wait for the correct font
connect(fontCache, &FGQCanvasFontCache::fontLoaded, this, &FGCanvasText::onFontLoaded);
return;
}
const int pixelSize = getCascadedStyle("character-size", 16).toInt();
f.setPixelSize(pixelSize);
_font = f;
_metrics = QFontMetricsF(_font);
rebuildAlignment(getCascadedStyle("alignment"));
if (_quickItem) {
_quickItem->setFont(f);
_quickItem->setColor(fillColor());
// _quickItem->setProperty("fontPixelSize", pixelSize);
}
}
#include "fgcanvastext.moc"
+70
View File
@@ -0,0 +1,70 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGCANVASTEXT_H
#define FGCANVASTEXT_H
#include <QFont>
#include <QFontMetricsF>
#include "fgcanvaselement.h"
class TextCanvasItem;
class FGCanvasText : public FGCanvasElement
{
public:
FGCanvasText(FGCanvasGroup* pr, LocalProp* prop);
CanvasItem* createQuickItem(QQuickItem *parent) override;
CanvasItem* quickItem() const override;
void dumpElement() override;
protected:
virtual void doPaint(FGCanvasPaintContext* context) const override;
void doPolish() override;
virtual void markStyleDirty() override;
void doDestroy() override;
private:
bool onChildAdded(LocalProp *prop) override;
void onTextChanged(QVariant var);
void setDrawMode(QVariant var);
void markFontDirty();
void onFontLoaded(QByteArray name);
private:
void rebuildFont() const;
void rebuildAlignment(QVariant var) const;
QString _text;
mutable Qt::Alignment _alignment;
mutable bool _fontDirty = true;
mutable QFont _font;
mutable QFontMetricsF _metrics;
TextCanvasItem* _quickItem = nullptr;
};
#endif // FGCANVASTEXT_H
+16
View File
@@ -0,0 +1,16 @@
<RCC>
<qresource prefix="/">
<file>qml/Window.qml</file>
<file>qml/Button.qml</file>
<file>qml/InputLine.qml</file>
<file alias="images/checkerboard">qml/checkerboard.png</file>
<file>qml/CanvasFrame.qml</file>
<file>qml/BrowsePanel.qml</file>
<file>qml/LoadSavePanel.qml</file>
<file>qml/VerticalTabPanel.qml</file>
<file>qml/SnapshotsPanel.qml</file>
<file>qml/CanvasMenu.qml</file>
<file>qml/GetStarted.qml</file>
<file>doc/gettingStarted.html</file>
</qresource>
</RCC>
+145
View File
@@ -0,0 +1,145 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgqcanvasfontcache.h"
#include <QNetworkAccessManager>
#include <QStandardPaths>
#include <QDebug>
#include <QUrl>
#include <QNetworkReply>
#include <QFontDatabase>
#include <QFile>
FGQCanvasFontCache::FGQCanvasFontCache(QNetworkAccessManager* nam, QObject *parent)
: QObject(parent)
, m_downloader(nam)
{
}
QFont FGQCanvasFontCache::fontForName(QByteArray name, bool* ok)
{
if (m_cache.contains(name)) {
if (ok) {
*ok = true;
}
return m_cache.value(name); // easy!
}
lookupFile(name);
if (m_cache.contains(name)) {
if (ok) {
*ok = true;
}
return m_cache.value(name);
}
if (ok) {
*ok = false;
}
return QFont(); // default font
}
void FGQCanvasFontCache::setHost(QString hostName, int portNumber)
{
m_hostName = hostName;
m_port = portNumber;
}
void FGQCanvasFontCache::onFontDownloadFinished()
{
QByteArray fontPath = sender()->property("font").toByteArray();
qDebug() << "finished download of " << fontPath;
QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
QString absPath = cacheDir.absoluteFilePath(fontPath);
QFileInfo finfo(fontPath);
cacheDir.mkpath(finfo.dir().path());
QFile f(absPath);
if (!f.open(QIODevice::WriteOnly)) {
qWarning() << "failed to open cache file" << f.fileName();
}
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
Q_ASSERT(m_transfers.contains(reply));
f.write(reply->readAll());
f.close();
m_transfers.removeOne(reply);
// call ourselves again now it's cached;
lookupFile(fontPath);
}
void FGQCanvasFontCache::onFontDownloadError(QNetworkReply::NetworkError)
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
qWarning() << "font download failed:" << reply->errorString();
}
void FGQCanvasFontCache::lookupFile(QByteArray name)
{
QString path = QStandardPaths::locate(QStandardPaths::CacheLocation, name);
if (!path.isEmpty()) {
qDebug() << "found font" << name << "at path" << path;
int fontFamilyId = QFontDatabase::addApplicationFont(path);
if (fontFamilyId >= 0) {
QStringList families = QFontDatabase::applicationFontFamilies(fontFamilyId);
qDebug() << "families are:" << families;
// compute a QFont and cache
QFont font(families.front());
m_cache.insert(name, font);
return;
} else {
qWarning() << "Failed to load font into QFontDatabase:" << path;
}
}
if (m_hostName.isEmpty()) {
qWarning() << "host name not specified";
return;
}
QUrl url;
url.setScheme("http");
url.setHost(m_hostName);
url.setPort(m_port);
url.setPath("/Fonts/" + name);
Q_FOREACH (QNetworkReply* transfer, m_transfers) {
if (transfer->url() == url) {
return; // transfer already active
}
}
qDebug() << "reqeusting font" << url;
QNetworkReply* reply = m_downloader->get(QNetworkRequest(url));
reply->setProperty("font", name);
connect(reply, &QNetworkReply::finished, this, &FGQCanvasFontCache::onFontDownloadFinished);
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
this, SLOT(onFontDownloadError(QNetworkReply::NetworkError)));
m_transfers.append(reply);
}
+56
View File
@@ -0,0 +1,56 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGQCANVASFONTCACHE_H
#define FGQCANVASFONTCACHE_H
#include <QObject>
#include <QFont>
#include <QDir>
#include <QHash>
#include <QNetworkReply>
class QNetworkAccessManager;
class FGQCanvasFontCache : public QObject
{
Q_OBJECT
public:
explicit FGQCanvasFontCache(QNetworkAccessManager* nam, QObject *parent = 0);
QFont fontForName(QByteArray name, bool* ok = nullptr);
void setHost(QString hostName, int portNumber);
signals:
void fontLoaded(QByteArray name);
private slots:
void onFontDownloadFinished();
void onFontDownloadError(QNetworkReply::NetworkError);
private:
QNetworkAccessManager* m_downloader;
QHash<QByteArray, QFont> m_cache;
QString m_hostName;
int m_port;
void lookupFile(QByteArray name);
QList<QNetworkReply*> m_transfers;
};
#endif // FGQCANVASFONTCACHE_H
+264
View File
@@ -0,0 +1,264 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgqcanvasimage.h"
#include <QPainter>
#include <QDebug>
#include <QQmlComponent>
#include "fgcanvaspaintcontext.h"
#include "localprop.h"
#include "fgqcanvasimageloader.h"
#include "canvasitem.h"
#include "canvasconnection.h"
#include <QSGGeometry>
#include <QSGGeometryNode>
#include <QSGFlatColorMaterial>
#include <QSGTexture>
#include <QSGSimpleTextureNode>
#include <QQuickWindow>
class ImageQuickItem : public CanvasItem
{
Q_OBJECT
public:
ImageQuickItem(QQuickItem* parent)
: CanvasItem(parent)
{
setFlag(ItemHasContents);
}
void setSourceRect(const QRectF& sourceRect)
{
m_sourceRect = sourceRect;
update();
}
void setSize(const QSizeF &size)
{
m_size = size;
setImplicitSize(size.width(), size.height());
update();
}
void setPixmap(QPixmap pixmap)
{
m_pixmap = pixmap;
update();
}
QSGNode* updateRealPaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData *data) override
{
if (m_pixmap.isNull()) {
return nullptr;
}
QSGSimpleTextureNode* texNode = static_cast<QSGSimpleTextureNode*>(oldNode);
if (!texNode) {
texNode = new QSGSimpleTextureNode;
texNode->setOwnsTexture(true);
}
texNode->setRect(QRectF(QPointF(), m_size));
texNode->setSourceRect(m_sourceRect);
if (m_texture) {
delete m_texture;
}
m_texture = window()->createTextureFromImage(m_pixmap.toImage(), QQuickWindow::TextureCanUseAtlas);
texNode->setTexture(m_texture);
return texNode;
}
protected:
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override
{
QQuickItem::geometryChanged(newGeometry, oldGeometry);
update();
}
QRectF boundingRect() const override
{
if (!widthValid() || !heightValid()) {
return QRectF(QPointF(), m_size);
}
return QQuickItem::boundingRect();
}
private:
QRectF m_sourceRect;
QSGTexture* m_texture = nullptr;
QSizeF m_size;
QPixmap m_pixmap;
};
FGQCanvasImage::FGQCanvasImage(FGCanvasGroup* pr, LocalProp* prop) :
FGCanvasElement(pr, prop)
{
}
void FGQCanvasImage::doPolish()
{
if (_imageDirty) {
rebuildImage();
_imageDirty = false;
}
if (_sourceRectDirty) {
recomputeSourceRect();
}
}
void FGQCanvasImage::doPaint(FGCanvasPaintContext *context) const
{
QRectF dstRect(0.0, 0.0, _destSize.width(), _destSize.height());
context->painter()->drawPixmap(dstRect, _image, _sourceRect);
}
bool FGQCanvasImage::onChildAdded(LocalProp *prop)
{
if (FGCanvasElement::onChildAdded(prop)) {
return true;
}
const QByteArray nm = prop->name();
if ((nm == "src") || (nm == "size") || (nm == "file")) {
connect(prop, &LocalProp::valueChanged, this, &FGQCanvasImage::markImageDirty);
return true;
}
if (nm == "source") {
FGQCanvasImage* self = this;
connect(prop, &LocalProp::childAdded, [self](LocalProp* newChild) {
connect(newChild, &LocalProp::valueChanged, self, &FGQCanvasImage::markSourceDirty);
});
return true;
}
return false;
}
void FGQCanvasImage::markImageDirty()
{
_imageDirty = true;
requestPolish();
}
void FGQCanvasImage::markSourceDirty()
{
_sourceRectDirty = true;
requestPolish();
}
void FGQCanvasImage::recomputeSourceRect() const
{
const float imageWidth = _image.width();
const float imageHeight = _image.height();
_sourceRect = QRectF(0, 0, imageWidth, imageHeight);
if (!_propertyRoot->hasChild("source")) {
return;
}
const bool normalized = _propertyRoot->value("source/normalized", true).toBool();
float left = _propertyRoot->value("source/left", 0.0).toFloat();
float top = _propertyRoot->value("source/top", 0.0).toFloat();
float right = _propertyRoot->value("source/right", 1.0).toFloat();
float bottom = _propertyRoot->value("source/bottom", 1.0).toFloat();
if (normalized) {
left *= imageWidth;
right *= imageWidth;
top *= imageHeight;
bottom *= imageHeight;
}
_sourceRect = QRectF(left, top, right - left, bottom - top);
_sourceRectDirty = false;
if (_quickItem) {
_quickItem->setSourceRect(_sourceRect);
}
}
void FGQCanvasImage::rebuildImage() const
{
QByteArray file = _propertyRoot->value("file", QByteArray()).toByteArray();
auto loader = connection()->imageLoader();
if (!file.isEmpty()) {
_image = loader->getImage(file);
if (_image.isNull()) {
// get notified when the image loads
loader->connectToImageLoaded(file,
const_cast<FGQCanvasImage*>(this),
SLOT(markImageDirty()));
} else {
// loaded image ok!
}
} else {
qDebug() << "src" << _propertyRoot->value("src", QString());
}
_destSize = QSizeF(_propertyRoot->value("size[0]", 0.0).toFloat(),
_propertyRoot->value("size[1]", 0.0).toFloat());
_imageDirty = false;
if (_quickItem) {
_quickItem->setSize(_destSize);
_quickItem->setPixmap(_image);
}
}
void FGQCanvasImage::markStyleDirty()
{
}
void FGQCanvasImage::doDestroy()
{
delete _quickItem;
}
CanvasItem *FGQCanvasImage::createQuickItem(QQuickItem *parent)
{
_quickItem = new ImageQuickItem(parent);
_quickItem->setSourceRect(_sourceRect);
_quickItem->setSize(_destSize);
_quickItem->setPixmap(_image);
return _quickItem;
}
CanvasItem *FGQCanvasImage::quickItem() const
{
return _quickItem;
}
void FGQCanvasImage::dumpElement()
{
qDebug() << "Image: " << _source << " at " << _propertyRoot->path();
}
#include "fgqcanvasimage.moc"
+67
View File
@@ -0,0 +1,67 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGQCANVASIMAGE_H
#define FGQCANVASIMAGE_H
#include <QPixmap>
#include <QQmlEngine>
#include "fgcanvaselement.h"
class ImageQuickItem;
class FGQCanvasImage : public FGCanvasElement
{
Q_OBJECT
public:
FGQCanvasImage(FGCanvasGroup* pr, LocalProp* prop);
CanvasItem* createQuickItem(QQuickItem *parent) override;
CanvasItem* quickItem() const override;
void dumpElement() override;
protected:
virtual void doPaint(FGCanvasPaintContext* context) const override;
virtual void markStyleDirty() override;
void doDestroy() override;
void doPolish() override;
private slots:
void markImageDirty();
void markSourceDirty();
private:
bool onChildAdded(LocalProp *prop) override;
void rebuildImage() const;
void recomputeSourceRect() const;
private:
mutable bool _imageDirty;
mutable bool _sourceRectDirty = true;
mutable QPixmap _image;
QString _source;
mutable QSizeF _destSize;
mutable QRectF _sourceRect;
ImageQuickItem* _quickItem = nullptr;
};
#endif // FGQCANVASIMAGE_H
+145
View File
@@ -0,0 +1,145 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgqcanvasimageloader.h"
#include <QDebug>
#include <QNetworkAccessManager>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QFileInfo>
class TransferSignalHolder : public QObject
{
Q_OBJECT
public:
TransferSignalHolder(QObject* pr) : QObject(pr) { }
signals:
void trigger();
};
FGQCanvasImageLoader::FGQCanvasImageLoader(QNetworkAccessManager* dl, QObject* pr)
: QObject(pr)
, m_downloader(dl)
{
}
void FGQCanvasImageLoader::onDownloadFinished()
{
QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
QPixmap pm;
if (!pm.loadFromData(reply->readAll())) {
qWarning() << "image loading failed";
} else {
QByteArray imagePath = reply->property("image").toByteArray();
m_cache.insert(imagePath, pm);
// cache on disk also, so snapshots work
writeToDiskCache(imagePath, reply);
TransferSignalHolder* signalHolder = reply->findChild<TransferSignalHolder*>("holder");
if (signalHolder) {
qDebug() << "triggering image updates";
signalHolder->trigger();
}
}
m_transfers.removeOne(reply);
reply->deleteLater();
}
void FGQCanvasImageLoader::writeToDiskCache(QByteArray imagePath, QNetworkReply* reply)
{
QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
QString absPath = cacheDir.absoluteFilePath(imagePath);
QFileInfo finfo(imagePath);
cacheDir.mkpath(finfo.dir().path());
QFile f(absPath);
if (!f.open(QIODevice::WriteOnly)) {
qWarning() << "failed to open cache file" << f.fileName();
}
f.write(reply->readAll());
f.close();
}
void FGQCanvasImageLoader::setHost(QString hostName, int portNumber)
{
m_hostName = hostName;
m_port = portNumber;
}
QPixmap FGQCanvasImageLoader::getImage(const QByteArray &imagePath)
{
if (m_cache.contains(imagePath)) {
// cached, easy
return m_cache.value(imagePath);
}
QString diskCachePath = QStandardPaths::locate(QStandardPaths::CacheLocation, imagePath);
if (!diskCachePath.isEmpty()) {
QPixmap pix;
pix.load(diskCachePath);
m_cache.insert(imagePath, pix);
qDebug() << "loaded from on-disk cache:" << imagePath;
return pix;
}
QUrl url;
url.setScheme("http");
url.setHost(m_hostName);
url.setPort(m_port);
url.setPath("/aircraft-dir/" + imagePath);
Q_FOREACH (QNetworkReply* transfer, m_transfers) {
if (transfer->url() == url) {
return QPixmap(); // transfer already active
}
}
QNetworkReply* reply = m_downloader->get(QNetworkRequest(url));
reply->setProperty("image", imagePath);
connect(reply, &QNetworkReply::finished, this, &FGQCanvasImageLoader::onDownloadFinished);
m_transfers.append(reply);
return QPixmap();
}
void FGQCanvasImageLoader::connectToImageLoaded(const QByteArray &imagePath, QObject *receiver, const char *slot)
{
Q_FOREACH (QNetworkReply* transfer, m_transfers) {
if (transfer->property("image").toByteArray() == imagePath) {
QObject* signalHolder = transfer->findChild<QObject*>("holder");
if (!signalHolder) {
signalHolder = new TransferSignalHolder(transfer);
signalHolder->setObjectName("holder");
}
connect(signalHolder, SIGNAL(trigger()), receiver, slot);
return;
}
}
qWarning() << "no transfer active for" << imagePath;
}
#include "fgqcanvasimageloader.moc"
+65
View File
@@ -0,0 +1,65 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGQCANVASIMAGELOADER_H
#define FGQCANVASIMAGELOADER_H
#include <QObject>
#include <QPixmap>
#include <QMap>
#include <QNetworkReply>
class QNetworkAccessManager;
class FGQCanvasImageLoader : public QObject
{
Q_OBJECT
public:
FGQCanvasImageLoader(QNetworkAccessManager* dl, QObject* pr = nullptr);
void setHost(QString hostName, int portNumber);
QPixmap getImage(const QByteArray& imagePath);
/**
* @brief connectToImageLoaded - allow images to discover when they are loaded
* @param imagePath - FGFS host relative path as found in the canvas (will be resolved
* against aircraft-data, and potentially other places)
* @param receiver - normal connect() receiver object
* @param slot - slot macro
*/
void connectToImageLoaded(const QByteArray& imagePath, QObject* receiver, const char* slot);
signals:
private:
void onDownloadFinished();
void writeToDiskCache(QByteArray imagePath, QNetworkReply *reply);
private:
QNetworkAccessManager* m_downloader;
QString m_hostName;
int m_port;
QMap<QByteArray, QPixmap> m_cache;
QList<QNetworkReply*> m_transfers;
};
#endif // FGQCANVASIMAGELOADER_H
+57
View File
@@ -0,0 +1,57 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "fgqcanvasmap.h"
#include <QDebug>
#include "localprop.h"
#include "fgcanvaspaintcontext.h"
FGQCanvasMap::FGQCanvasMap(FGCanvasGroup* pr, LocalProp* prop) :
FGCanvasGroup(pr, prop)
{
}
void FGQCanvasMap::doPaint(FGCanvasPaintContext *context) const
{
FGCanvasGroup::doPaint(context);
}
bool FGQCanvasMap::onChildAdded(LocalProp *prop)
{
const QByteArray nm = prop->name();
if ((nm == "ref-lon") || (nm == "ref-lat") || (nm == "hdg") || (nm == "range")
|| (nm == "screen-range")) {
connect(prop, &LocalProp::valueChanged, this, &FGQCanvasMap::markProjectionDirty);
return true;
}
if (FGCanvasGroup::onChildAdded(prop)) {
return true;
}
qDebug() << Q_FUNC_INFO << "deal with:" << prop->name();
return false;
}
void FGQCanvasMap::markProjectionDirty()
{
_projectionChanged = true;
}
+44
View File
@@ -0,0 +1,44 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef FGQCANVASMAP_H
#define FGQCANVASMAP_H
#include "fgcanvasgroup.h"
class FGQCanvasMap : public FGCanvasGroup
{
public:
FGQCanvasMap(FGCanvasGroup* pr, LocalProp* prop);
protected:
virtual void doPaint(FGCanvasPaintContext* context) const;
virtual bool onChildAdded(LocalProp* prop);
private:
void markProjectionDirty();
private:
double _projectionCenterLat;
double _projectionCenterLon;
double _range;
mutable bool _projectionChanged;
};
#endif // FGQCANVASMAP_H
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--<key>CFBundleIconFile</key>
<string>@ICON@</string> -->
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string>
<!-- <key>CFBundleSignature</key>
<string>@TYPEINFO@</string> -->
<key>CFBundleExecutable</key>
<string>fgqcanvas</string>
<key>CFBundleIdentifier</key>
<string>org.flightgear.fgqcanvas</string>
<key>CFBundleDisplayName</key>
<string>Flightgear Canvas</string>
<key>CFBundleName</key>
<string>Flightgear Canvas</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+34
View File
@@ -0,0 +1,34 @@
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "jsonutils.h"
QJsonArray rectToJsonArray(const QRect& r)
{
return QJsonArray{r.x(), r.y(), r.width(), r.height()};
}
QRect jsonArrayToRect(QJsonArray a)
{
if (a.size() < 4) {
return {};
}
return QRect(a[0].toInt(), a[1].toInt(),
a[2].toInt(), a[3].toInt());
}
+28
View File
@@ -0,0 +1,28 @@
//
// Copyright (C) 2018 James Turner <james@flightgear.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef JSONUTILS_H
#define JSONUTILS_H
#include <QRect>
#include <QJsonArray>
QJsonArray rectToJsonArray(const QRect& r);
QRect jsonArrayToRect(QJsonArray a);
#endif // JSONUTILS_H
+267
View File
@@ -0,0 +1,267 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "localprop.h"
#include <QJsonValue>
#include <QDebug>
QDataStream& operator<<(QDataStream& stream, const NameIndexTuple& nameIndex)
{
stream << nameIndex.name << nameIndex.index;
return stream;
}
QDataStream& operator>>(QDataStream& stream, NameIndexTuple& nameIndex)
{
stream >> nameIndex.name >> nameIndex.index;
return stream;
}
LocalProp *LocalProp::getOrCreateWithPath(const QByteArray &path, QVariant defaultValue)
{
if (path.isEmpty()) {
return this;
}
QList<QByteArray> segments = path.split('/');
LocalProp* result = this;
while (segments.size() > 1) {
QByteArray nameIndex = segments.front();
result = result->getOrCreateChildWithNameAndIndex(nameIndex);
segments.pop_front();
}
// for the final segment, pass the default value
if (!segments.empty()) {
result = result->getOrCreateChildWithNameAndIndex(segments.front(), defaultValue);
}
return result;
}
LocalProp *LocalProp::getWithPath(const QByteArray &path) const
{
if (path.isEmpty()) {
return const_cast<LocalProp*>(this);
}
QList<QByteArray> segments = path.split('/');
LocalProp* result = const_cast<LocalProp*>(this);
while (!segments.empty()) {
QByteArray nameIndex = segments.front();
result = result->childWithNameAndIndex(nameIndex);
segments.pop_front();
if (!result) {
return nullptr;
}
}
return result;
}
static bool lessThanPropNameIndex(const LocalProp* prop, const NameIndexTuple& ni)
{
return prop->id() < ni;
}
LocalProp::LocalProp(LocalProp *pr, const NameIndexTuple& ni) :
QObject(pr),
_id(ni),
_parent(pr)
{
}
LocalProp::~LocalProp()
{
for (auto c : _children) {
delete c;
}
}
void LocalProp::processChange(QJsonValue json)
{
QVariant newValue = json.toVariant();
if (newValue != _value) {
_value = newValue;
emit valueChanged(_value);
}
}
const NameIndexTuple &LocalProp::id() const
{
return _id;
}
QByteArray LocalProp::path() const
{
if (_parent) {
return _parent->path() + '/' + _id.toString();
}
return _id.toString();
}
LocalProp *LocalProp::childWithNameAndIndex(const NameIndexTuple& ni) const
{
auto it = std::lower_bound(_children.begin(), _children.end(), ni, lessThanPropNameIndex);
if ((it != _children.end()) && ((*it)->id() == ni)) {
return *it;
}
return nullptr;
}
bool LocalProp::hasChild(const char* name) const
{
return childWithNameAndIndex(QByteArray::fromRawData(name, strlen(name))) != nullptr;
}
void LocalProp::changeValue(const char *path, QVariant value)
{
LocalProp* p = getOrCreateWithPath(path);
p->_value = value;
p->valueChanged(value);
}
void LocalProp::saveToStream(QDataStream &stream) const
{
stream << _id << _position << _value;
stream << static_cast<int>(_children.size());
for (auto child : _children) {
child->saveToStream(stream);
}
}
LocalProp* LocalProp::restoreFromStream(QDataStream &stream, LocalProp* parent)
{
NameIndexTuple id;
stream >> id;
LocalProp* prop = new LocalProp(parent, id);
stream >> prop->_position >> prop->_value;
int childCount;
stream >> childCount;
for (int c=0; c< childCount; ++c) {
prop->_children.push_back(restoreFromStream(stream, prop));
}
return prop;
}
void LocalProp::recursiveNotifyRestored()
{
emit valueChanged(_value);
for (auto child : _children) {
emit childAdded(child);
}
for (auto cc : _children) {
cc->recursiveNotifyRestored();
}
}
LocalProp *LocalProp::getOrCreateChildWithNameAndIndex(const NameIndexTuple& ni,
QVariant defaultValue)
{
auto it = std::lower_bound(_children.begin(), _children.end(), ni, lessThanPropNameIndex);
if ((it != _children.end()) && ((*it)->id() == ni)) {
return *it;
}
LocalProp* newChild = new LocalProp(this, ni);
newChild->_value = defaultValue;
_children.insert(it, newChild);
emit childAdded(newChild);
return newChild;
}
LocalProp *LocalProp::getOrCreateWithPath(const char *name)
{
return getOrCreateWithPath(QByteArray::fromRawData(name, strlen(name)));
}
LocalProp *LocalProp::getWithPath(const char *name) const
{
return getWithPath(QByteArray::fromRawData(name, strlen(name)));
}
QByteArray LocalProp::name() const
{
return _id.name;
}
unsigned int LocalProp::index() const
{
return _id.index;
}
void LocalProp::setPosition(unsigned int pos)
{
_position = pos;
}
LocalProp *LocalProp::parent() const
{
return const_cast<LocalProp*>(_parent);
}
std::vector<QVariant> LocalProp::valuesOfChildren(const char *name) const
{
std::vector<QVariant> result;
for (LocalProp* c : childrenWithName(name)) {
result.push_back(c->value());
}
return result;
}
std::vector<LocalProp *> LocalProp::childrenWithName(const char *name) const
{
std::vector<LocalProp *> result;
for (LocalProp* child : _children) {
if (child->_id.name == name)
result.push_back(child);
}
return result;
}
QVariant LocalProp::value() const
{
return _value;
}
QVariant LocalProp::value(const char *path, QVariant defaultValue) const
{
LocalProp* n = getWithPath(path);
if (!n || n->value().isNull()) {
return defaultValue;
}
return n->value();
}
void LocalProp::removeChild(LocalProp *prop)
{
Q_ASSERT(prop->parent() == this);
auto it = std::find(_children.begin(), _children.end(), prop);
Q_ASSERT(it != _children.end());
_children.erase(it);
emit childRemoved(prop);
delete prop;
}
+157
View File
@@ -0,0 +1,157 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#ifndef LOCALPROP_H
#define LOCALPROP_H
#include <QByteArray>
#include <QVariant>
#include <QVector>
#include <QObject>
#include <QDataStream>
struct NameIndexTuple
{
QByteArray name;
unsigned int index = 0;
NameIndexTuple()
{}
NameIndexTuple(const char* nm, unsigned int idx) :
name(nm),
index(idx)
{}
NameIndexTuple(const QByteArray& bytes) :
name(bytes)
{
Q_ASSERT(bytes.indexOf('/') == -1);
if (bytes.endsWith(']')) {
int leftBracket = bytes.indexOf('[');
index = bytes.mid(leftBracket + 1, bytes.length() - (leftBracket + 2)).toInt();
name = bytes.left(leftBracket);
}
}
QByteArray toString() const
{
QByteArray p = name;
if (index > 0) {
p += '[' + QByteArray::number(index) + ']';
}
return p;
}
bool operator==(const NameIndexTuple& other) const
{
return (name == other.name) && (index == other.index);
}
bool operator<(const NameIndexTuple& other) const
{
if (name == other.name) {
return index < other.index;
}
return name < other.name;
}
};
QDataStream& operator<<(QDataStream& stream, const NameIndexTuple& nameIndex);
QDataStream& operator>>(QDataStream& stream, NameIndexTuple& nameIndex);
class LocalProp : public QObject
{
Q_OBJECT
public:
LocalProp(LocalProp* parent, const NameIndexTuple& ni);
virtual ~LocalProp();
void processChange(QJsonValue newValue);
const NameIndexTuple& id() const;
QByteArray path() const;
LocalProp* getOrCreateWithPath(const QByteArray& path, QVariant defaultValue = {});
LocalProp* childWithNameAndIndex(const NameIndexTuple& ni) const;
LocalProp* getOrCreateChildWithNameAndIndex(const NameIndexTuple& ni, QVariant defaultValue = {});
LocalProp* getOrCreateWithPath(const char* name);
LocalProp* getWithPath(const QByteArray& path) const;
LocalProp* getWithPath(const char* name) const;
QByteArray name() const;
unsigned int index() const;
/// position in the main FG propery tree. Normally
/// irrelevant but unfortunately necessary for correct
/// z-ordering of Canvas elements
unsigned int position() const
{
return _position;
}
void setPosition(unsigned int pos);
LocalProp* parent() const;
std::vector<LocalProp*> children() const
{ return _children; }
std::vector<QVariant> valuesOfChildren(const char* name) const;
std::vector<LocalProp*> childrenWithName(const char* name) const;
QVariant value() const;
QVariant value(const char* path, QVariant defaultValue) const;
void removeChild(LocalProp* prop);
bool hasChild(const char* name) const;
void changeValue(const char* path, QVariant value);
void saveToStream(QDataStream& stream) const;
static LocalProp* restoreFromStream(QDataStream& stream, LocalProp *parent);
void recursiveNotifyRestored();
signals:
void valueChanged(QVariant val);
void childAdded(LocalProp* child);
void childRemoved(LocalProp* child);
private:
const NameIndexTuple _id;
const LocalProp* _parent;
std::vector<LocalProp*> _children;
QVariant _value;
unsigned int _position = 0;
};
#endif // LOCALPROP_H
+65
View File
@@ -0,0 +1,65 @@
//
// Copyright (C) 2017 James Turner zakalawe@mac.com
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include <QApplication>
#include <QQmlEngine>
#include <QQuickView>
#include <QQmlContext>
#include <QCommandLineParser>
#include <QScreen>
#include "canvasitem.h"
#include "applicationcontroller.h"
#include "canvasdisplay.h"
#include "canvasconnection.h"
#include "canvaspainteddisplay.h"
#include "WindowData.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setApplicationName("FGCanvas");
a.setOrganizationDomain("flightgear.org");
a.setOrganizationName("FlightGear");
QCommandLineParser parser;
parser.addPositionalArgument("config", QCoreApplication::translate("main", "JSON configuration to load"));
parser.process(a);
ApplicationController appController;
qmlRegisterType<CanvasItem>("FlightGear", 1, 0, "CanvasItem");
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");
const QStringList args = parser.positionalArguments();
if (!args.empty()) {
appController.setDaemonMode();
appController.loadFromFile(args.front());
}
appController.createWindows();
int result = a.exec();
return result;
}
+140
View File
@@ -0,0 +1,140 @@
import QtQuick 2.0
import FlightGear 1.0 as FG
Item {
Rectangle {
id: hostPanel
height: hostPanelContent.childrenRect.height + 16
width: 300
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
border.color: "#9f9f9f"
border.width: 1
color: "#5f5f5f"
opacity: 0.8
Column {
spacing: 8
id: hostPanelContent
width: parent.width - 30
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
InputLine {
id: hostInput
width: parent.width
label: "Hostname"
text: _application.host
onEditingFinished: {
_application.host = text
portInput.forceActiveFocus();
}
KeyNavigation.tab: portInput
}
InputLine {
id: portInput
width: parent.width
label: "Port"
text: _application.port
KeyNavigation.tab: queryButton
onEditingFinished: {
_application.port = text
}
}
Button {
id: queryButton
label: "Query"
enabled: _application.host != ""
visible: (_application.status == FG.Application.Idle)
onClicked: {
_application.query();
}
}
Button {
id: cancelButton
label: "Cancel"
anchors.right: parent.right
visible: (_application.status == FG.Application.Querying)
onClicked: {
_application.cancelQuery();
}
}
Button {
id: clearlButton
label: "Clear"
anchors.right: parent.right
visible: (_application.status == FG.Application.SuccessfulQuery) |
(_application.status == FG.Application.QueryFailed)
onClicked: {
_application.clearQuery();
}
}
}
}
Rectangle {
id: canvasListPanel
border.color: "#9f9f9f"
border.width: 1
color: "#5f5f5f"
opacity: 0.8
anchors.top: hostPanel.bottom
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: 300
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
visible: _application.canvases.length > 0
ListView {
id: canvasList
model: _application.canvases
visible: (_application.status == FG.Application.SuccessfulQuery)
width: parent.width - 30
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
delegate: Rectangle {
width: canvasLabel.implicitWidth
height: canvasLabel.implicitHeight + 20
color: "#3f3f3f"
Text {
id: canvasLabel
text: modelData['name']
// todo - different color if we already have a connection?
color: "white"
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
onClicked: {
_application.openCanvas(modelData['path']);
}
}
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
import QtQuick 2.0
Rectangle {
id: root
property alias label: labelText.text
property bool enabled: true
signal clicked
border.width: 2
border.color: enabled ? "orange" : "9f9f9f"
color: "#3f3f3f"
implicitWidth: 100
implicitHeight: 30
Text {
id: labelText
anchors.centerIn: parent
color: enabled ? "white" : "9f9f9f"
}
MouseArea {
anchors.fill: parent
enabled: root.enabled
onClicked: {
root.clicked();
}
}
}
+153
View File
@@ -0,0 +1,153 @@
import QtQuick 2.0
import FlightGear 1.0 as FG
Item {
id: root
property bool showDecorations: true
property alias canvas: paintedDisplay.canvas
property bool showUi: true
property bool showMenu: false
Component.onCompleted: {
if (canvas) {
width = canvas.size.width
height = canvas.size.height
x = canvas.origin.x
y = canvas.origin.y
}
}
function saveGeometry()
{
canvas.origin = Qt.point(x, y )
canvas.size = Qt.size(root.width, root.height);
}
Item {
id: clipShell
anchors.fill: parent
clip: true
// FG.CanvasDisplay {
// id: canvasDisplay
// anchors.fill: parent
// onCanvasChanged: {
// if (canvas) {
// root.width = canvas.size.width
// root.height = canvas.size.height
// root.x = canvas.origin.x
// root.y = canvas.origin.y
// }
// }
// }
FG.PaintedCanvasDisplay {
id: paintedDisplay
anchors.fill: parent
// canvas: canvasDisplay.canvas
onCanvasChanged: {
if (canvas) {
root.width = canvas.size.width
root.height = canvas.size.height
root.x = canvas.origin.x
root.y = canvas.origin.y
}
}
}
}
Rectangle {
border.width: 1
border.color: "orange"
color: "transparent"
anchors.centerIn: parent
width: parent.width
height: parent.height
visible: showUi
MouseArea {
anchors.fill: parent
drag.target: root
onReleased: {
root.saveGeometry();
}
onPressAndHold: {
root.showMenu = true;
}
}
Rectangle {
width: 32
height: 32
color: "orange"
opacity: 0.5
anchors.right: parent.right
anchors.bottom: parent.bottom
MouseArea {
anchors.fill: parent
// resizing
onPositionChanged: {
var rootPos = mapToItem(root, mouse.x, mouse.y);
// var rootDiff = Qt.point(rootPos.x - root.x,
// rootPos.y - root.y);
root.width = rootPos.x;
root.height = rootPos.y;
}
onReleased: {
saveGeometry();
}
}
}
Text {
color: "orange"
anchors.centerIn: parent
text: "Canvas"
visible: !root.showMenu
}
Text {
anchors.fill: parent
verticalAlignment: Text.AlignBottom
function statusAsString(status)
{
switch (status) {
case FG.CanvasConnection.NotConnected: return "Not connected";
case FG.CanvasConnection.Connecting: return "Connecting";
case FG.CanvasConnection.Connected: return "Connected";
case FG.CanvasConnection.Closed: return "Closed";
case FG.CanvasConnection.Reconnecting: return "Re-connecting";
case FG.CanvasConnection.Error: return "Error";
case FG.CanvasConnection.Snapshot: return "Snapshot";
}
}
text: "WS: " + canvas.webSocketUrl + "\n"
+ "Root:" + canvas.rootPath + "\n"
+ "Status:" + statusAsString(canvas.status);
color: "white"
}
CanvasMenu {
anchors.fill:parent
visible: root.showMenu
onMenuBack: root.showMenu = false
onCloseCanvas: _application.closeCanvas(canvas);
onReconnectCanvas: canvas.reconnect();
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import QtQuick 2.0
Item {
id: root
signal menuBack();
signal closeCanvas();
signal reconnectCanvas();
Rectangle {
anchors.centerIn: parent
width: buttons.childrenRect.width + 20
height: buttons.childrenRect.height + 20
border.width: 1
border.color: "orange"
color: "#5f5f5f"
Column {
id: buttons
spacing: 30
Button {
label: qsTr("Back")
onClicked: root.menuBack();
anchors.horizontalCenter: parent.horizontalCenter
}
Button {
label: qsTr("Close")
onClicked: root.closeCanvas();
anchors.horizontalCenter: parent.horizontalCenter
}
Button {
label: qsTr("Reconnect")
onClicked: root.reconnectCanvas();
anchors.horizontalCenter: parent.horizontalCenter
}
} // of buttons column
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick 2.0
Rectangle {
id: root
border.width: 1
border.color: "orange"
color: "#1f1f1f"
height: contentBox.childrenRect.height + 40
width: 600
Component.onCompleted: {
if (!_application.showGettingStarted) {
root.visible = false;
}
}
Column {
id: contentBox
width: parent.width - 20
y: 20
spacing: 20
anchors {
horizontalCenter: parent.horizontalCenter
}
Text {
width: parent.width
text: _application.gettingStartedText
wrapMode: Text.WordWrap
color: "white"
}
Row {
id: buttonRow
spacing: 20
anchors.horizontalCenter: parent.horizontalCenter
height: childrenRect.height
Button {
label: qsTr("Okay")
onClicked: root.visible = false
width: 150
}
Button {
label: qsTr("Don't show again")
onClicked: {
_application.showGettingStarted = false;
root.visible = false
}
width: 150
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
import QtQuick 2.2
Item {
id: root
property alias text: input.text
property alias label: labelText.text
signal editingFinished
implicitHeight: 30
implicitWidth: 200
Text {
id: labelText
anchors.left: parent.left
anchors.right: inputFrame.left
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: inputFrame
border.width: 2
border.color: input.focus ? "orange" : "#9f9f9f"
color: "#3f3f3f"
width: parent.width * 0.5
anchors.right: parent.right
height: root.height
TextInput {
id: input
anchors {
left: parent.left
leftMargin: 8
right: parent.right
rightMargin: 8
verticalCenter: parent.verticalCenter
}
onActiveFocusChanged: {
if (activeFocus) {
selectAll();
}
}
verticalAlignment: Text.AlignVCenter
onEditingFinished: {
root.editingFinished();
}
color: "#9f9f9f"
}
}
}
+136
View File
@@ -0,0 +1,136 @@
import QtQuick 2.0
import FlightGear 1.0 as FG
Item {
id: root
signal requestPanelClose();
Rectangle {
id: savePanel
width: parent.width - 8
anchors.top: parent.top
anchors.topMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
border.color: "#9f9f9f"
border.width: 1
color: "#5f5f5f"
opacity: 0.8
layer.enabled: true
InputLine {
id: saveTitleInput
width: parent.width
label: "Title"
anchors {
top: parent.top
topMargin: 8
left: parent.left
leftMargin: 8
right: saveButton.left
rightMargin: 8
}
}
Button {
id: saveButton
label: "Save"
enabled: (saveTitleInput.text != "")
anchors.right: parent.right
anchors.rightMargin: 8
anchors.top: parent.top
anchors.topMargin: 8
onClicked: {
_application.save(saveTitleInput.text);
}
}
ListView {
id: savedList
model: _application.configs
width: parent.width - 30
anchors.top: saveTitleInput.bottom
anchors.topMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
delegate: Item {
width: parent.width
height:delegateFrame.height + 8
Rectangle {
id: delegateBackFrame
color: "#1f1f1f"
width: delegateFrame.width
height: delegateFrame.height
clip: true
Button {
id: deleteButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 8
label: "Delete"
onClicked: {
_application.deleteConfig(model.index)
}
}
Button {
anchors.verticalCenter: parent.verticalCenter
anchors.right: deleteButton.left
anchors.rightMargin: 8
label: "Save"
onClicked: {
_application.saveConfigChanges(model.index)
}
}
}
Rectangle {
id: delegateFrame
width: parent.width
// anchors.horizontalCenter: parent.horizontalCenter
height: configLabel.implicitHeight + 20
opacity: 1.0
color: "#3f3f3f"
Text {
id: configLabel
text: modelData['name']
color: "white"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
}
MouseArea {
anchors.fill: parent
onClicked: {
_application.restoreConfig(model.index)
root.requestPanelClose();
}
drag.target: delegateFrame
drag.axis: Drag.XAxis
drag.minimumX: -delegateFrame.width
drag.maximumX: 0
}
} // of visible rect
} // of delegate item
}
} // of frame rect
}
+126
View File
@@ -0,0 +1,126 @@
import QtQuick 2.0
import FlightGear 1.0 as FG
Item {
id: root
signal requestPanelClose();
Rectangle {
id: savePanel
width: parent.width - 8
anchors.top: parent.top
anchors.topMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
border.color: "#9f9f9f"
border.width: 1
color: "#5f5f5f"
opacity: 0.8
layer.enabled: true
InputLine {
id: saveTitleInput
width: parent.width
label: "Title"
anchors {
top: parent.top
topMargin: 8
left: parent.left
leftMargin: 8
right: saveButton.left
rightMargin: 8
}
}
Button {
id: saveButton
label: "Save"
enabled: (saveTitleInput.text != "")
anchors.right: parent.right
anchors.rightMargin: 8
anchors.top: parent.top
anchors.topMargin: 8
onClicked: {
_application.saveSnapshot(saveTitleInput.text);
}
}
ListView {
id: savedList
model: _application.snapshots
width: parent.width - 30
anchors.top: saveTitleInput.bottom
anchors.topMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
delegate: Item {
width: parent.width
height:delegateFrame.height + 8
Rectangle {
id: delegateBackFrame
color: "#1f1f1f"
width: delegateFrame.width
height: delegateFrame.height
Button {
id: deleteButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 8
label: "Delete"
onClicked: {
// _application.deleteConfig(model.index)
// _application.deleteConfig
}
}
}
Rectangle {
id: delegateFrame
width: parent.width
height: configLabel.implicitHeight + 20
opacity: 1.0
color: "#3f3f3f"
Text {
id: configLabel
text: modelData['name']
color: "white"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 8
}
MouseArea {
anchors.fill: parent
onClicked: {
_application.restoreSnapshot(model.index);
root.requestPanelClose();
}
drag.target: delegateFrame
drag.axis: Drag.XAxis
drag.minimumX: -delegateFrame.width
drag.maximumX: 0
}
} // of visible rect
} // of delegate item
}
} // of frame rect
}
+112
View File
@@ -0,0 +1,112 @@
import QtQuick 2.0
Item {
id: root
property int activeTab: -1
property var tabs: []
property var titles: []
property int __panelWidth: 250
readonly property int panelWidth: __panelWidth
Rectangle {
id: contentBox
x: parent.width - width // can't use an anchors, for dragability
height: parent.height
width: (activeTab >= 0) ? __panelWidth : 0
color: "#1f1f1f"
Behavior on width {
enabled: !splitterDrag.drag.active
NumberAnimation {
duration: 250
}
}
clip: true
Loader {
id: loader
anchors.fill: parent
sourceComponent: (activeTab >= 0) ? tabs[activeTab] : null
}
Connections {
target: loader.item
onRequestPanelClose: root.activeTab = -1;
ignoreUnknownSignals: true
}
}
Rectangle {
id: splitter
width: 2
height: parent.height
anchors.right: contentBox.left
color: "orange"
MouseArea {
id: splitterDrag
height: parent.height
width: parent.width * 25
drag.target: contentBox
drag.axis: Drag.XAxis
drag.minimumX: 0
drag.maximumX: root.width
onMouseXChanged: {
__panelWidth = root.width - contentBox.x
}
// enabled when open?
// click to toggle open / close
cursorShape: Qt.SizeHorCursor
}
}
Column {
anchors.right: splitter.left
anchors.top: parent.top
anchors.topMargin: 20
spacing: 1
Repeater {
model: tabs
Rectangle {
readonly property bool tabIsActive: (model.index == activeTab)
width: tabLabel.implicitHeight + 10
height: tabLabel.implicitWidth + 20
color: "#3f3f3f"
border.width: 1
border.color: "#5f5f5f"
Text {
anchors.centerIn: parent
id: tabLabel
text: titles[model.index]
rotation: 90
color: tabIsActive ? "orange" : tabMouse.containsMouse ? "#afafaf" : "#9f9f9f"
transformOrigin: Item.Center
}
MouseArea {
id: tabMouse
hoverEnabled: true
anchors.fill: parent
onClicked: {
if (parent.tabIsActive) {
activeTab = -1
} else {
activeTab = model.index
}
}
}
} // rectangle
} // of repeater
}
}
+89
View File
@@ -0,0 +1,89 @@
import QtQuick 2.0
Rectangle {
width: 1024
height: 768
color: "black"
// only show the UI on the main window
property double __uiOpacity: __shouldShowUi ? 1.0 : 0.0
property bool __uiVisible: true
readonly property bool __shouldShowUi: (isMainWindow && _application.showUI)
readonly property bool isMainWindow: (_windowNumber === 0)
Component.onCompleted: {
// synchronize insitial state of this
__uiVisible = __shouldShowUi;
}
Behavior on __uiOpacity {
SequentialAnimation {
ScriptAction { script: if (_application.showUI) __uiVisible = true; }
NumberAnimation { duration: 400 }
ScriptAction { script: if (!_application.showUI) __uiVisible = false; }
}
}
Image {
opacity: __uiOpacity * 0.5
source: "qrc:///images/checkerboard"
fillMode: Image.Tile
anchors.fill: parent
visible: __uiVisible
}
Repeater {
model: _application.activeCanvases
// 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
}
}
VerticalTabPanel {
anchors.fill: parent
tabs: [browsePanel, configPanel, snapshotsPanel]
titles: ["Connect", "Load / Save", "Snapshots"]
visible: __uiVisible
opacity: __uiOpacity
}
Component {
id: browsePanel
BrowsePanel { }
}
Component {
id: configPanel
LoadSavePanel { }
}
Component {
id: snapshotsPanel
SnapshotsPanel { }
}
GetStarted {
visible: isMainWindow
anchors.centerIn: parent
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B