1
0
Fork 0
flightgear/utils/fgqcanvas/fgcanvaselement.cpp
James Turner 8f205040dc Work on clipping in remote canvas
Different canvas clip reference frames are handled now, and updating
the clip nodes should not longer crash. Unfortunately, clips set
on groups don't work yet, further work is needed here.
2018-06-21 14:34:05 +01:00

519 lines
14 KiB
C++

//
// 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)
{
float m[6] = { 1.0, 0.0, 0.0, 1.0, 0.0, 0.0 }; // identity matrix
for (int i =0; i< 6; ++i) {
LocalProp* mProp = prop->getOrCreateChildWithNameAndIndex(NameIndexTuple("m", i));
if (!mProp->value().isNull()) {
m[i] = mProp->value().toFloat();
}
}
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)
{
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();
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.toFloat());
}
_styleDirty = false;
}
doPolish();
_polishRequired = false;
}
void FGCanvasElement::paint(FGCanvasPaintContext *context) const
{
if (!isVisible()) {
return;
}
QPainter* p = context->painter();
p->save();
if (_hasClip)
{
// clip is defined in the global coordinate system
#if 0
p->save();
p->setTransform(context->globalCoordinateTransform());
p->setPen(Qt::yellow);
p->setBrush(QBrush(Qt::yellow, Qt::DiagCrossPattern));
p->drawRect(_clipRect);
p->restore();
#endif
QTransform t = p->transform();
p->setTransform(context->globalCoordinateTransform());
p->setClipRect(_clipRect);
p->setClipping(true);
p->setTransform(t);
}
QTransform combined = combinedTransform();
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](QVariant value) { _svgElementId = value.toByteArray(); });
return true;
} else if (prop->name() == "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 (prop->name() == "layer-type") {
connect(prop, &LocalProp::valueChanged, [this](QVariant value)
{qDebug() << "layer-type:" << value.toByteArray() << "on" << _propertyRoot->path(); });
return true;
} else if (prop->name() == "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());
int centerTerm = senderProp->index();
if (centerTerm == 0) {
_center.setX(value.toFloat());
} else {
_center.setY(value.toFloat());
}
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();
}
float 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;
float v = value.toFloat(&ok);
if (!ok) {
qWarning() << "failed to parse:" << value;
}
return v;
}
QColor FGCanvasElement::parseColorValue(QVariant value) const
{
QString colorString = value.toString();
if (colorString.isEmpty() || colorString == "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::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 float top = parseCSSValue(clipRectDesc.at(0));
const float right = parseCSSValue(clipRectDesc.at(1));
const float bottom = parseCSSValue(clipRectDesc.at(2));
const float left = parseCSSValue(clipRectDesc.at(3));
_clipRect = QRectF(left, top, right - left, bottom - top);
// qDebug() << "final clip rect:" << _clipRect << "from" << value;
_hasClip = true;
requestPolish();
}