File indexing completed on 2025-02-02 04:26:09
0001 /* 0002 * SPDX-FileCopyrightText: 2022 Marco Martin <mart@kde.org> 0003 * 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "AnnotationDocument.h" 0008 #include "EffectUtils.h" 0009 #include "Geometry.h" 0010 0011 #include <QGuiApplication> 0012 #include <QPainter> 0013 #include <QPainterPath> 0014 #include <QQuickItem> 0015 #include <QQuickWindow> 0016 #include <QScreen> 0017 #include <memory> 0018 0019 using G = Geometry; 0020 0021 AnnotationDocument::AnnotationDocument(QObject *parent) 0022 : QObject(parent) 0023 , m_tool(new AnnotationTool(this)) 0024 , m_selectedItemWrapper(new SelectedItemWrapper(this)) 0025 { 0026 } 0027 0028 AnnotationDocument::~AnnotationDocument() 0029 { 0030 } 0031 0032 AnnotationTool *AnnotationDocument::tool() const 0033 { 0034 return m_tool; 0035 } 0036 0037 SelectedItemWrapper *AnnotationDocument::selectedItemWrapper() const 0038 { 0039 return m_selectedItemWrapper; 0040 } 0041 0042 int AnnotationDocument::undoStackDepth() const 0043 { 0044 return m_history.undoList().size(); 0045 } 0046 0047 int AnnotationDocument::redoStackDepth() const 0048 { 0049 return m_history.redoList().size(); 0050 } 0051 0052 QSizeF AnnotationDocument::canvasSize() const 0053 { 0054 return m_canvasRect.size(); 0055 } 0056 0057 QSizeF AnnotationDocument::imageSize() const 0058 { 0059 return m_image.size(); 0060 } 0061 0062 qreal AnnotationDocument::imageDpr() const 0063 { 0064 return m_image.devicePixelRatio(); 0065 } 0066 0067 void AnnotationDocument::setImage(const QImage &image) 0068 { 0069 m_image = image; 0070 m_canvasRect = {{0, 0}, QSizeF(image.size()) / image.devicePixelRatio()}; 0071 0072 Q_EMIT canvasSizeChanged(); 0073 Q_EMIT imageSizeChanged(); 0074 Q_EMIT imageDprChanged(); 0075 Q_EMIT repaintNeeded(); 0076 } 0077 0078 void AnnotationDocument::cropCanvas(const QRectF &cropRect) 0079 { 0080 if (cropRect == m_canvasRect) { 0081 return; 0082 } 0083 0084 auto translate = QTransform::fromTranslate(-cropRect.x(), -cropRect.y()); 0085 auto intersectsRect = [cropRect](History::List::const_reference item) { 0086 return item && item->renderRect().intersects(cropRect); 0087 }; 0088 History::Lists filteredLists = m_history.filteredLists(intersectsRect); 0089 auto &filteredUndo = filteredLists.undoList; 0090 auto &filteredRedo = filteredLists.redoList; 0091 for (auto it = filteredUndo.begin(); it != filteredUndo.end(); ++it) { 0092 Traits::transformTraits(translate, it->get()->traits()); 0093 } 0094 for (auto it = filteredRedo.begin(); it != filteredRedo.end(); ++it) { 0095 Traits::transformTraits(translate, it->get()->traits()); 0096 } 0097 m_history = {filteredUndo, filteredRedo}; 0098 0099 setImage(m_image.copy(G::rectScaled(cropRect, imageDpr()).toAlignedRect())); 0100 0101 Q_EMIT canvasSizeChanged(); 0102 Q_EMIT undoStackDepthChanged(); 0103 Q_EMIT redoStackDepthChanged(); 0104 Q_EMIT repaintNeeded(); 0105 } 0106 0107 void AnnotationDocument::clearAnnotations() 0108 { 0109 auto result = m_history.clearLists(); 0110 m_tool->resetType(); 0111 m_tool->resetNumber(); 0112 deselectItem(); 0113 if (result.undoListChanged) { 0114 Q_EMIT undoStackDepthChanged(); 0115 } 0116 if (result.redoListChanged) { 0117 Q_EMIT redoStackDepthChanged(); 0118 } 0119 Q_EMIT repaintNeeded(); 0120 } 0121 0122 void AnnotationDocument::clear() 0123 { 0124 clearAnnotations(); 0125 setImage({}); 0126 } 0127 0128 void AnnotationDocument::paintBaseImage(QPainter *painter, const Viewport &viewport) const 0129 { 0130 painter->save(); 0131 auto imageRect = G::rectScaled(viewport.rect, imageDpr() / viewport.scale); 0132 // Enable smooth transform for fractional scales. 0133 painter->setRenderHint(QPainter::SmoothPixmapTransform, fmod(imageDpr() / viewport.scale, 1) != 0); 0134 if (viewport.scale == 1) { 0135 painter->drawImage({0, 0}, m_image, imageRect); 0136 } else { 0137 // More High quality scale down 0138 auto scaledImg = m_image.scaled(m_image.size() * viewport.scale, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); 0139 painter->drawImage({0, 0}, scaledImg, imageRect); 0140 } 0141 painter->restore(); 0142 } 0143 void AnnotationDocument::paintAnnotations(QPainter *painter, const Viewport &viewport, std::optional<History::ConstSpan> span) const 0144 { 0145 painter->save(); 0146 painter->scale(viewport.scale, viewport.scale); 0147 painter->setRenderHints({QPainter::Antialiasing, QPainter::TextAntialiasing}); 0148 const auto &undoList = m_history.undoList(); 0149 if (!span) { 0150 span.emplace(undoList); 0151 } 0152 const auto begin = span->begin(); 0153 for (auto it = begin; it != span->end(); ++it) { 0154 if (!m_history.itemVisible(*it)) { 0155 continue; 0156 } 0157 // Render the temporary item instead if this item is selected. 0158 auto selectedItem = m_selectedItemWrapper->selectedItem().lock(); 0159 auto renderedItem = *it == selectedItem ? m_tempItem.get() : it->get(); 0160 if (!renderedItem) { 0161 continue; 0162 } 0163 auto &geometry = std::get<Traits::Geometry::Opt>(renderedItem->traits()); 0164 if (!geometry->visualRect.intersects(G::rectScaled(viewport.rect, 1 / viewport.scale))) { 0165 continue; 0166 } 0167 0168 painter->save(); // Remember to call restore later. 0169 painter->translate(-viewport.rect.topLeft()); 0170 painter->setPen(Qt::NoPen); 0171 painter->setBrush(Qt::NoBrush); 0172 0173 auto &highlight = std::get<Traits::Highlight::Opt>(renderedItem->traits()); 0174 painter->setCompositionMode(highlight ? highlight->compositionMode : QPainter::CompositionMode_SourceOver); 0175 0176 // Draw the shadow if existent 0177 auto &shadow = std::get<Traits::Shadow::Opt>(renderedItem->traits()); 0178 if (shadow && shadow->enabled) { 0179 QImage image = shapeShadow(renderedItem->traits()); 0180 painter->setRenderHint(QPainter::SmoothPixmapTransform, true); 0181 painter->drawImage(geometry->visualRect, image); 0182 painter->setRenderHint(QPainter::SmoothPixmapTransform, false); 0183 } 0184 0185 if (auto &fillOpt = std::get<Traits::Fill::Opt>(renderedItem->traits())) { 0186 using namespace Traits; 0187 auto &fill = fillOpt.value(); 0188 switch (fill.index()) { 0189 case Fill::Brush: 0190 painter->setBrush(std::get<Fill::Brush>(fill)); 0191 painter->drawPath(geometry->path); 0192 break; 0193 case Traits::Fill::Blur: { 0194 auto &blur = std::get<Fill::Blur>(fill); 0195 auto getImage = [this, begin, it] { 0196 return renderToImage(std::span{begin, it}); 0197 }; 0198 const auto &rect = geometry->path.boundingRect(); 0199 const auto &image = blur.image(getImage, rect, imageDpr()); 0200 painter->drawImage(rect, image); 0201 } break; 0202 case Traits::Fill::Pixelate: { 0203 auto &pixelate = std::get<Fill::Pixelate>(fill); 0204 auto getImage = [this, begin, it] { 0205 return renderToImage(std::span{begin, it}); 0206 }; 0207 const auto &rect = geometry->path.boundingRect(); 0208 const auto &image = pixelate.image(getImage, rect, imageDpr()); 0209 painter->drawImage(rect, image); 0210 } break; 0211 default: 0212 break; 0213 } 0214 } 0215 0216 if (auto &stroke = std::get<Traits::Stroke::Opt>(renderedItem->traits())) { 0217 painter->setBrush(stroke->pen.brush()); 0218 painter->drawPath(stroke->path); 0219 } 0220 0221 if (auto &text = std::get<Traits::Text::Opt>(renderedItem->traits())) { 0222 painter->setBrush(Qt::NoBrush); 0223 painter->setPen(text->brush.color()); 0224 painter->setFont(text->font); 0225 painter->drawText(geometry->path.boundingRect(), text->textFlags(), text->text()); 0226 } 0227 0228 painter->restore(); 0229 } 0230 painter->restore(); 0231 } 0232 0233 void AnnotationDocument::paint(QPainter *painter, const Viewport &viewport, std::optional<History::ConstSpan> span) const 0234 { 0235 paintBaseImage(painter, viewport); 0236 paintAnnotations(painter, viewport, span); 0237 } 0238 0239 QImage AnnotationDocument::renderToImage(const Viewport &viewport, std::optional<History::ConstSpan> span) const 0240 { 0241 QImage img((viewport.rect.size() * imageDpr()).toSize(), QImage::Format_ARGB32_Premultiplied); 0242 img.setDevicePixelRatio(imageDpr()); 0243 img.fill(Qt::transparent); 0244 QPainter p(&img); 0245 p.setRenderHint(QPainter::Antialiasing); 0246 // Makes pixelate and blur look better 0247 p.setRenderHint(QPainter::SmoothPixmapTransform, true); 0248 paint(&p, viewport, span); 0249 p.end(); 0250 0251 return img; 0252 } 0253 0254 QImage AnnotationDocument::renderToImage(std::optional<History::ConstSpan> span) const 0255 { 0256 return renderToImage({m_canvasRect, 1}, span); 0257 } 0258 0259 bool AnnotationDocument::isCurrentItemValid() const 0260 { 0261 return m_history.currentItem() && m_history.currentItem()->isValid(); 0262 } 0263 0264 HistoryItem::shared_ptr AnnotationDocument::popCurrentItem() 0265 { 0266 auto result = m_history.pop(); 0267 if (result.item) { 0268 if (result.item == m_selectedItemWrapper->selectedItem().lock()) { 0269 deselectItem(); 0270 } 0271 Q_EMIT undoStackDepthChanged(); 0272 emitRepaintNeededUnlessEmpty(result.item->renderRect()); 0273 } 0274 if (result.redoListChanged) { 0275 Q_EMIT redoStackDepthChanged(); 0276 } 0277 return result.item; 0278 } 0279 0280 HistoryItem::const_shared_ptr AnnotationDocument::itemAt(const QRectF &rect) const 0281 { 0282 const auto &undoList = m_history.undoList(); 0283 // Precisely the first time so that users can get exactly what they click. 0284 for (auto it = undoList.crbegin(); it != undoList.crend(); ++it) { 0285 auto &item = *it; 0286 if (m_history.itemVisible(item)) { 0287 auto &geometry = std::get<Traits::Geometry::Opt>(item->traits()); 0288 if (geometry->mousePath.contains(rect.center())) { 0289 return item; 0290 } 0291 } 0292 } 0293 // If rect has no width or height 0294 if (rect.isNull()) { 0295 return nullptr; 0296 } 0297 // Forgiving if that failed so that you don't need to be perfect. 0298 for (auto it = undoList.crbegin(); it != undoList.crend(); ++it) { 0299 auto &item = *it; 0300 if (m_history.itemVisible(item)) { 0301 QPainterPath path(rect.topLeft()); 0302 path.addEllipse(rect); 0303 auto &geometry = std::get<Traits::Geometry::Opt>(item->traits()); 0304 if (geometry->mousePath.intersects(path)) { 0305 return item; 0306 } 0307 } 0308 } 0309 return nullptr; 0310 } 0311 0312 void AnnotationDocument::undo() 0313 { 0314 const auto undoCount = m_history.undoList().size(); 0315 if (!undoCount) { 0316 return; 0317 } 0318 0319 auto currentItem = m_history.currentItem(); 0320 auto prevItem = undoCount > 1 ? m_history.undoList()[undoCount - 2] : nullptr; 0321 auto updateRect = currentItem->renderRect(); 0322 if (prevItem) { 0323 updateRect |= prevItem->renderRect(); 0324 } 0325 if (auto text = std::get<Traits::Text::Opt>(currentItem->traits())) { 0326 if (text->index() == Traits::Text::Number) { 0327 m_tool->setNumber(std::get<Traits::Text::Number>(text.value())); 0328 } 0329 } 0330 if (currentItem == m_selectedItemWrapper->selectedItem().lock()) { 0331 if (prevItem && currentItem->hasParent() && (prevItem == currentItem->parent())) { 0332 m_selectedItemWrapper->setSelectedItem(prevItem); 0333 } else { 0334 deselectItem(); 0335 } 0336 } 0337 m_history.undo(); 0338 0339 Q_EMIT undoStackDepthChanged(); 0340 Q_EMIT redoStackDepthChanged(); 0341 emitRepaintNeededUnlessEmpty(updateRect); 0342 } 0343 0344 void AnnotationDocument::redo() 0345 { 0346 if (m_history.redoList().empty()) { 0347 return; 0348 } 0349 0350 auto currentItem = m_history.currentItem(); 0351 auto nextItem = *m_history.redoList().crbegin(); 0352 auto updateRect = nextItem->renderRect(); 0353 if (currentItem) { 0354 updateRect |= currentItem->renderRect(); 0355 } 0356 if (auto text = std::get<Traits::Text::Opt>(nextItem->traits())) { 0357 if (text->index() == Traits::Text::Number) { 0358 m_tool->setNumber(std::get<Traits::Text::Number>(text.value()) + 1); 0359 } 0360 } 0361 if (currentItem && currentItem == m_selectedItemWrapper->selectedItem()) { 0362 if (nextItem == currentItem->child()) { 0363 m_selectedItemWrapper->setSelectedItem(nextItem); 0364 } else { 0365 deselectItem(); 0366 } 0367 } 0368 m_history.redo(); 0369 0370 Q_EMIT undoStackDepthChanged(); 0371 Q_EMIT redoStackDepthChanged(); 0372 emitRepaintNeededUnlessEmpty(updateRect); 0373 } 0374 0375 bool isAnyOfToolType(AnnotationTool::Tool type, auto... args) 0376 { 0377 return ((type == args) || ...); 0378 } 0379 0380 void AnnotationDocument::beginItem(const QPointF &point) 0381 { 0382 if (!m_tool->isCreationTool()) { 0383 return; 0384 } 0385 0386 QRectF updateRect; 0387 // if the last item was not valid, discard it (for instance a rectangle with 0 size) 0388 if (!isCurrentItemValid()) { 0389 auto result = m_history.pop(); 0390 if (result.item) { 0391 updateRect = result.item->renderRect(); 0392 } 0393 } 0394 0395 using enum AnnotationTool::Tool; 0396 using enum AnnotationTool::Option; 0397 HistoryItem temp; 0398 auto &geometry = std::get<Traits::Geometry::Opt>(temp.traits()); 0399 geometry.emplace(QPainterPath{point}, QPainterPath{point}, QRectF{point, point}); 0400 0401 auto toolType = m_tool->type(); 0402 auto toolOptions = m_tool->options(); 0403 if (toolType == BlurTool) { 0404 auto &fill = std::get<Traits::Fill::Opt>(temp.traits()); 0405 fill.emplace(Traits::ImageEffects::Blur{4}); 0406 } else if (toolType == PixelateTool) { 0407 auto &fill = std::get<Traits::Fill::Opt>(temp.traits()); 0408 fill.emplace(Traits::ImageEffects::Pixelate{4}); 0409 } else if (toolOptions.testFlag(FillOption)) { 0410 auto &fill = std::get<Traits::Fill::Opt>(temp.traits()); 0411 fill.emplace(m_tool->fillColor()); 0412 } 0413 0414 if (toolOptions.testFlag(StrokeOption)) { 0415 auto &stroke = std::get<Traits::Stroke::Opt>(temp.traits()); 0416 auto pen = Traits::Stroke::defaultPen(); 0417 pen.setBrush(m_tool->strokeColor()); 0418 pen.setWidthF(m_tool->strokeWidth()); 0419 stroke.emplace(pen); 0420 } 0421 0422 if (toolOptions.testFlag(ShadowOption)) { 0423 auto &shadow = std::get<Traits::Shadow::Opt>(temp.traits()); 0424 shadow.emplace(m_tool->hasShadow()); 0425 } 0426 0427 if (isAnyOfToolType(toolType, FreehandTool, HighlighterTool)) { 0428 geometry->path = Traits::minPath(geometry->path); 0429 } 0430 if (toolType == HighlighterTool) { 0431 std::get<Traits::Highlight::Opt>(temp.traits()).emplace(); 0432 } else if (toolType == ArrowTool) { 0433 std::get<Traits::Arrow::Opt>(temp.traits()).emplace(); 0434 } else if (toolType == NumberTool) { 0435 std::get<Traits::Text::Opt>(temp.traits()).emplace(m_tool->number(), m_tool->fontColor(), m_tool->font()); 0436 m_tool->setNumber(m_tool->number() + 1); 0437 } else if (toolType == TextTool) { 0438 std::get<Traits::Text::Opt>(temp.traits()).emplace(QString{}, m_tool->fontColor(), m_tool->font()); 0439 } 0440 0441 Traits::initOptTuple(temp.traits()); 0442 0443 auto newItem = std::make_shared<HistoryItem>(std::move(temp)); 0444 updateRect |= newItem->renderRect(); 0445 addItem(newItem); 0446 m_selectedItemWrapper->setSelectedItem(newItem); 0447 emitRepaintNeededUnlessEmpty(updateRect); 0448 } 0449 0450 void AnnotationDocument::continueItem(const QPointF &point, ContinueOptions options) 0451 { 0452 const auto ¤tItem = m_history.currentItem(); 0453 bool isSelected = m_selectedItemWrapper->selectedItem() == currentItem; 0454 const auto &item = isSelected ? m_tempItem : currentItem; 0455 if (!m_tool->isCreationTool() || !item || !std::get<Traits::Geometry::Opt>(item->traits())) { 0456 return; 0457 } 0458 0459 auto renderRect = item->renderRect(); 0460 auto &geometry = std::get<Traits::Geometry::Opt>(item->traits()); 0461 auto &path = geometry->path; 0462 switch (m_tool->type()) { 0463 case AnnotationTool::FreehandTool: 0464 case AnnotationTool::HighlighterTool: { 0465 auto prev = path.currentPosition(); 0466 // smooth path as we go. 0467 path.quadTo(prev, (prev + point) / 2); 0468 } break; 0469 case AnnotationTool::LineTool: 0470 case AnnotationTool::ArrowTool: { 0471 auto count = path.elementCount(); 0472 auto lastElement = path.elementAt(count - 1); 0473 QPointF endPoint = point; 0474 if (options & ContinueOption::SnapAngle) { 0475 const auto &prevElement = count > 1 ? path.elementAt(count - 2) : lastElement; 0476 QPointF posDiff = point - prevElement; 0477 if (qAbs(posDiff.x()) / 1.5 > qAbs(posDiff.y())) { 0478 // horizontal 0479 endPoint.setY(prevElement.y); 0480 } else if (qAbs(posDiff.x()) < qAbs(posDiff.y()) / 1.5) { 0481 // vertical 0482 endPoint.setX(prevElement.x); 0483 } else { 0484 // diagonal when 1/3 in between horizontal and vertical 0485 auto xSign = std::copysign(1.0, posDiff.x()); 0486 auto ySign = std::copysign(1.0, posDiff.y()); 0487 qreal max = qMax(qAbs(posDiff.x()), qAbs(posDiff.y())); 0488 endPoint = prevElement + QPointF(max * xSign, max * ySign); 0489 } 0490 } 0491 if (count > 1 && !lastElement.isMoveTo()) { 0492 path.setElementPositionAt(count - 1, endPoint.x(), endPoint.y()); 0493 } else { 0494 path.lineTo(endPoint); 0495 } 0496 } break; 0497 case AnnotationTool::RectangleTool: 0498 case AnnotationTool::EllipseTool: 0499 case AnnotationTool::BlurTool: 0500 case AnnotationTool::PixelateTool: { 0501 const auto count = path.elementCount(); 0502 // We always make the real start point the last point so we can easily keep it without 0503 // needing to keep a separate point or rectangle. Qt automatically moves the first MoveTo 0504 // element if one exists, so we can't just keep it at the start. 0505 auto start = path.currentPosition(); 0506 // Can have a negative size with bottom right being visually top left. 0507 QRectF rect(start, point); 0508 if (options & ContinueOption::SnapAngle) { 0509 auto wSign = std::copysign(1.0, rect.width()); 0510 auto hSign = std::copysign(1.0, rect.height()); 0511 qreal max = qMax(qAbs(rect.width()), qAbs(rect.height())); 0512 rect.setSize({max * wSign, max * hSign}); 0513 } 0514 0515 if (options & ContinueOption::CenterResize) { 0516 if (count > 1) { 0517 auto oldBounds = path.boundingRect(); 0518 rect.moveCenter(oldBounds.center()); 0519 } else { 0520 rect.moveCenter(start); 0521 } 0522 } 0523 path.clear(); 0524 if (m_tool->type() == AnnotationTool::EllipseTool) { 0525 path.addEllipse(rect); 0526 } else { 0527 path.addRect(rect); 0528 } 0529 // the top left is now the real start point 0530 path.moveTo(rect.topLeft()); 0531 } break; 0532 case AnnotationTool::TextTool: { 0533 const auto count = path.elementCount(); 0534 auto rect = path.boundingRect(); 0535 if (count == 1) { 0536 // BUG: boundingRect won't have the correct position if the only element is a MoveTo. 0537 // Fixed in https://codereview.qt-project.org/c/qt/qtbase/+/534966. 0538 rect.moveTo(path.elementAt(0)); 0539 } 0540 path.translate(point - QPointF{rect.x(), rect.center().y()}); 0541 } break; 0542 case AnnotationTool::NumberTool: { 0543 const auto count = path.elementCount(); 0544 auto rect = path.boundingRect(); 0545 if (count == 1) { 0546 // BUG: boundingRect won't have the correct position if the only element is a MoveTo. 0547 // Fixed in https://codereview.qt-project.org/c/qt/qtbase/+/534966. 0548 rect.moveTo(path.elementAt(0)); 0549 } 0550 path.translate(point - rect.center()); 0551 } break; 0552 default: 0553 return; 0554 } 0555 0556 Traits::clearForInit(item->traits()); 0557 Traits::fastInitOptTuple(item->traits()); 0558 0559 if (isSelected) { 0560 *currentItem = *item; 0561 renderRect |= m_selectedItemWrapper->reset(); 0562 m_selectedItemWrapper->setSelectedItem(currentItem); 0563 } 0564 emitRepaintNeededUnlessEmpty(renderRect | item->renderRect()); 0565 } 0566 0567 void AnnotationDocument::finishItem() 0568 { 0569 const auto ¤tItem = m_history.currentItem(); 0570 bool isSelected = m_selectedItemWrapper->selectedItem() == currentItem; 0571 const auto &item = isSelected ? m_tempItem : currentItem; 0572 if (!m_tool->isCreationTool() || !item || !std::get<Traits::Geometry::Opt>(item->traits())) { 0573 return; 0574 } 0575 0576 Traits::initOptTuple(item->traits()); 0577 if (isSelected) { 0578 *currentItem = *item; 0579 auto renderRect = m_selectedItemWrapper->reset(); 0580 m_selectedItemWrapper->setSelectedItem(currentItem); 0581 Q_EMIT selectedItemWrapperChanged(); // re-evaluate qml bindings 0582 emitRepaintNeededUnlessEmpty(renderRect); 0583 } 0584 } 0585 0586 void AnnotationDocument::selectItem(const QRectF &rect) 0587 { 0588 m_selectedItemWrapper->setSelectedItem(itemAt(rect)); 0589 } 0590 0591 void AnnotationDocument::deselectItem() 0592 { 0593 m_selectedItemWrapper->setSelectedItem(nullptr); 0594 } 0595 0596 void AnnotationDocument::deleteSelectedItem() 0597 { 0598 auto selectedItem = m_selectedItemWrapper->selectedItem().lock(); 0599 if (!selectedItem) { 0600 return; 0601 } 0602 0603 auto newItem = std::make_shared<HistoryItem>(); 0604 HistoryItem::setItemRelations(selectedItem, newItem); 0605 addItem(newItem); 0606 deselectItem(); 0607 emitRepaintNeededUnlessEmpty(selectedItem->renderRect()); 0608 } 0609 0610 void AnnotationDocument::addItem(const HistoryItem::shared_ptr &item) 0611 { 0612 auto result = m_history.push(item); 0613 if (result.undoListChanged) { 0614 Q_EMIT undoStackDepthChanged(); 0615 } 0616 if (result.redoListChanged) { 0617 Q_EMIT redoStackDepthChanged(); 0618 } 0619 } 0620 0621 void AnnotationDocument::emitRepaintNeededUnlessEmpty(const QRectF &area) 0622 { 0623 if (!area.isEmpty()) { 0624 Q_EMIT repaintNeeded(area); 0625 } 0626 } 0627 0628 ////////////////////////// 0629 0630 SelectedItemWrapper::SelectedItemWrapper(AnnotationDocument *document) 0631 : QObject(document) 0632 , m_document(document) 0633 { 0634 } 0635 0636 SelectedItemWrapper::~SelectedItemWrapper() 0637 { 0638 } 0639 0640 HistoryItem::const_weak_ptr SelectedItemWrapper::selectedItem() const 0641 { 0642 return m_selectedItem; 0643 } 0644 0645 void SelectedItemWrapper::setSelectedItem(const HistoryItem::const_shared_ptr &historyItem) 0646 { 0647 if (m_selectedItem == historyItem // 0648 || (historyItem && !std::get<Traits::Geometry::Opt>(historyItem->traits()))) { 0649 return; 0650 } 0651 0652 m_selectedItem = historyItem; 0653 if (historyItem) { 0654 auto &temp = m_document->m_tempItem; 0655 temp = std::make_shared<HistoryItem>(*historyItem); 0656 m_options.setFlag(AnnotationTool::StrokeOption, // 0657 std::get<Traits::Stroke::Opt>(temp->traits()).has_value()); 0658 0659 auto &fill = std::get<Traits::Fill::Opt>(temp->traits()); 0660 m_options.setFlag(AnnotationTool::FillOption, // 0661 fill.has_value() && fill->index() == Traits::Fill::Brush); 0662 0663 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0664 m_options.setFlag(AnnotationTool::FontOption, text.has_value()); 0665 m_options.setFlag(AnnotationTool::TextOption, // 0666 text && text->index() == Traits::Text::String); 0667 m_options.setFlag(AnnotationTool::NumberOption, // 0668 text && text->index() == Traits::Text::Number); 0669 0670 m_options.setFlag(AnnotationTool::ShadowOption, // 0671 std::get<Traits::Shadow::Opt>(temp->traits()).has_value()); 0672 } else { 0673 m_document->emitRepaintNeededUnlessEmpty(reset()); 0674 } 0675 // all bindings using the selectedItem property should be re-evalulated when emitted 0676 Q_EMIT m_document->selectedItemWrapperChanged(); 0677 } 0678 0679 void SelectedItemWrapper::transform(qreal dx, qreal dy, Qt::Edges edges) 0680 { 0681 auto selectedItem = m_selectedItem.lock(); 0682 auto &temp = m_document->m_tempItem; 0683 if (!selectedItem || !temp || (qFuzzyIsNull(dx) && qFuzzyIsNull(dy))) { 0684 return; 0685 } 0686 auto oldRenderRect = temp->renderRect(); 0687 auto &oldPath = std::get<Traits::Geometry::Opt>(selectedItem->traits())->path; 0688 auto &path = std::get<Traits::Geometry::Opt>(temp->traits())->path; 0689 if (edges.toInt() == 0 // 0690 || edges.testFlags({Qt::TopEdge, Qt::LeftEdge, Qt::RightEdge, Qt::BottomEdge})) { 0691 const auto pathDelta = path.boundingRect().topLeft() - oldPath.boundingRect().topLeft(); 0692 QTransform transform; 0693 transform.translate(dx - pathDelta.x(), dy - pathDelta.y()); 0694 // This is less expensive since we don't regenerate stroke or mousePath when translating. 0695 Traits::transformTraits(transform, temp->traits()); 0696 } else { 0697 const auto oldRect = oldPath.boundingRect(); 0698 const auto leftEdge = edges.testFlag(Qt::LeftEdge); 0699 const auto topEdge = edges.testFlag(Qt::TopEdge); 0700 const auto newRect = oldRect.adjusted(leftEdge ? dx : 0, // 0701 topEdge ? dy : 0, 0702 edges.testFlag(Qt::RightEdge) ? dx : 0, 0703 edges.testFlag(Qt::BottomEdge) ? dy : 0); 0704 auto scale = Traits::scaleForSize(oldRect.size(), newRect.size()); 0705 auto translation = Traits::unTranslateScale(scale.sx, scale.sy, oldRect.topLeft()); 0706 translation.dx += leftEdge || oldRect.width() == 0 ? dx : 0; 0707 translation.dy += topEdge || oldRect.height() == 0 ? dy : 0; 0708 // Translate before scale to avoid scaling translation. 0709 auto transform = QTransform::fromTranslate(translation.dx, translation.dy); 0710 transform.scale(scale.sx, scale.sy); 0711 path = transform.map(oldPath); 0712 Traits::reInitTraits(temp->traits()); 0713 } 0714 const auto &newRenderRect = temp->renderRect(); 0715 Q_EMIT mousePathChanged(); 0716 m_document->emitRepaintNeededUnlessEmpty(oldRenderRect | newRenderRect); 0717 } 0718 0719 bool SelectedItemWrapper::commitChanges() 0720 { 0721 auto selectedItem = m_selectedItem.lock(); 0722 auto &temp = m_document->m_tempItem; 0723 if (!selectedItem || !temp || !temp->isValid() // 0724 || temp->traits() == selectedItem->traits()) { 0725 return false; 0726 } 0727 0728 if (!selectedItem->isValid() && selectedItem == m_document->m_history.currentItem()) { 0729 auto result = m_document->m_history.pop(); 0730 if (result.redoListChanged) { 0731 Q_EMIT m_document->redoStackDepthChanged(); 0732 } 0733 } else { 0734 HistoryItem::setItemRelations(selectedItem, temp); 0735 } 0736 m_document->addItem(temp); 0737 setSelectedItem(temp); 0738 return true; 0739 } 0740 0741 QRectF SelectedItemWrapper::reset() 0742 { 0743 auto &temp = m_document->m_tempItem; 0744 if (!hasSelection() && m_options == AnnotationTool::NoOptions) { 0745 return {}; 0746 } 0747 QRectF renderRect; 0748 if (temp) { 0749 auto selectedItem = m_selectedItem.lock(); 0750 if (selectedItem) { 0751 renderRect = selectedItem->renderRect(); 0752 } 0753 renderRect |= temp->renderRect(); 0754 } 0755 temp.reset(); 0756 m_selectedItem.reset(); 0757 m_options = AnnotationTool::NoOptions; 0758 // Not emitting selectedItemWrapperChanged. 0759 // Use the return value to determine if that should be done when necessary. 0760 return renderRect; 0761 } 0762 0763 bool SelectedItemWrapper::hasSelection() const 0764 { 0765 return !m_selectedItem.expired() && m_document->m_tempItem; 0766 } 0767 0768 AnnotationTool::Options SelectedItemWrapper::options() const 0769 { 0770 return m_options; 0771 } 0772 0773 int SelectedItemWrapper::strokeWidth() const 0774 { 0775 auto &temp = m_document->m_tempItem; 0776 if (!m_options.testFlag(AnnotationTool::StrokeOption) || !temp) { 0777 return 0; 0778 } 0779 auto &stroke = std::get<Traits::Stroke::Opt>(temp->traits()); 0780 return stroke->pen.widthF(); 0781 } 0782 0783 void SelectedItemWrapper::setStrokeWidth(int width) 0784 { 0785 auto &temp = m_document->m_tempItem; 0786 if (!m_options.testFlag(AnnotationTool::StrokeOption) || !temp) { 0787 return; 0788 } 0789 auto &stroke = std::get<Traits::Stroke::Opt>(temp->traits()); 0790 if (stroke->pen.widthF() == width) { 0791 return; 0792 } 0793 auto oldRect = temp->renderRect(); 0794 stroke->pen.setWidthF(width); 0795 Traits::reInitTraits(temp->traits()); 0796 const auto &newRect = temp->renderRect(); 0797 Q_EMIT strokeWidthChanged(); 0798 if (oldRect != newRect) { 0799 Q_EMIT mousePathChanged(); 0800 } 0801 m_document->emitRepaintNeededUnlessEmpty(oldRect | newRect); 0802 } 0803 0804 QColor SelectedItemWrapper::strokeColor() const 0805 { 0806 auto &temp = m_document->m_tempItem; 0807 if (!m_options.testFlag(AnnotationTool::StrokeOption) || !temp) { 0808 return {}; 0809 } 0810 auto &stroke = std::get<Traits::Stroke::Opt>(temp->traits()); 0811 return stroke->pen.color(); 0812 } 0813 0814 void SelectedItemWrapper::setStrokeColor(const QColor &color) 0815 { 0816 auto &temp = m_document->m_tempItem; 0817 if (!m_options.testFlag(AnnotationTool::StrokeOption) || !temp) { 0818 return; 0819 } 0820 auto &stroke = std::get<Traits::Stroke::Opt>(temp->traits()); 0821 if (stroke->pen.color() == color) { 0822 return; 0823 } 0824 stroke->pen.setColor(color); 0825 Q_EMIT strokeColorChanged(); 0826 m_document->emitRepaintNeededUnlessEmpty(temp->renderRect()); 0827 } 0828 0829 QColor SelectedItemWrapper::fillColor() const 0830 { 0831 auto &temp = m_document->m_tempItem; 0832 if (!m_options.testFlag(AnnotationTool::FillOption) || !temp) { 0833 return {}; 0834 } 0835 auto &fill = std::get<Traits::Fill::Opt>(temp->traits()).value(); 0836 auto &brush = std::get<Traits::Fill::Brush>(fill); 0837 return brush.color(); 0838 } 0839 0840 void SelectedItemWrapper::setFillColor(const QColor &color) 0841 { 0842 auto &temp = m_document->m_tempItem; 0843 if (!m_options.testFlag(AnnotationTool::FillOption) || !temp) { 0844 return; 0845 } 0846 auto &fill = std::get<Traits::Fill::Opt>(temp->traits()).value(); 0847 auto &brush = std::get<Traits::Fill::Brush>(fill); 0848 if (brush.color() == color) { 0849 return; 0850 } 0851 brush = color; 0852 Q_EMIT fillColorChanged(); 0853 m_document->emitRepaintNeededUnlessEmpty(temp->renderRect()); 0854 } 0855 0856 QFont SelectedItemWrapper::font() const 0857 { 0858 auto &temp = m_document->m_tempItem; 0859 if (!m_options.testFlag(AnnotationTool::FontOption) || !temp) { 0860 return {}; 0861 } 0862 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0863 return text->font; 0864 } 0865 0866 void SelectedItemWrapper::setFont(const QFont &font) 0867 { 0868 auto &temp = m_document->m_tempItem; 0869 if (!m_options.testFlag(AnnotationTool::FontOption) || !temp) { 0870 return; 0871 } 0872 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0873 if (text->font == font) { 0874 return; 0875 } 0876 auto oldRect = temp->renderRect(); 0877 text->font = font; 0878 Traits::reInitTraits(temp->traits()); 0879 const auto &newRect = temp->renderRect(); 0880 Q_EMIT fontChanged(); 0881 if (oldRect != newRect) { 0882 Q_EMIT mousePathChanged(); 0883 } 0884 m_document->emitRepaintNeededUnlessEmpty(oldRect | newRect); 0885 } 0886 0887 QColor SelectedItemWrapper::fontColor() const 0888 { 0889 auto &temp = m_document->m_tempItem; 0890 if (!m_options.testFlag(AnnotationTool::FontOption) || !temp) { 0891 return {}; 0892 } 0893 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0894 return text->brush.color(); 0895 } 0896 0897 void SelectedItemWrapper::setFontColor(const QColor &color) 0898 { 0899 auto &temp = m_document->m_tempItem; 0900 if (!m_options.testFlag(AnnotationTool::FontOption) || !temp) { 0901 return; 0902 } 0903 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0904 if (text->brush.color() == color) { 0905 return; 0906 } 0907 text->brush = color; 0908 Q_EMIT fontColorChanged(); 0909 m_document->emitRepaintNeededUnlessEmpty(temp->renderRect()); 0910 } 0911 0912 int SelectedItemWrapper::number() const 0913 { 0914 auto &temp = m_document->m_tempItem; 0915 if (!m_options.testFlag(AnnotationTool::NumberOption) || !temp) { 0916 return 0; 0917 } 0918 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0919 const auto *number = std::get_if<Traits::Text::Number>(&text.value()); 0920 return number ? *number : 0; 0921 } 0922 0923 void SelectedItemWrapper::setNumber(int number) 0924 { 0925 auto &temp = m_document->m_tempItem; 0926 if (!m_options.testFlag(AnnotationTool::NumberOption) || !temp) { 0927 return; 0928 } 0929 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0930 const auto *oldNumber = std::get_if<Traits::Text::Number>(&text.value()); 0931 if (!oldNumber || *oldNumber == number) { 0932 return; 0933 } 0934 auto oldRect = temp->renderRect(); 0935 text.value().emplace<Traits::Text::Number>(number); 0936 Traits::reInitTraits(temp->traits()); 0937 const auto &newRect = temp->renderRect(); 0938 Q_EMIT numberChanged(); 0939 if (oldRect != newRect) { 0940 Q_EMIT mousePathChanged(); 0941 } 0942 m_document->emitRepaintNeededUnlessEmpty(oldRect | newRect); 0943 } 0944 0945 QString SelectedItemWrapper::text() const 0946 { 0947 auto &temp = m_document->m_tempItem; 0948 if (!m_options.testFlag(AnnotationTool::TextOption) || !temp) { 0949 return {}; 0950 } 0951 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0952 const auto *string = std::get_if<Traits::Text::String>(&text.value()); 0953 return string ? *string : QString{}; 0954 } 0955 0956 void SelectedItemWrapper::setText(const QString &string) 0957 { 0958 auto &temp = m_document->m_tempItem; 0959 if (!m_options.testFlag(AnnotationTool::TextOption) || !temp) { 0960 return; 0961 } 0962 auto &text = std::get<Traits::Text::Opt>(temp->traits()); 0963 const auto *oldString = std::get_if<Traits::Text::String>(&text.value()); 0964 if (!oldString || *oldString == string) { 0965 return; 0966 } 0967 auto oldRect = temp->renderRect(); 0968 text.value().emplace<Traits::Text::String>(string); 0969 Traits::reInitTraits(temp->traits()); 0970 const auto &newRect = temp->renderRect(); 0971 Q_EMIT textChanged(); 0972 if (oldRect != newRect) { 0973 Q_EMIT mousePathChanged(); 0974 } 0975 m_document->emitRepaintNeededUnlessEmpty(oldRect | newRect); 0976 } 0977 0978 bool SelectedItemWrapper::hasShadow() const 0979 { 0980 auto &temp = m_document->m_tempItem; 0981 if (!m_options.testFlag(AnnotationTool::ShadowOption) || !temp) { 0982 return false; 0983 } 0984 auto &shadow = std::get<Traits::Shadow::Opt>(temp->traits()); 0985 return shadow ? shadow->enabled : false; 0986 } 0987 0988 void SelectedItemWrapper::setShadow(bool enabled) 0989 { 0990 auto &temp = m_document->m_tempItem; 0991 if (!m_options.testFlag(AnnotationTool::ShadowOption) || !temp) { 0992 return; 0993 } 0994 auto &shadow = std::get<Traits::Shadow::Opt>(temp->traits()); 0995 if (shadow->enabled == enabled) { 0996 return; 0997 } 0998 auto oldRect = temp->renderRect(); 0999 shadow->enabled = enabled; 1000 Traits::reInitTraits(temp->traits()); 1001 Q_EMIT shadowChanged(); 1002 m_document->emitRepaintNeededUnlessEmpty(oldRect | temp->renderRect()); 1003 } 1004 1005 QPainterPath SelectedItemWrapper::mousePath() const 1006 { 1007 auto &temp = m_document->m_tempItem; 1008 if (!hasSelection()) { 1009 return {}; 1010 } 1011 return Traits::mousePath(temp->traits()); 1012 } 1013 1014 QDebug operator<<(QDebug debug, const SelectedItemWrapper *wrapper) 1015 { 1016 QDebugStateSaver stateSaver(debug); 1017 debug.nospace(); 1018 debug << "SelectedItemWrapper("; 1019 if (!wrapper) { 1020 return debug << "0x0)"; 1021 } 1022 debug << (const void *)wrapper; 1023 debug << ",\n selectedItem=" << wrapper->selectedItem().lock().get(); 1024 debug << ')'; 1025 return debug; 1026 } 1027 1028 #include <moc_AnnotationDocument.cpp>