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