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 &currentItem = 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 &currentItem = 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>