File indexing completed on 2024-05-19 05:42:21

0001 // ct_lvtqtc_lakosrelation.cpp                                      -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtqtc_lakosrelation.h>
0021 
0022 #include <ct_lvtqtc_graphicsscene.h>
0023 #include <ct_lvtqtc_inspect_dependency_window.h>
0024 #include <ct_lvtqtc_lakosentity.h>
0025 
0026 #include <QDebug>
0027 #include <QDialog>
0028 #include <QFileInfo>
0029 #include <QGraphicsPathItem>
0030 #include <QGraphicsScene>
0031 #include <QHeaderView>
0032 #include <QLayout>
0033 #include <QLine>
0034 #include <QMenu>
0035 #include <QPainter>
0036 #include <QUrl>
0037 #include <QtMath>
0038 
0039 #include "preferences.h"
0040 
0041 namespace {
0042 
0043 constexpr int DEBUG_PATH_LAYER = 100;
0044 
0045 // static, we share those for every instance of the edges.
0046 // 5 bytes less per instance, and sometimes we have *lots* of instances.
0047 bool s_showBoundingRect = false; // NOLINT
0048 bool s_showShape = false; // NOLINT
0049 bool s_showTextualInformation = false; // NOLINT
0050 bool s_showIntersectionPaths = false; // NOLINT
0051 bool s_showOriginalLine = false; // NOLINT
0052 
0053 QPointF closestPointTo(const QPointF& target, const QPainterPath& sourcePath)
0054 // Returns the closest element (position) in \a sourcePath to \a target,
0055 // using \l{QPoint::manhattanLength()} to determine the distances.
0056 {
0057     if (sourcePath.isEmpty()) {
0058         return {};
0059     }
0060 
0061     qreal comparedLength = std::numeric_limits<int>::max();
0062 
0063     QPointF foundDistance;
0064     for (int i = 0; i < sourcePath.elementCount(); ++i) {
0065         const qreal length = QPointF(sourcePath.elementAt(i) - target).manhattanLength();
0066         if (length < comparedLength) {
0067             foundDistance = sourcePath.elementAt(i);
0068             comparedLength = length;
0069         }
0070     }
0071 
0072     return foundDistance;
0073 }
0074 
0075 } // namespace
0076 
0077 namespace Codethink::lvtqtc {
0078 
0079 struct LakosRelation::Private {
0080     QLineF line;
0081     // This is the line, as set by setLine
0082     QLineF adjustedLine;
0083     // This is the line, after the adjusts we need to do
0084     // to take into consideration the size of the tail item
0085     // head item, and any boundary that we might intercept.
0086 
0087     QGraphicsPathItem *head = nullptr;
0088     // the graphicsitem on the .p2() of the line
0089 
0090     QGraphicsPathItem *tail = nullptr;
0091     // the graphicsitem on the .p1() of the line
0092 
0093     LakosEntity *pointsFrom = nullptr;
0094     LakosEntity *pointsTo = nullptr;
0095     // the Nodes that this edge connects
0096 
0097     int relationFlags = EdgeCollection::RelationFlags::RelationFlagsNone;
0098     // We use the information below to paint the item.
0099 
0100     int selectedCounter = 0;
0101     // Keep track on how many selections this relation has.
0102     // This is used to avoid unselecting something that should be selected.
0103 
0104     qreal thickness = 0.5;
0105     // thickness of the line being drawn.
0106 
0107     bool dashed = false;
0108     Qt::PenStyle penStyle = Qt::PenStyle::SolidLine;
0109 
0110     bool shouldBeHidden = false;
0111     // This will not be touched by Qt when we show() or hide()
0112     // an item, so it's an way to *really* hide the items.
0113     // TODO: create a StateMachine to control visibility.
0114 
0115     QGraphicsPathItem *fromIntersectionItem = nullptr;
0116     QGraphicsPathItem *toIntersectionItem = nullptr;
0117 
0118     QPainterPath fromIntersection;
0119     QPainterPath toIntersection;
0120 
0121     QRectF boundingRect;
0122     QPainterPath shape;
0123     // used to return the `shape()` call.
0124     // also used to draw the selection area.
0125 
0126     QColor color = QColor(0, 0, 0);
0127     QColor highlightColor = QColor(255, 0, 0);
0128 };
0129 
0130 LakosRelation::LakosRelation(LakosEntity *source, LakosEntity *target): d(std::make_unique<LakosRelation::Private>())
0131 {
0132     d->pointsFrom = source;
0133     d->pointsTo = target;
0134 
0135     d->fromIntersectionItem = new QGraphicsPathItem(d->fromIntersection, this);
0136     d->toIntersectionItem = new QGraphicsPathItem(d->toIntersection, this);
0137     d->fromIntersectionItem->setZValue(DEBUG_PATH_LAYER);
0138     d->toIntersectionItem->setZValue(DEBUG_PATH_LAYER);
0139     d->fromIntersectionItem->setVisible(false);
0140     d->toIntersectionItem->setVisible(false);
0141 
0142     d->color = Preferences::edgeColor();
0143     d->highlightColor = Preferences::highlightEdgeColor();
0144     connect(Preferences::self(), &Preferences::edgeColorChanged, this, [this] {
0145         d->color = Preferences::edgeColor();
0146         update();
0147     });
0148     connect(Preferences::self(), &Preferences::highlightEdgeColorChanged, this, [this] {
0149         d->highlightColor = Preferences::highlightEdgeColor();
0150         update();
0151     });
0152 }
0153 
0154 LakosRelation::~LakosRelation() = default;
0155 
0156 void LakosRelation::setColor(QColor const& newColor)
0157 {
0158     if (newColor == d->color) {
0159         return;
0160     }
0161 
0162     d->color = newColor;
0163     update();
0164 }
0165 
0166 void LakosRelation::setStyle(Qt::PenStyle const& newStyle)
0167 {
0168     if (newStyle == d->penStyle) {
0169         return;
0170     }
0171     d->penStyle = newStyle;
0172     update();
0173 }
0174 
0175 QRectF LakosRelation::boundingRect() const
0176 {
0177     return d->boundingRect;
0178 }
0179 
0180 void LakosRelation::setHead(QGraphicsPathItem *head)
0181 {
0182     d->head = head;
0183     d->head->setParentItem(this);
0184     d->head->setPos(d->line.p2());
0185     QPen currentPen = head->pen();
0186     currentPen.setCosmetic(true);
0187     head->setPen(currentPen);
0188 }
0189 
0190 void LakosRelation::setTail(QGraphicsPathItem *tail)
0191 {
0192     d->tail = tail;
0193     d->tail->setParentItem(this);
0194     d->tail->setPos(d->line.p1());
0195 
0196     QPen currentPen = tail->pen();
0197     currentPen.setCosmetic(true);
0198     tail->setPen(currentPen);
0199 }
0200 
0201 std::pair<QPainterPath, QPainterPath> LakosRelation::calculateIntersection(QGraphicsItem *lhs, QGraphicsItem *rhs)
0202 // creates a intersection area from the origin point up to
0203 // the boundary of the element with 1px wide, with minimum of
0204 // 4 points, that are going to be used on the closestTo function
0205 // above.
0206 // The closest point is then returned as the 'hit' point - the point
0207 // that the line intersects to.
0208 {
0209     // silence scanbuild.
0210     if (!lhs || !rhs) {
0211         return {};
0212     }
0213 
0214     QLineF pathAsLine = d->line;
0215 
0216     // Extend the first point in the path out by 1 pixel.
0217     QLineF startEdge = pathAsLine.normalVector();
0218     startEdge.setLength(1);
0219 
0220     // Swap the points in the line so the normal vector is at the other end of the line.
0221     pathAsLine.setPoints(pathAsLine.p2(), pathAsLine.p1());
0222     QLineF endEdge = pathAsLine.normalVector();
0223 
0224     // The end point is currently pointing the wrong way; move it to face the same
0225     // direction as startEdge.
0226     endEdge.setLength(-1);
0227 
0228     // Now we can create a rectangle from our edges.
0229     QPainterPath rectPath(startEdge.p1());
0230     rectPath.lineTo(startEdge.p2());
0231     rectPath.lineTo(endEdge.p2());
0232     rectPath.lineTo(endEdge.p1());
0233     rectPath.lineTo(startEdge.p1());
0234 
0235     // Map the path to global coordinates
0236     rectPath = mapToScene(rectPath);
0237 
0238     // Calculate things on the global coordinates
0239     const QPainterPath lhsIntersection = lhs->mapToScene(lhs->shape()).intersected(rectPath);
0240     const QPainterPath rhsIntersection = rhs->mapToScene(rhs->shape()).intersected(rectPath);
0241 
0242     // map the results to Local coordinates
0243     return {mapFromScene(lhsIntersection), mapFromScene(rhsIntersection)};
0244 }
0245 
0246 void LakosRelation::setLine(const QLineF& line)
0247 {
0248     prepareGeometryChange();
0249 
0250     d->line = line;
0251 
0252     if (!d->pointsFrom->isVisible() && !d->pointsTo->isVisible()) {
0253         if (d->pointsFrom->parentItem() == d->pointsTo->parentItem()) {
0254             setVisible(false);
0255             return;
0256         }
0257     }
0258 
0259     QGraphicsItem *lhs = d->pointsFrom->isVisible() ? d->pointsFrom : d->pointsFrom->parentItem();
0260     QGraphicsItem *rhs = d->pointsTo->isVisible() ? d->pointsTo : d->pointsTo->parentItem();
0261     assert(lhs);
0262     assert(rhs);
0263 
0264     auto [intersection1, intersection2] = calculateIntersection(lhs, rhs);
0265 
0266     d->fromIntersection = intersection1;
0267     d->toIntersection = intersection2;
0268 
0269     d->fromIntersectionItem->setPath(d->fromIntersection);
0270     d->toIntersectionItem->setPath(d->toIntersection);
0271 
0272     // The hit position will be the element (point) of the rectangle that is the
0273     // closest to where the projectile was fired from.
0274     auto hitPoint1 = closestPointTo(line.p2(), intersection1);
0275     auto hitPoint2 = closestPointTo(line.p1(), intersection2);
0276 
0277     d->adjustedLine.setP1(hitPoint1);
0278     d->adjustedLine.setP2(hitPoint2);
0279 
0280     const float headLength = d->head ? d->head->boundingRect().width() : 0;
0281     const float tailLength = d->tail ? d->tail->boundingRect().width() : 0;
0282 
0283     // Shorten line to give space for head
0284     if (d->adjustedLine.length() <= headLength) {
0285         d->adjustedLine.setLength(0.01);
0286     } else {
0287         d->adjustedLine.setLength(d->adjustedLine.length() - headLength);
0288     }
0289 
0290     static const qreal kClickTolerance = 10;
0291 
0292     QPointF vec = d->adjustedLine.p2() - d->adjustedLine.p1();
0293     vec = vec * (kClickTolerance / std::sqrt(QPointF::dotProduct(vec, vec)));
0294 
0295     QPointF orthogonal(vec.y(), -vec.x());
0296 
0297     QPainterPath result(d->adjustedLine.p1() - vec + orthogonal);
0298     result.lineTo(d->adjustedLine.p1() - vec - orthogonal);
0299     result.lineTo(d->adjustedLine.p2() + vec - orthogonal);
0300     result.lineTo(d->adjustedLine.p2() + vec + orthogonal);
0301     result.closeSubpath();
0302 
0303     d->shape = result;
0304     d->boundingRect = d->shape.boundingRect();
0305 
0306     if (d->head) {
0307         QLineF headVector = QLineF::fromPolar(headLength, d->adjustedLine.angle());
0308         headVector.setAngle(d->adjustedLine.angle());
0309         d->head->setPos(d->adjustedLine.p2().x() + headVector.x2(), d->adjustedLine.p2().y() + headVector.y2());
0310         d->head->setRotation(-d->adjustedLine.angle());
0311     }
0312 
0313     if (d->tail) {
0314         QLineF tailVector = QLineF::fromPolar(tailLength, d->adjustedLine.angle());
0315         d->tail->setPos(d->adjustedLine.p1().x() + tailVector.x2(), d->adjustedLine.p1().y() + tailVector.y2());
0316         d->tail->setRotation(-d->adjustedLine.angle());
0317     }
0318 
0319     updateTooltip();
0320     update();
0321 }
0322 
0323 void LakosRelation::updateTooltip()
0324 {
0325     setToolTip(QString::fromStdString(legendText()));
0326 }
0327 
0328 void LakosRelation::setLine(qreal x1, qreal y1, qreal x2, qreal y2)
0329 {
0330     setLine(QLineF(x1, y1, x2, y2));
0331 }
0332 
0333 QLineF LakosRelation::line() const
0334 {
0335     return d->line;
0336 }
0337 
0338 QLineF LakosRelation::adjustedLine() const
0339 {
0340     return d->adjustedLine;
0341 }
0342 
0343 QPainterPath LakosRelation::shape() const
0344 {
0345     return d->shape;
0346 }
0347 
0348 void LakosRelation::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0349 {
0350     // This feels like a hack, but we need a different flag from isVisible
0351     // to hide some data that we *really* do not want to display.
0352     if (d->shouldBeHidden) {
0353         return;
0354     }
0355 
0356     Q_UNUSED(option);
0357     Q_UNUSED(widget);
0358 
0359     const auto overrideC = overrideColor();
0360     auto const color = overrideC.isValid() ? overrideC : d->color;
0361     auto pen = QPen(color);
0362 
0363     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsHighlighted) {
0364         pen.setWidthF(d->thickness + 2);
0365     } else {
0366         pen.setWidthF(d->thickness);
0367     }
0368     pen.setStyle(d->penStyle);
0369 
0370     painter->save();
0371     painter->setPen(pen);
0372     painter->drawLine(d->adjustedLine);
0373     painter->restore();
0374 
0375     auto updateQGraphicsPathItemColor = [](QGraphicsPathItem *item, QColor const& color) {
0376         if (item == nullptr) {
0377             return;
0378         }
0379 
0380         auto headPen = item->pen();
0381         headPen.setColor(color);
0382         item->setPen(headPen);
0383 
0384         auto headBrush = item->brush();
0385         headBrush.setColor(color);
0386         item->setBrush(headBrush);
0387     };
0388     updateQGraphicsPathItemColor(d->head, color);
0389     updateQGraphicsPathItemColor(d->tail, color);
0390 
0391     if (s_showOriginalLine) {
0392         painter->save();
0393         painter->setPen(Qt::red);
0394         painter->drawLine(d->line);
0395         painter->restore();
0396     }
0397 
0398     if (s_showBoundingRect) {
0399         painter->save();
0400         painter->setPen(QPen(Qt::blue));
0401         painter->drawRect(boundingRect());
0402         painter->restore();
0403     }
0404 
0405     if (s_showShape) {
0406         painter->save();
0407         painter->setPen(QPen(Qt::magenta));
0408         painter->drawPath(shape());
0409         painter->restore();
0410     }
0411 
0412     if (s_showTextualInformation) {
0413         painter->save();
0414         painter->setPen(QPen(Qt::red));
0415 
0416         const auto x = boundingRect().topLeft().x();
0417         const auto y = boundingRect().topLeft().y();
0418 
0419         QString information = QObject::tr("Pos: (%1, %2), P1: (%3, %4), P2: (%5, %6)")
0420                                   .arg(pos().x())
0421                                   .arg(pos().y())
0422                                   .arg(d->line.p1().x())
0423                                   .arg(d->line.p1().y())
0424                                   .arg(d->line.p2().x())
0425                                   .arg(d->line.p2().y());
0426 
0427         painter->drawText(QPointF(x, y), information);
0428         painter->restore();
0429     }
0430 
0431     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsHighlighted
0432         || d->relationFlags & EdgeCollection::RelationFlags::RelationIsSelected) {
0433         setZValue(99);
0434     } else {
0435         setZValue(1);
0436     }
0437 }
0438 
0439 QGraphicsPathItem *LakosRelation::defaultArrow()
0440 {
0441     QPainterPath arrowHead;
0442     arrowHead.moveTo(0, 0);
0443     arrowHead.lineTo(-10, 5);
0444     arrowHead.lineTo(-10, -5);
0445     arrowHead.lineTo(0, 0);
0446     arrowHead.closeSubpath();
0447     arrowHead.setFillRule(Qt::FillRule::WindingFill);
0448 
0449     auto *arrow = new QGraphicsPathItem();
0450     arrow->setPath(arrowHead);
0451     arrow->setBrush(Qt::black);
0452 
0453     return arrow;
0454 }
0455 
0456 QGraphicsPathItem *LakosRelation::diamondArrow()
0457 {
0458     QPainterPath arrowHead;
0459     arrowHead.moveTo(0, 0);
0460     arrowHead.lineTo(-10, 5);
0461     arrowHead.lineTo(-20, 0);
0462     arrowHead.lineTo(-10, -5);
0463     arrowHead.lineTo(0, 0);
0464     arrowHead.closeSubpath();
0465     arrowHead.setFillRule(Qt::FillRule::WindingFill);
0466 
0467     auto *arrow = new QGraphicsPathItem();
0468     arrow->setPath(arrowHead);
0469     arrow->setBrush(Qt::black);
0470 
0471     return arrow;
0472 }
0473 
0474 QGraphicsPathItem *LakosRelation::defaultTail()
0475 {
0476     QPainterPath circlePath;
0477     circlePath.addEllipse(0, -5, 10, 10);
0478 
0479     auto *tail = new QGraphicsPathItem();
0480     tail->setPath(circlePath);
0481     tail->setBrush(QBrush(QColor(Qt::white)));
0482     return tail;
0483 }
0484 
0485 LakosEntity *LakosRelation::from() const
0486 {
0487     return d->pointsFrom;
0488 }
0489 
0490 LakosEntity *LakosRelation::to() const
0491 {
0492     return d->pointsTo;
0493 }
0494 
0495 void LakosRelation::toggleRelationFlags(EdgeCollection::RelationFlags flags, bool toggle)
0496 {
0497     if (toggle) {
0498         if (flags & EdgeCollection::RelationFlags::RelationIsSelected) {
0499             d->selectedCounter += 1;
0500         }
0501         d->relationFlags |= flags;
0502     } else {
0503         if (flags & EdgeCollection::RelationFlags::RelationIsSelected) {
0504             d->selectedCounter -= 1;
0505             if (d->selectedCounter == 0) {
0506                 d->relationFlags &= ~flags;
0507             }
0508         } else {
0509             d->relationFlags &= ~flags;
0510         }
0511     }
0512 
0513     update();
0514 }
0515 
0516 void LakosRelation::setThickness(qreal thickness)
0517 {
0518     if (!qFuzzyCompare(thickness, d->thickness)) {
0519         d->thickness = thickness;
0520         update();
0521     }
0522 }
0523 
0524 void LakosRelation::setDashed(bool dashed)
0525 {
0526     if (dashed == d->dashed) {
0527         return;
0528     }
0529 
0530     d->dashed = dashed;
0531     this->setStyle(d->dashed ? Qt::DashLine : Qt::SolidLine);
0532     update();
0533 }
0534 
0535 void LakosRelation::setShouldBeHidden(bool hidden)
0536 {
0537     if (d->shouldBeHidden != hidden) {
0538         d->shouldBeHidden = hidden;
0539         if (hidden) {
0540             if (d->tail) {
0541                 delete d->tail;
0542                 d->tail = nullptr;
0543             }
0544             if (d->head) {
0545                 delete d->head;
0546                 d->head = nullptr;
0547             }
0548         }
0549         update();
0550     }
0551 }
0552 
0553 bool LakosRelation::shouldBeHidden() const
0554 {
0555     return d->shouldBeHidden;
0556 }
0557 
0558 void LakosRelation::populateMenu(QMenu& menu, QMenu *debugMenu)
0559 {
0560     auto *gs = qobject_cast<GraphicsScene *>(scene());
0561 
0562     menu.addAction(
0563         QObject::tr("%1 to %2").arg(QString::fromStdString(from()->name()), QString::fromStdString(to()->name())));
0564 
0565     auto *removeAction = new QAction(tr("Remove"));
0566     removeAction->setToolTip(tr("Removes this relation from the database"));
0567     connect(removeAction, &QAction::triggered, this, &LakosRelation::requestRemoval);
0568     menu.addAction(removeAction);
0569 
0570     auto *node = d->pointsFrom->internalNode();
0571     if (node->type() == lvtshr::DiagramType::PackageType && !node->isPackageGroup()) {
0572         auto *action = new QAction(tr("Inspect package dependencies"));
0573         action->setToolTip(tr("Shows a list of dependencies between those packages"));
0574         connect(action, &QAction::triggered, this, [this, gs]() {
0575             auto inspectDepWindow = InspectDependencyWindow{gs->projectFile(), *this};
0576             inspectDepWindow.show();
0577             inspectDepWindow.exec();
0578         });
0579         menu.addAction(action);
0580     }
0581 
0582     Q_UNUSED(debugMenu);
0583 }
0584 
0585 std::string LakosRelation::legendText() const
0586 {
0587     std::string ret = from()->name() + " to " + to()->name() + "\n";
0588     ret += "Type: " + relationTypeAsString() + "\n";
0589 
0590     if (Preferences::enableSceneContextMenu()) {
0591         const std::string x1 = std::to_string(d->adjustedLine.p1().x());
0592         const std::string x2 = std::to_string(d->adjustedLine.p2().x());
0593         const std::string y1 = std::to_string(d->adjustedLine.p1().y());
0594         const std::string y2 = std::to_string(d->adjustedLine.p2().y());
0595 
0596         ret += "From: (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")\n";
0597     }
0598 
0599     return ret;
0600 }
0601 
0602 void LakosRelation::toggleIntersectionPaths()
0603 {
0604     s_showIntersectionPaths = !s_showIntersectionPaths;
0605 }
0606 
0607 void LakosRelation::toggleBoundingRect()
0608 {
0609     s_showBoundingRect = !s_showBoundingRect;
0610 }
0611 
0612 void LakosRelation::toggleShape()
0613 {
0614     s_showShape = !s_showShape;
0615 }
0616 
0617 void LakosRelation::toggleTextualInformation()
0618 {
0619     s_showTextualInformation = !s_showTextualInformation;
0620 }
0621 
0622 void LakosRelation::toggleOriginalLine()
0623 {
0624     s_showOriginalLine = !s_showOriginalLine;
0625 }
0626 
0627 void LakosRelation::updateDebugInformation()
0628 {
0629     d->fromIntersectionItem->setVisible(s_showIntersectionPaths);
0630     d->toIntersectionItem->setVisible(s_showIntersectionPaths);
0631     update();
0632 }
0633 
0634 bool LakosRelation::showBoundingRect()
0635 {
0636     return s_showBoundingRect;
0637 }
0638 
0639 bool LakosRelation::showShape()
0640 {
0641     return s_showShape;
0642 }
0643 
0644 bool LakosRelation::showTextualInformation()
0645 {
0646     return s_showTextualInformation;
0647 }
0648 
0649 bool LakosRelation::showIntersectionPaths()
0650 {
0651     return s_showIntersectionPaths;
0652 }
0653 
0654 bool LakosRelation::showOriginalLine()
0655 {
0656     return s_showOriginalLine;
0657 }
0658 
0659 QColor LakosRelation::hoverColor() const
0660 {
0661     return d->highlightColor;
0662 }
0663 
0664 QColor LakosRelation::overrideColor() const
0665 {
0666     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsSelected) {
0667         return Qt::red;
0668     }
0669     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsHighlighted) {
0670         return Qt::red;
0671     }
0672     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsCyclic) {
0673         return Qt::blue;
0674     }
0675     if (d->relationFlags & EdgeCollection::RelationFlags::RelationIsParentHovered) {
0676         return hoverColor();
0677     }
0678     return {};
0679 }
0680 
0681 } // end namespace Codethink::lvtqtc