first commit
This commit is contained in:
544
utils/fgqcanvas/fgcanvaselement.cpp
Normal file
544
utils/fgqcanvas/fgcanvaselement.cpp
Normal file
@@ -0,0 +1,544 @@
|
||||
//
|
||||
// Copyright (C) 2017 James Turner zakalawe@mac.com
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License as
|
||||
// published by the Free Software Foundation; either version 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but
|
||||
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
#include "fgcanvaselement.h"
|
||||
|
||||
#include "localprop.h"
|
||||
#include "fgcanvaspaintcontext.h"
|
||||
#include "fgcanvasgroup.h"
|
||||
#include "canvasitem.h"
|
||||
#include "canvasconnection.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QMatrix4x4>
|
||||
|
||||
QTransform qTransformFromCanvas(LocalProp* prop)
|
||||
{
|
||||
double m[6] = { 1.0, 0.0, 0.0, 1.0, 0.0, 0.0 }; // identity matrix
|
||||
for (unsigned int i =0; i< 6; ++i) {
|
||||
LocalProp* mProp = prop->getOrCreateChildWithNameAndIndex(NameIndexTuple("m", i));
|
||||
if (!mProp->value().isNull()) {
|
||||
m[i] = mProp->value().toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
return QTransform(m[0], m[1], 0.0,
|
||||
m[2], m[3], 0.0,
|
||||
m[4], m[5], 1.0);
|
||||
}
|
||||
|
||||
bool FGCanvasElement::isStyleProperty(QByteArray name)
|
||||
{
|
||||
if ((name == "font") || (name == "line-height") || (name == "alignment")
|
||||
|| (name == "character-size") || (name == "fill") || (name == "background")
|
||||
|| (name == "fill-opacity"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalProp *FGCanvasElement::property() const
|
||||
{
|
||||
return const_cast<LocalProp*>(_propertyRoot);
|
||||
}
|
||||
|
||||
void FGCanvasElement::setHighlighted(bool hilighted)
|
||||
{
|
||||
_highlighted = hilighted;
|
||||
}
|
||||
|
||||
bool FGCanvasElement::isHighlighted() const
|
||||
{
|
||||
return _highlighted;
|
||||
}
|
||||
|
||||
CanvasItem *FGCanvasElement::createQuickItem(QQuickItem *parent)
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CanvasItem *FGCanvasElement::quickItem() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FGCanvasElement::FGCanvasElement(FGCanvasGroup* pr, LocalProp* prop) :
|
||||
QObject(pr),
|
||||
_propertyRoot(prop),
|
||||
_parent(pr)
|
||||
{
|
||||
connect(prop->getOrCreateWithPath("visible", true), &LocalProp::valueChanged,
|
||||
this, &FGCanvasElement::onVisibleChanged);
|
||||
connect(prop, &LocalProp::childAdded, this, &FGCanvasElement::onChildAdded);
|
||||
connect(prop, &LocalProp::childRemoved, this, &FGCanvasElement::onChildRemoved);
|
||||
connect(prop, &LocalProp::destroyed, this, &FGCanvasElement::onPropDestroyed);
|
||||
|
||||
if (pr) {
|
||||
pr->markChildZIndicesDirty();
|
||||
}
|
||||
|
||||
requestPolish();
|
||||
}
|
||||
|
||||
void FGCanvasElement::onPropDestroyed()
|
||||
{
|
||||
doDestroy();
|
||||
if (_parent) {
|
||||
const_cast<FGCanvasGroup*>(_parent)->removeChild(this);
|
||||
}
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
void FGCanvasElement::requestPolish()
|
||||
{
|
||||
_polishRequired = true;
|
||||
}
|
||||
|
||||
void FGCanvasElement::polish()
|
||||
{
|
||||
bool vis = isVisible();
|
||||
auto qq = quickItem();
|
||||
if (qq && (qq->isVisible() != vis)) {
|
||||
qq->setVisible(vis);
|
||||
}
|
||||
|
||||
if (!vis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_clipDirty) {
|
||||
_clipDirty = false;
|
||||
if (qq) {
|
||||
if (_hasClip) {
|
||||
if (_clipFrame == ReferenceFrame::GLOBAL) {
|
||||
qq->setClipReferenceFrameItem(rootGroup()->quickItem());
|
||||
} else if (_clipFrame == ReferenceFrame::PARENT) {
|
||||
qq->setClipReferenceFrameItem(parentGroup()->quickItem());
|
||||
}
|
||||
qq->setObjectName(_propertyRoot->path());
|
||||
qq->setClip(_clipRect, _clipFrame);
|
||||
} else {
|
||||
qq->clearClip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (qq) {
|
||||
qq->setTransform(combinedTransform());
|
||||
}
|
||||
|
||||
if (_styleDirty) {
|
||||
_fillColor = parseColorValue(getCascadedStyle("fill"));
|
||||
const auto opacity = getCascadedStyle("fill-opacity");
|
||||
if (!opacity.isNull()) {
|
||||
_fillColor.setAlphaF(opacity.toReal());
|
||||
}
|
||||
|
||||
_styleDirty = false;
|
||||
}
|
||||
|
||||
doPolish();
|
||||
_polishRequired = false;
|
||||
}
|
||||
|
||||
void FGCanvasElement::dumpElement()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void FGCanvasElement::paint(FGCanvasPaintContext *context) const
|
||||
{
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter* p = context->painter();
|
||||
p->save();
|
||||
|
||||
QTransform combined = combinedTransform();
|
||||
|
||||
if (_hasClip)
|
||||
{
|
||||
QTransform t = p->transform();
|
||||
|
||||
// clip is defined in the global coordinate system
|
||||
if (_clipFrame == ReferenceFrame::GLOBAL) {
|
||||
// this rpelaces the transform entirely
|
||||
p->setTransform(context->globalCoordinateTransform());
|
||||
} else if (_clipFrame == ReferenceFrame::LOCAL) {
|
||||
p->setTransform(combined, true /* combine */);
|
||||
} else if (_clipFrame == ReferenceFrame::PARENT) {
|
||||
// incoming transform is already our parent
|
||||
} else {
|
||||
qWarning() << "Unhandled clip type:" << static_cast<int>(_clipFrame) << "at" << property()->path();
|
||||
}
|
||||
|
||||
#if defined(DEBUG_PAINTING)
|
||||
p->setPen(Qt::yellow);
|
||||
p->setBrush(QBrush(Qt::yellow, Qt::DiagCrossPattern));
|
||||
p->drawRect(_clipRect);
|
||||
#endif
|
||||
p->setClipping(true);
|
||||
p->setClipRect(_clipRect);
|
||||
p->setTransform(t); // restore the previous transformation
|
||||
}
|
||||
|
||||
p->setTransform(combined, true /* combine */);
|
||||
|
||||
if (!_fillColor.isValid()) {
|
||||
p->setBrush(Qt::NoBrush);
|
||||
} else {
|
||||
p->setBrush(_fillColor);
|
||||
}
|
||||
|
||||
doPaint(context);
|
||||
|
||||
if (_hasClip) {
|
||||
p->setClipping(false);
|
||||
}
|
||||
|
||||
p->restore();
|
||||
}
|
||||
|
||||
void FGCanvasElement::doPaint(FGCanvasPaintContext* context) const
|
||||
{
|
||||
Q_UNUSED(context);
|
||||
}
|
||||
|
||||
void FGCanvasElement::doPolish()
|
||||
{
|
||||
}
|
||||
|
||||
QTransform FGCanvasElement::combinedTransform() const
|
||||
{
|
||||
if (_transformsDirty) {
|
||||
_combinedTransform.reset();
|
||||
|
||||
for (LocalProp* tfProp : _propertyRoot->childrenWithName("tf")) {
|
||||
_combinedTransform *= qTransformFromCanvas(tfProp);
|
||||
}
|
||||
|
||||
#if 0
|
||||
QPointF offset(_propertyRoot->value("center-offset-x", 0.0).toFloat(),
|
||||
_propertyRoot->value("center-offset-y", 0.0).toFloat());
|
||||
|
||||
_combinedTransform.translate(offset.x(), offset.y());
|
||||
#endif
|
||||
_transformsDirty = false;
|
||||
}
|
||||
|
||||
return _combinedTransform;
|
||||
}
|
||||
|
||||
bool FGCanvasElement::isVisible() const
|
||||
{
|
||||
return _visible;
|
||||
}
|
||||
|
||||
int FGCanvasElement::zIndex() const
|
||||
{
|
||||
return _zIndex;
|
||||
}
|
||||
|
||||
const FGCanvasGroup *FGCanvasElement::parentGroup() const
|
||||
{
|
||||
return _parent;
|
||||
}
|
||||
|
||||
const FGCanvasGroup *FGCanvasElement::rootGroup() const
|
||||
{
|
||||
if (!_parent) {
|
||||
return qobject_cast<const FGCanvasGroup*>(this);
|
||||
}
|
||||
|
||||
return _parent->rootGroup();
|
||||
}
|
||||
|
||||
CanvasConnection *FGCanvasElement::connection() const
|
||||
{
|
||||
if (_parent)
|
||||
return _parent->connection();
|
||||
return qobject_cast<CanvasConnection*>(parent());
|
||||
}
|
||||
|
||||
bool FGCanvasElement::onChildAdded(LocalProp *prop)
|
||||
{
|
||||
const QByteArray nm = prop->name();
|
||||
if (nm == "tf") {
|
||||
connect(prop, &LocalProp::childAdded, this, &FGCanvasElement::onChildAdded);
|
||||
return true;
|
||||
} else if (nm == "visible") {
|
||||
return true;
|
||||
} else if (nm == "tf-rot-index") {
|
||||
// ignored, this is noise from the Nasal SVG parser
|
||||
return true;
|
||||
} else if (nm.startsWith("center-offset-")) {
|
||||
// ignored, this is noise from the Nasal SVG parser
|
||||
return true;
|
||||
} else if (nm == "center") {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::onCenterChanged);
|
||||
return true;
|
||||
} else if (nm == "m") {
|
||||
if ((prop->parent()->name() == "tf") && (prop->parent()->parent() == _propertyRoot)) {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markTransformsDirty);
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "saw confusing 'm' property" << prop->path();
|
||||
}
|
||||
} else if (nm == "m-geo") {
|
||||
// ignore for now, we do geo projection server-side
|
||||
return true;
|
||||
} else if (nm == "id") {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markSVGIDDirty);
|
||||
return true;
|
||||
} else if (nm == "update") {
|
||||
// disable updates optionally?
|
||||
return true;
|
||||
} else if ((nm == "clip") || (nm == "clip-frame")) {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markClipDirty);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isStyleProperty(nm)) {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markStyleDirty);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nm == "symbol-type") {
|
||||
// ignored for now
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nm == "layer-type") {
|
||||
connect(prop, &LocalProp::valueChanged, [this](QVariant value)
|
||||
{qDebug() << "layer-type:" << value.toByteArray() << "on" << _propertyRoot->path(); });
|
||||
return true;
|
||||
} else if (nm == "z-index") {
|
||||
connect(prop, &LocalProp::valueChanged, this, &FGCanvasElement::markZIndexDirty);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FGCanvasElement::onChildRemoved(LocalProp *prop)
|
||||
{
|
||||
const QByteArray nm = prop->name();
|
||||
if ((nm == "tf") || (nm == "m")) {
|
||||
markTransformsDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void FGCanvasElement::doDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
QColor FGCanvasElement::fillColor() const
|
||||
{
|
||||
return _fillColor;
|
||||
}
|
||||
|
||||
void FGCanvasElement::onCenterChanged(QVariant value)
|
||||
{
|
||||
LocalProp* senderProp = static_cast<LocalProp*>(sender());
|
||||
const unsigned int centerTerm = senderProp->index();
|
||||
|
||||
if (centerTerm == 0) {
|
||||
_center.setX(value.toReal());
|
||||
} else {
|
||||
_center.setY(value.toReal());
|
||||
}
|
||||
|
||||
requestPolish();
|
||||
}
|
||||
|
||||
void FGCanvasElement::markTransformsDirty()
|
||||
{
|
||||
_transformsDirty = true;
|
||||
requestPolish();
|
||||
}
|
||||
|
||||
void FGCanvasElement::markClipDirty()
|
||||
{
|
||||
_clipDirty = true;
|
||||
parseCSSClip(_propertyRoot->value("clip", QVariant()).toByteArray());
|
||||
_clipFrame = static_cast<ReferenceFrame>(_propertyRoot->value("clip-frame", 0).toInt());
|
||||
requestPolish();
|
||||
}
|
||||
|
||||
double FGCanvasElement::parseCSSValue(QByteArray value) const
|
||||
{
|
||||
value = value.trimmed();
|
||||
// deal with %, px suffixes
|
||||
if (value.indexOf('%') >= 0) {
|
||||
qWarning() << Q_FUNC_INFO << "extend parsing to deal with:" << value;
|
||||
}
|
||||
if (value.endsWith("px")) {
|
||||
value.truncate(value.length() - 2);
|
||||
}
|
||||
bool ok = false;
|
||||
qreal v = value.toDouble(&ok);
|
||||
if (!ok) {
|
||||
qWarning() << "failed to parse:" << value;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
QColor FGCanvasElement::parseColorValue(QVariant value) const
|
||||
{
|
||||
QString colorString = value.toString();
|
||||
if (colorString.isEmpty() || (colorString == QStringLiteral("none"))) {
|
||||
return QColor(); // return an invalid color
|
||||
}
|
||||
|
||||
int alpha = 255;
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
bool good = false;
|
||||
|
||||
if (colorString.startsWith('#')) {
|
||||
// web style
|
||||
if (colorString.length() == 9) {
|
||||
QRegularExpression re("#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})");
|
||||
QRegularExpressionMatch match = re.match(colorString);
|
||||
if (match.hasMatch()) {
|
||||
red = match.captured(1).toInt(nullptr, 16);
|
||||
green = match.captured(2).toInt(nullptr, 16);
|
||||
blue = match.captured(3).toInt(nullptr, 16);
|
||||
alpha = match.captured(4).toInt(nullptr, 16);
|
||||
good = true;
|
||||
}
|
||||
} else if (colorString.length() == 7) {
|
||||
// long form, RGB
|
||||
QRegularExpression re("#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})");
|
||||
QRegularExpressionMatch match = re.match(colorString);
|
||||
if (match.hasMatch()) {
|
||||
red = match.captured(1).toInt(nullptr, 16);
|
||||
green = match.captured(2).toInt(nullptr, 16);
|
||||
blue = match.captured(3).toInt(nullptr, 16);
|
||||
good = true;
|
||||
}
|
||||
}
|
||||
} else if (colorString.startsWith("rgb")) {
|
||||
// try rgb(ddd, ddd, ddd) syntax
|
||||
QRegularExpression re("rgb\\((\\d*),(\\d*),(\\d*)\\)");
|
||||
QRegularExpressionMatch match = re.match(value.toString());
|
||||
if (match.hasMatch()) {
|
||||
red = match.captured(1).toInt();
|
||||
green = match.captured(2).toInt();
|
||||
blue = match.captured(3).toInt();
|
||||
good = true;
|
||||
}
|
||||
|
||||
QRegularExpression re2("rgba\\((\\d*),(\\d*),(\\d*),(\\d*)\\)");
|
||||
match = re2.match(value.toString());
|
||||
if (match.hasMatch()) {
|
||||
red = match.captured(1).toInt();
|
||||
green = match.captured(2).toInt();
|
||||
blue = match.captured(3).toInt();
|
||||
alpha = match.captured(4).toInt() * 255;
|
||||
good = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (good) {
|
||||
return QColor(red, green, blue, alpha);
|
||||
}
|
||||
|
||||
qWarning() << _propertyRoot->path() << "failed to parse color:" << colorString;
|
||||
return Qt::magenta; // default horrible colour
|
||||
}
|
||||
|
||||
void FGCanvasElement::markStyleDirty()
|
||||
{
|
||||
_styleDirty = true;
|
||||
requestPolish();
|
||||
// group will cascade
|
||||
}
|
||||
|
||||
QVariant FGCanvasElement::getCascadedStyle(const char *name, QVariant defaultValue) const
|
||||
{
|
||||
LocalProp* style = _propertyRoot->childWithNameAndIndex(NameIndexTuple(name, 0));
|
||||
if (style) {
|
||||
return style->value();
|
||||
}
|
||||
|
||||
if (_parent) {
|
||||
return _parent->getCascadedStyle(name);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
void FGCanvasElement::markZIndexDirty(QVariant value)
|
||||
{
|
||||
_zIndex = value.toInt();
|
||||
_parent->markChildZIndicesDirty();
|
||||
}
|
||||
|
||||
void FGCanvasElement::markSVGIDDirty(QVariant value)
|
||||
{
|
||||
_svgElementId = value.toByteArray();
|
||||
}
|
||||
|
||||
void FGCanvasElement::onVisibleChanged(QVariant value)
|
||||
{
|
||||
_visible = value.toBool();
|
||||
requestPolish();
|
||||
}
|
||||
|
||||
void FGCanvasElement::parseCSSClip(QByteArray value)
|
||||
{
|
||||
if (value.isEmpty()) {
|
||||
_hasClip = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// https://www.w3.org/wiki/CSS/Properties/clip for the stupid order here
|
||||
if (value.startsWith("rect(")) {
|
||||
int closingParen = value.indexOf(')');
|
||||
value = value.mid(5, closingParen - 5); // trim front portion
|
||||
}
|
||||
|
||||
QByteArrayList clipRectDesc = value.split(',');
|
||||
const int parts = clipRectDesc.size();
|
||||
if (parts != 4) {
|
||||
qWarning() << "implement parsing for non-standard clip" << value;
|
||||
return;
|
||||
}
|
||||
|
||||
const qreal top = parseCSSValue(clipRectDesc.at(0));
|
||||
const qreal right = parseCSSValue(clipRectDesc.at(1));
|
||||
const qreal bottom = parseCSSValue(clipRectDesc.at(2));
|
||||
const qreal left = parseCSSValue(clipRectDesc.at(3));
|
||||
|
||||
_clipRect = QRectF(left, top, right - left, bottom - top);
|
||||
// qDebug() << "final clip rect:" << _clipRect << "from" << value;
|
||||
_hasClip = true;
|
||||
|
||||
requestPolish();
|
||||
}
|
||||
Reference in New Issue
Block a user