first commit
This commit is contained in:
@@ -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})
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 |
Reference in New Issue
Block a user