File indexing completed on 2024-04-28 15:51:50

0001 /*
0002     SPDX-FileCopyrightText: 2005 Enrico Ros <eros.kde@email.it>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "pageviewannotator.h"
0008 
0009 // qt / kde includes
0010 #include <KLocalizedString>
0011 #include <KMessageBox>
0012 #include <QApplication>
0013 #include <QColor>
0014 #include <QEvent>
0015 #include <QFile>
0016 #include <QFileDialog>
0017 #include <QInputDialog>
0018 #include <QList>
0019 #include <QLoggingCategory>
0020 #include <QPainter>
0021 #include <QSet>
0022 #include <QVariant>
0023 
0024 #include <KUser>
0025 #include <QDebug>
0026 #include <QMenu>
0027 
0028 // system includes
0029 #include <QKeyEvent>
0030 #include <QStandardPaths>
0031 #include <QTabletEvent>
0032 #include <math.h>
0033 #include <memory>
0034 
0035 // local includes
0036 #include "annotationactionhandler.h"
0037 #include "core/annotations.h"
0038 #include "core/area.h"
0039 #include "core/document.h"
0040 #include "core/page.h"
0041 #include "core/signatureutils.h"
0042 #include "editannottooldialog.h"
0043 #include "gui/debug_ui.h"
0044 #include "gui/guiutils.h"
0045 #include "pageview.h"
0046 #include "settings.h"
0047 #include "signaturepartutils.h"
0048 
0049 /** @short PickPointEngine */
0050 class PickPointEngine : public AnnotatorEngine
0051 {
0052 public:
0053     explicit PickPointEngine(const QDomElement &engineElement)
0054         : AnnotatorEngine(engineElement)
0055         , clicked(false)
0056         , xscale(1.0)
0057         , yscale(1.0)
0058     {
0059         // parse engine specific attributes
0060         hoverIconName = engineElement.attribute(QStringLiteral("hoverIcon"));
0061         iconName = m_annotElement.attribute(QStringLiteral("icon"));
0062         if (m_annotElement.attribute(QStringLiteral("type")) == QLatin1String("Stamp") && !iconName.simplified().isEmpty()) {
0063             hoverIconName = iconName;
0064         }
0065         center = QVariant(engineElement.attribute(QStringLiteral("center"))).toBool();
0066         bool ok = true;
0067         size = engineElement.attribute(QStringLiteral("size"), QStringLiteral("32")).toInt(&ok);
0068         if (!ok) {
0069             size = 32;
0070         }
0071         m_block = QVariant(engineElement.attribute(QStringLiteral("block"))).toBool();
0072 
0073         // create engine objects
0074         if (!hoverIconName.simplified().isEmpty()) {
0075             pixmap = Okular::AnnotationUtils::loadStamp(hoverIconName, size);
0076         }
0077     }
0078 
0079     QRect event(EventType type, Button button, Modifiers modifiers, double nX, double nY, double xScale, double yScale, const Okular::Page *page) override
0080     {
0081         xscale = xScale;
0082         yscale = yScale;
0083         pagewidth = page->width();
0084         pageheight = page->height();
0085         // only proceed if pressing left button
0086         if (button != Left) {
0087             return QRect();
0088         }
0089 
0090         // start operation on click
0091         if (type == Press && clicked == false) {
0092             clicked = true;
0093             startpoint.x = nX;
0094             startpoint.y = nY;
0095         }
0096         // repaint if moving while pressing
0097         else if (type == Move && clicked == true) {
0098         }
0099         // operation finished on release
0100         else if (type == Release && clicked == true) {
0101             m_creationCompleted = true;
0102         } else {
0103             return QRect();
0104         }
0105 
0106         // Constrain to 1:1 form factor (e.g. circle or square)
0107         if (modifiers.constrainRatioAndAngle) {
0108             double side = qMin(qAbs(nX - startpoint.x) * xScale, qAbs(nY - startpoint.y) * yScale);
0109             nX = qBound(startpoint.x - side / xScale, nX, startpoint.x + side / xScale);
0110             nY = qBound(startpoint.y - side / yScale, nY, startpoint.y + side / yScale);
0111         }
0112         // update variables and extents (zoom invariant rect)
0113         point.x = nX;
0114         point.y = nY;
0115         if (center) {
0116             rect.left = nX - (size / (xScale * 2.0));
0117             rect.top = nY - (size / (yScale * 2.0));
0118         } else {
0119             rect.left = nX;
0120             rect.top = nY;
0121         }
0122         rect.right = rect.left + size;
0123         rect.bottom = rect.top + size;
0124         QRect boundrect = rect.geometry((int)xScale, (int)yScale).adjusted(0, 0, 1, 1);
0125         if (m_block) {
0126             const Okular::NormalizedRect tmprect(qMin(startpoint.x, point.x), qMin(startpoint.y, point.y), qMax(startpoint.x, point.x), qMax(startpoint.y, point.y));
0127             boundrect |= tmprect.geometry((int)xScale, (int)yScale).adjusted(0, 0, 1, 1);
0128         }
0129         return boundrect;
0130     }
0131 
0132     void paint(QPainter *painter, double xScale, double yScale, const QRect & /*clipRect*/) override
0133     {
0134         if (clicked) {
0135             if (m_block) {
0136                 const QPen origpen = painter->pen();
0137                 QPen pen = painter->pen();
0138                 pen.setStyle(Qt::DashLine);
0139                 painter->setPen(pen);
0140                 const Okular::NormalizedRect tmprect(qMin(startpoint.x, point.x), qMin(startpoint.y, point.y), qMax(startpoint.x, point.x), qMax(startpoint.y, point.y));
0141                 const QRect realrect = tmprect.geometry((int)xScale, (int)yScale);
0142                 painter->drawRect(realrect);
0143                 painter->setPen(origpen);
0144             }
0145             if (!pixmap.isNull()) {
0146                 painter->drawPixmap(QPointF(rect.left * xScale, rect.top * yScale), pixmap);
0147             }
0148         }
0149     }
0150 
0151     void addInPlaceTextAnnotation(Okular::Annotation *&ann, const QString &summary, const QString &content, Okular::TextAnnotation::InplaceIntent inplaceIntent)
0152     {
0153         Okular::TextAnnotation *ta = new Okular::TextAnnotation();
0154         ann = ta;
0155         ta->setFlags(ta->flags() | Okular::Annotation::FixedRotation);
0156         ta->setContents(content);
0157         ta->setTextType(Okular::TextAnnotation::InPlace);
0158         ta->setInplaceIntent(inplaceIntent);
0159         // set alignment
0160         if (m_annotElement.hasAttribute(QStringLiteral("align"))) {
0161             ta->setInplaceAlignment(m_annotElement.attribute(QStringLiteral("align")).toInt());
0162         }
0163         // set font
0164         if (m_annotElement.hasAttribute(QStringLiteral("font"))) {
0165             QFont f;
0166             // Workaround broken old code that saved fonts incorrectly with extra backslashes
0167             QString fontString = m_annotElement.attribute(QStringLiteral("font"));
0168             if (fontString.count(QStringLiteral("\\\\,")) > 9) {
0169                 fontString.replace(QStringLiteral("\\\\,"), QStringLiteral(","));
0170             }
0171             f.fromString(fontString);
0172             ta->setTextFont(f);
0173         }
0174         // set font color
0175         if (m_annotElement.hasAttribute(QStringLiteral("textColor"))) {
0176             if (inplaceIntent == Okular::TextAnnotation::TypeWriter) {
0177                 ta->setTextColor(m_annotElement.attribute(QStringLiteral("textColor")));
0178             } else {
0179                 ta->setTextColor(Qt::black);
0180             }
0181         }
0182         // set width
0183         if (m_annotElement.hasAttribute(QStringLiteral("width"))) {
0184             ta->style().setWidth(m_annotElement.attribute(QStringLiteral("width")).toDouble());
0185         }
0186         // set boundary
0187         rect.left = qMin(startpoint.x, point.x);
0188         rect.top = qMin(startpoint.y, point.y);
0189         rect.right = qMax(startpoint.x, point.x);
0190         rect.bottom = qMax(startpoint.y, point.y);
0191         qCDebug(OkularUiDebug).nospace() << "xyScale=" << xscale << "," << yscale;
0192         static const int padding = 2;
0193         const QFontMetricsF mf(ta->textFont());
0194         const QRectF rcf =
0195             mf.boundingRect(Okular::NormalizedRect(rect.left, rect.top, 1.0, 1.0).geometry((int)pagewidth, (int)pageheight).adjusted(padding, padding, -padding, -padding), Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap, ta->contents());
0196         rect.right = qMax(rect.right, rect.left + (rcf.width() + padding * 2) / pagewidth);
0197         rect.bottom = qMax(rect.bottom, rect.top + (rcf.height() + padding * 2) / pageheight);
0198         ta->window().setSummary(summary);
0199     }
0200 
0201     QList<Okular::Annotation *> end() override
0202     {
0203         // find out annotation's description node
0204         if (m_annotElement.isNull()) {
0205             m_creationCompleted = false;
0206             clicked = false;
0207             return QList<Okular::Annotation *>();
0208         }
0209 
0210         // find out annotation's type
0211         Okular::Annotation *ann = nullptr;
0212         const QString typeString = m_annotElement.attribute(QStringLiteral("type"));
0213         // create InPlace TextAnnotation from path
0214         if (typeString == QLatin1String("FreeText")) {
0215             addInPlaceTextAnnotation(ann, i18n("Inline Note"), QString(), Okular::TextAnnotation::Unknown);
0216         } else if (typeString == QLatin1String("Typewriter")) {
0217             bool resok;
0218             const QString content = QInputDialog::getMultiLineText(nullptr, i18n("New Text Note"), i18n("Text of the new note:"), QString(), &resok);
0219             if (resok) {
0220                 addInPlaceTextAnnotation(ann, i18n("Typewriter"), content, Okular::TextAnnotation::TypeWriter);
0221             }
0222         } else if (typeString == QLatin1String("Text")) {
0223             Okular::TextAnnotation *ta = new Okular::TextAnnotation();
0224             ann = ta;
0225             ta->setTextType(Okular::TextAnnotation::Linked);
0226             ta->setTextIcon(iconName);
0227             // ta->window.flags &= ~(Okular::Annotation::Hidden);
0228             const double iconhei = 0.03;
0229             rect.left = point.x;
0230             rect.top = point.y;
0231             rect.right = rect.left + iconhei;
0232             rect.bottom = rect.top + iconhei * xscale / yscale;
0233             ta->window().setSummary(i18n("Pop-up Note"));
0234         }
0235         // create StampAnnotation from path
0236         else if (typeString == QLatin1String("Stamp")) {
0237             Okular::StampAnnotation *sa = new Okular::StampAnnotation();
0238             ann = sa;
0239             sa->setStampIconName(iconName);
0240             // set boundary
0241             rect.left = qMin(startpoint.x, point.x);
0242             rect.top = qMin(startpoint.y, point.y);
0243             rect.right = qMax(startpoint.x, point.x);
0244             rect.bottom = qMax(startpoint.y, point.y);
0245             const QRectF rcf = rect.geometry((int)xscale, (int)yscale);
0246             const int ml = (rcf.bottomRight() - rcf.topLeft()).toPoint().manhattanLength();
0247             if (ml <= QApplication::startDragDistance()) {
0248                 const double stampxscale = pixmap.width() / xscale;
0249                 const double stampyscale = pixmap.height() / yscale;
0250                 if (center) {
0251                     rect.left = point.x - stampxscale / 2;
0252                     rect.top = point.y - stampyscale / 2;
0253                 } else {
0254                     rect.left = point.x;
0255                     rect.top = point.y;
0256                 }
0257                 rect.right = rect.left + stampxscale;
0258                 rect.bottom = rect.top + stampyscale;
0259             }
0260         }
0261         // create GeomAnnotation
0262         else if (typeString == QLatin1String("GeomSquare") || typeString == QLatin1String("GeomCircle")) {
0263             Okular::GeomAnnotation *ga = new Okular::GeomAnnotation();
0264             ann = ga;
0265             // set the type
0266             if (typeString == QLatin1String("GeomSquare")) {
0267                 ga->setGeometricalType(Okular::GeomAnnotation::InscribedSquare);
0268             } else {
0269                 ga->setGeometricalType(Okular::GeomAnnotation::InscribedCircle);
0270             }
0271             if (m_annotElement.hasAttribute(QStringLiteral("width"))) {
0272                 ann->style().setWidth(m_annotElement.attribute(QStringLiteral("width")).toDouble());
0273             }
0274             if (m_annotElement.hasAttribute(QStringLiteral("innerColor"))) {
0275                 ga->setGeometricalInnerColor(QColor(m_annotElement.attribute(QStringLiteral("innerColor"))));
0276             }
0277             // set boundary
0278             rect.left = qMin(startpoint.x, point.x);
0279             rect.top = qMin(startpoint.y, point.y);
0280             rect.right = qMax(startpoint.x, point.x);
0281             rect.bottom = qMax(startpoint.y, point.y);
0282         }
0283 
0284         m_creationCompleted = false;
0285         clicked = false;
0286 
0287         // safety check
0288         if (!ann) {
0289             return QList<Okular::Annotation *>();
0290         }
0291 
0292         // set common attributes
0293         ann->style().setColor(m_annotElement.hasAttribute(QStringLiteral("color")) ? m_annotElement.attribute(QStringLiteral("color")) : m_engineColor);
0294         if (m_annotElement.hasAttribute(QStringLiteral("opacity"))) {
0295             ann->style().setOpacity(m_annotElement.attribute(QStringLiteral("opacity"), QStringLiteral("1.0")).toDouble());
0296         }
0297 
0298         // set the bounding rectangle, and make sure that the newly created
0299         // annotation lies within the page by translating it if necessary
0300         if (rect.right > 1) {
0301             rect.left -= rect.right - 1;
0302             rect.right = 1;
0303         }
0304         if (rect.bottom > 1) {
0305             rect.top -= rect.bottom - 1;
0306             rect.bottom = 1;
0307         }
0308         ann->setBoundingRectangle(rect);
0309 
0310         // return annotation
0311         return QList<Okular::Annotation *>() << ann;
0312     }
0313 
0314 protected:
0315     bool clicked;
0316     bool m_block;
0317     double xscale, yscale;
0318     Okular::NormalizedRect rect;
0319     Okular::NormalizedPoint startpoint;
0320     Okular::NormalizedPoint point;
0321 
0322 private:
0323     QPixmap pixmap;
0324     QString hoverIconName, iconName;
0325     int size;
0326     double pagewidth, pageheight;
0327     bool center;
0328 };
0329 
0330 class PickPointEngineSignature : public PickPointEngine
0331 {
0332 public:
0333     PickPointEngineSignature(Okular::Document *document, PageView *pageView)
0334         : PickPointEngine({})
0335         , m_document(document)
0336         , m_page(nullptr)
0337         , m_pageView(pageView)
0338         , m_startOver(false)
0339         , m_aborted(false)
0340     {
0341         m_block = true;
0342     }
0343 
0344     QRect event(EventType type, Button button, Modifiers modifiers, double nX, double nY, double xScale, double yScale, const Okular::Page *page) override
0345     {
0346         m_page = page;
0347         return PickPointEngine::event(type, button, modifiers, nX, nY, xScale, yScale, page);
0348     }
0349 
0350     QList<Okular::Annotation *> end() override
0351     {
0352         m_startOver = false;
0353         rect.left = qMin(startpoint.x, point.x);
0354         rect.top = qMin(startpoint.y, point.y);
0355         rect.right = qMax(startpoint.x, point.x);
0356         rect.bottom = qMax(startpoint.y, point.y);
0357 
0358         // FIXME this is a bit arbitrary, try to figure out a better rule, potentially based in cm and not pixels?
0359         if (rect.width() * m_page->width() < 100 || rect.height() * m_page->height() < 100) {
0360             const KMessageBox::ButtonCode answer = KMessageBox::questionTwoActions(
0361                 m_pageView,
0362                 xi18nc("@info", "A signature of this size may be too small to read. If you would like to create a potentially more readable signature, press <interface>Start over</interface> and draw a bigger rectangle."),
0363                 QString(),
0364                 KGuiItem(i18nc("@action:button", "Start Over")),
0365                 KGuiItem(i18nc("@action:button", "Sign")),
0366                 QStringLiteral("TooSmallDigitalSignatureQuestion"));
0367             if (answer == KMessageBox::PrimaryAction) {
0368                 m_startOver = true;
0369                 return {};
0370             }
0371         }
0372 
0373         const auto signInfo = SignaturePartUtils::getCertificateAndPasswordForSigning(m_pageView, m_document, SignaturePartUtils::SigningInformationOption::BackgroundImage);
0374         if (!signInfo) {
0375             m_aborted = true;
0376             passToUse.clear();
0377             documentPassword.clear();
0378         } else {
0379             certNicknameToUse = signInfo->certificate->nickName();
0380             certCommonName = signInfo->certificate->subjectInfo(Okular::CertificateInfo::CommonName, Okular::CertificateInfo::EmptyString::TranslatedNotAvailable);
0381             passToUse = signInfo->certificatePassword;
0382             documentPassword = signInfo->documentPassword;
0383             reason = signInfo->reason;
0384             location = signInfo->location;
0385             backgroundImagePath = signInfo->backgroundImagePath;
0386         }
0387 
0388         m_creationCompleted = false;
0389         clicked = false;
0390 
0391         return {};
0392     }
0393 
0394     bool isAccepted() const
0395     {
0396         return !m_aborted && !certNicknameToUse.isEmpty();
0397     }
0398 
0399     bool userWantsToStartOver() const
0400     {
0401         return m_startOver;
0402     }
0403 
0404     bool isAborted() const
0405     {
0406         return m_aborted;
0407     }
0408 
0409     bool sign(const QString &newFilePath)
0410     {
0411         Okular::NewSignatureData data;
0412         data.setCertNickname(certNicknameToUse);
0413         data.setCertSubjectCommonName(certCommonName);
0414         data.setPassword(passToUse);
0415         data.setDocumentPassword(documentPassword);
0416         data.setPage(m_page->number());
0417         data.setBoundingRectangle(rect);
0418         data.setReason(reason);
0419         data.setLocation(location);
0420         data.setBackgroundImagePath(backgroundImagePath);
0421         passToUse.clear();
0422         documentPassword.clear();
0423         return m_document->sign(data, newFilePath);
0424     }
0425 
0426 private:
0427     QString certNicknameToUse;
0428     QString certCommonName;
0429     QString passToUse;
0430     QString documentPassword;
0431     QString location;
0432     QString backgroundImagePath;
0433     QString reason;
0434 
0435     Okular::Document *m_document;
0436     const Okular::Page *m_page;
0437     PageView *m_pageView;
0438 
0439     bool m_startOver;
0440     bool m_aborted;
0441 };
0442 
0443 /** @short PolyLineEngine */
0444 class PolyLineEngine : public AnnotatorEngine
0445 {
0446 public:
0447     explicit PolyLineEngine(const QDomElement &engineElement)
0448         : AnnotatorEngine(engineElement)
0449         , last(false)
0450     {
0451         // parse engine specific attributes
0452         m_block = engineElement.attribute(QStringLiteral("block")) == QLatin1String("true");
0453         bool ok = true;
0454         // numofpoints represents the max number of points for the current
0455         // polygon/polyline, with a pair of exceptions:
0456         // -1 means: the polyline must close on the first point (polygon)
0457         // 0 means: construct as many points as you want, right-click
0458         //   to construct the last point
0459         numofpoints = engineElement.attribute(QStringLiteral("points")).toInt(&ok);
0460         if (!ok) {
0461             numofpoints = -1;
0462         }
0463     }
0464 
0465     static Okular::NormalizedPoint constrainAngle(const Okular::NormalizedPoint &p1, double x, double y, double xScale, double yScale, double angleIncrement)
0466     {
0467         // given the normalized point (x, y), return the closest point such that the line segment from p1 forms an angle
0468         // with the horizontal axis which is an integer multiple of angleIncrement on a reference area of size xScale x yScale
0469         double dist = sqrt(p1.distanceSqr(x, y, xScale, yScale));
0470         double angle = atan2((y - p1.y) * yScale, (x - p1.x) * xScale);
0471         double constrainedAngle = round(angle / angleIncrement) * angleIncrement;
0472         double offset = dist * sin(angle - constrainedAngle);
0473         x += offset * sin(constrainedAngle) / xScale;
0474         y -= offset * cos(constrainedAngle) / yScale;
0475         return Okular::NormalizedPoint(x, y);
0476     }
0477 
0478     QRect event(EventType type, Button button, Modifiers modifiers, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/) override
0479     {
0480         // only proceed if pressing left button
0481         //            if ( button != Left )
0482         //                return rect;
0483 
0484         // Constrain to 15° steps, except first point of course.
0485         if (modifiers.constrainRatioAndAngle && !points.isEmpty()) {
0486             const Okular::NormalizedPoint constrainedPoint = constrainAngle(points.constLast(), nX, nY, xScale, yScale, M_PI / 12.);
0487             nX = constrainedPoint.x;
0488             nY = constrainedPoint.y;
0489         }
0490         // process button press
0491         if (type == Press) {
0492             newPoint.x = nX;
0493             newPoint.y = nY;
0494             if (button == Right) {
0495                 last = true;
0496             }
0497         }
0498         // move the second point
0499         else if (type == Move) {
0500             movingpoint.x = nX;
0501             movingpoint.y = nY;
0502             const QRect oldmovingrect = movingrect;
0503             movingrect = rect | QRect((int)(movingpoint.x * xScale), (int)(movingpoint.y * yScale), 1, 1);
0504             return oldmovingrect | movingrect;
0505         } else if (type == Release) {
0506             const Okular::NormalizedPoint tmppoint(nX, nY);
0507             if (fabs(tmppoint.x - newPoint.x) + fabs(tmppoint.y - newPoint.y) > 1e-2) {
0508                 return rect;
0509             }
0510 
0511             if (numofpoints == -1 && points.count() > 1 && (fabs(points[0].x - newPoint.x) + fabs(points[0].y - newPoint.y) < 1e-2)) {
0512                 last = true;
0513             } else {
0514                 points.append(newPoint);
0515                 rect |= QRect((int)(newPoint.x * xScale), (int)(newPoint.y * yScale), 1, 1);
0516             }
0517             // end creation if we have constructed the last point of enough points
0518             if (last || points.count() == numofpoints) {
0519                 m_creationCompleted = true;
0520                 last = false;
0521                 normRect = Okular::NormalizedRect(rect, xScale, yScale);
0522             }
0523         }
0524 
0525         return rect;
0526     }
0527 
0528     void paint(QPainter *painter, double xScale, double yScale, const QRect & /*clipRect*/) override
0529     {
0530         if (points.count() < 1) {
0531             return;
0532         }
0533 
0534         if (m_block && points.count() == 2) {
0535             const Okular::NormalizedPoint first = points[0];
0536             const Okular::NormalizedPoint second = points[1];
0537             // draw a semitransparent block around the 2 points
0538             painter->setPen(m_engineColor);
0539             painter->setBrush(QBrush(m_engineColor.lighter(), Qt::Dense4Pattern));
0540             painter->drawRect((int)(first.x * (double)xScale), (int)(first.y * (double)yScale), (int)((second.x - first.x) * (double)xScale), (int)((second.y - first.y) * (double)yScale));
0541         } else {
0542             // draw a polyline that connects the constructed points
0543             painter->setPen(QPen(m_engineColor, 2));
0544             for (int i = 1; i < points.count(); ++i) {
0545                 painter->drawLine((int)(points[i - 1].x * (double)xScale), (int)(points[i - 1].y * (double)yScale), (int)(points[i].x * (double)xScale), (int)(points[i].y * (double)yScale));
0546             }
0547             painter->drawLine((int)(points.last().x * (double)xScale), (int)(points.last().y * (double)yScale), (int)(movingpoint.x * (double)xScale), (int)(movingpoint.y * (double)yScale));
0548         }
0549     }
0550 
0551     QList<Okular::Annotation *> end() override
0552     {
0553         m_creationCompleted = false;
0554 
0555         // find out annotation's description node
0556         if (m_annotElement.isNull()) {
0557             return QList<Okular::Annotation *>();
0558         }
0559 
0560         // find out annotation's type
0561         Okular::Annotation *ann = nullptr;
0562         const QString typeString = m_annotElement.attribute(QStringLiteral("type"));
0563 
0564         // create LineAnnotation from path
0565         if (typeString == QLatin1String("Line") || typeString == QLatin1String("Polyline") || typeString == QLatin1String("Polygon")) {
0566             if (points.count() < 2) {
0567                 return QList<Okular::Annotation *>();
0568             }
0569 
0570             // add note
0571             Okular::LineAnnotation *la = new Okular::LineAnnotation();
0572             ann = la;
0573 
0574             la->setLinePoints(points);
0575 
0576             if (numofpoints == -1) {
0577                 la->setLineClosed(true);
0578                 if (m_annotElement.hasAttribute(QStringLiteral("innerColor"))) {
0579                     la->setLineInnerColor(QColor(m_annotElement.attribute(QStringLiteral("innerColor"))));
0580                 }
0581             } else if (numofpoints == 2) {
0582                 if (m_annotElement.hasAttribute(QStringLiteral("leadFwd"))) {
0583                     la->setLineLeadingForwardPoint(m_annotElement.attribute(QStringLiteral("leadFwd")).toDouble());
0584                 }
0585                 if (m_annotElement.hasAttribute(QStringLiteral("leadBack"))) {
0586                     la->setLineLeadingBackwardPoint(m_annotElement.attribute(QStringLiteral("leadBack")).toDouble());
0587                 }
0588             }
0589             if (m_annotElement.hasAttribute(QStringLiteral("startStyle"))) {
0590                 la->setLineStartStyle((Okular::LineAnnotation::TermStyle)m_annotElement.attribute(QStringLiteral("startStyle")).toInt());
0591             }
0592             if (m_annotElement.hasAttribute(QStringLiteral("endStyle"))) {
0593                 la->setLineEndStyle((Okular::LineAnnotation::TermStyle)m_annotElement.attribute(QStringLiteral("endStyle")).toInt());
0594             }
0595 
0596             la->setBoundingRectangle(normRect);
0597         }
0598 
0599         // safety check
0600         if (!ann) {
0601             return QList<Okular::Annotation *>();
0602         }
0603 
0604         if (m_annotElement.hasAttribute(QStringLiteral("width"))) {
0605             ann->style().setWidth(m_annotElement.attribute(QStringLiteral("width")).toDouble());
0606         }
0607 
0608         // set common attributes
0609         ann->style().setColor(m_annotElement.hasAttribute(QStringLiteral("color")) ? m_annotElement.attribute(QStringLiteral("color")) : m_engineColor);
0610         if (m_annotElement.hasAttribute(QStringLiteral("opacity"))) {
0611             ann->style().setOpacity(m_annotElement.attribute(QStringLiteral("opacity"), QStringLiteral("1.0")).toDouble());
0612         }
0613         // return annotation
0614 
0615         return QList<Okular::Annotation *>() << ann;
0616     }
0617 
0618 private:
0619     QList<Okular::NormalizedPoint> points;
0620     Okular::NormalizedPoint newPoint;
0621     Okular::NormalizedPoint movingpoint;
0622     QRect rect;
0623     QRect movingrect;
0624     Okular::NormalizedRect normRect;
0625     bool m_block;
0626     bool last;
0627     int numofpoints;
0628 };
0629 
0630 /** @short TextSelectorEngine */
0631 class TextSelectorEngine : public AnnotatorEngine
0632 {
0633 public:
0634     TextSelectorEngine(const QDomElement &engineElement, PageView *pageView)
0635         : AnnotatorEngine(engineElement)
0636         , m_pageView(pageView)
0637     {
0638         // parse engine specific attributes
0639     }
0640 
0641     QRect event(EventType type, Button button, Modifiers /*modifiers*/, double nX, double nY, double xScale, double yScale, const Okular::Page * /*page*/) override
0642     {
0643         // only proceed if pressing left button
0644         if (button != Left) {
0645             return QRect();
0646         }
0647 
0648         if (type == Press) {
0649             lastPoint.x = nX;
0650             lastPoint.y = nY;
0651             const QRect oldrect = rect;
0652             rect = QRect();
0653             return oldrect;
0654         } else if (type == Move) {
0655             if (item()) {
0656                 const QPoint start((int)(lastPoint.x * item()->uncroppedWidth()), (int)(lastPoint.y * item()->uncroppedHeight()));
0657                 const QPoint end((int)(nX * item()->uncroppedWidth()), (int)(nY * item()->uncroppedHeight()));
0658                 selection.reset();
0659                 std::unique_ptr<Okular::RegularAreaRect> newselection(m_pageView->textSelectionForItem(item(), start, end));
0660                 if (newselection && !newselection->isEmpty()) {
0661                     const QList<QRect> geom = newselection->geometry((int)xScale, (int)yScale);
0662                     QRect newrect;
0663                     for (const QRect &r : geom) {
0664                         if (newrect.isNull()) {
0665                             newrect = r;
0666                         } else {
0667                             newrect |= r;
0668                         }
0669                     }
0670                     rect |= newrect;
0671                     selection = std::move(newselection);
0672                 }
0673             }
0674         } else if (type == Release) {
0675             m_creationCompleted = true;
0676         }
0677         return rect;
0678     }
0679 
0680     void paint(QPainter *painter, double xScale, double yScale, const QRect & /*clipRect*/) override
0681     {
0682         if (selection) {
0683             painter->setPen(Qt::NoPen);
0684             QColor col = m_engineColor;
0685             col.setAlphaF(0.5);
0686             painter->setBrush(col);
0687             for (const Okular::NormalizedRect &r : std::as_const(*selection)) {
0688                 painter->drawRect(r.geometry((int)xScale, (int)yScale));
0689             }
0690         }
0691     }
0692 
0693     QList<Okular::Annotation *> end() override
0694     {
0695         m_creationCompleted = false;
0696 
0697         // safety checks
0698         if (m_annotElement.isNull() || !selection) {
0699             return QList<Okular::Annotation *>();
0700         }
0701 
0702         // find out annotation's type
0703         Okular::Annotation *ann = nullptr;
0704         const QString typeString = m_annotElement.attribute(QStringLiteral("type"));
0705 
0706         Okular::HighlightAnnotation::HighlightType type = Okular::HighlightAnnotation::Highlight;
0707         bool typevalid = false;
0708         // create HighlightAnnotation's from the selected area
0709         if (typeString == QLatin1String("Highlight")) {
0710             type = Okular::HighlightAnnotation::Highlight;
0711             typevalid = true;
0712         } else if (typeString == QLatin1String("Squiggly")) {
0713             type = Okular::HighlightAnnotation::Squiggly;
0714             typevalid = true;
0715         } else if (typeString == QLatin1String("Underline")) {
0716             type = Okular::HighlightAnnotation::Underline;
0717             typevalid = true;
0718         } else if (typeString == QLatin1String("StrikeOut")) {
0719             type = Okular::HighlightAnnotation::StrikeOut;
0720             typevalid = true;
0721         }
0722         if (typevalid) {
0723             Okular::HighlightAnnotation *ha = new Okular::HighlightAnnotation();
0724             ha->setHighlightType(type);
0725             ha->setBoundingRectangle(Okular::NormalizedRect(rect, item()->uncroppedWidth(), item()->uncroppedHeight()));
0726             for (const Okular::NormalizedRect &r : std::as_const(*selection)) {
0727                 Okular::HighlightAnnotation::Quad q;
0728                 q.setCapStart(false);
0729                 q.setCapEnd(false);
0730                 q.setFeather(1.0);
0731                 q.setPoint(Okular::NormalizedPoint(r.left, r.bottom), 0);
0732                 q.setPoint(Okular::NormalizedPoint(r.right, r.bottom), 1);
0733                 q.setPoint(Okular::NormalizedPoint(r.right, r.top), 2);
0734                 q.setPoint(Okular::NormalizedPoint(r.left, r.top), 3);
0735                 ha->highlightQuads().append(q);
0736             }
0737             ann = ha;
0738         }
0739 
0740         selection.reset();
0741 
0742         // safety check
0743         if (!ann) {
0744             return QList<Okular::Annotation *>();
0745         }
0746 
0747         // set common attributes
0748         ann->style().setColor(m_annotElement.hasAttribute(QStringLiteral("color")) ? m_annotElement.attribute(QStringLiteral("color")) : m_engineColor);
0749         if (m_annotElement.hasAttribute(QStringLiteral("opacity"))) {
0750             ann->style().setOpacity(m_annotElement.attribute(QStringLiteral("opacity"), QStringLiteral("1.0")).toDouble());
0751         }
0752 
0753         // return annotations
0754         return QList<Okular::Annotation *>() << ann;
0755     }
0756 
0757     QCursor cursor() const override
0758     {
0759         return Qt::IBeamCursor;
0760     }
0761 
0762 private:
0763     // data
0764     PageView *m_pageView;
0765     // TODO: support more pages
0766     std::unique_ptr<Okular::RegularAreaRect> selection;
0767     Okular::NormalizedPoint lastPoint;
0768     QRect rect;
0769 };
0770 
0771 /** @short AnnotationTools*/
0772 class AnnotationTools
0773 {
0774 public:
0775     AnnotationTools()
0776         : m_toolsCount(0)
0777     {
0778     }
0779 
0780     void setTools(const QStringList &tools)
0781     {
0782         // Populate m_toolsDefinition
0783         m_toolsCount = 0;
0784         m_toolsDefinition.clear();
0785         QDomElement root = m_toolsDefinition.createElement(QStringLiteral("root"));
0786         m_toolsDefinition.appendChild(root);
0787         for (const QString &toolXml : tools) {
0788             QDomDocument entryParser;
0789             if (entryParser.setContent(toolXml)) {
0790                 root.appendChild(m_toolsDefinition.importNode(entryParser.documentElement(), true));
0791                 m_toolsCount++;
0792             } else {
0793                 qCWarning(OkularUiDebug) << "Skipping malformed tool XML in AnnotationTools setting";
0794             }
0795         }
0796     }
0797 
0798     QStringList toStringList()
0799     {
0800         QStringList tools;
0801         QDomElement toolElement = m_toolsDefinition.documentElement().firstChildElement();
0802         QString str;
0803         QTextStream stream(&str);
0804         while (!toolElement.isNull()) {
0805             str.clear();
0806             toolElement.save(stream, -1 /* indent disabled */);
0807             tools << str;
0808             toolElement = toolElement.nextSiblingElement();
0809         }
0810         return tools;
0811     }
0812 
0813     QDomElement tool(int toolId)
0814     {
0815         QDomElement toolElement = m_toolsDefinition.documentElement().firstChildElement();
0816         while (!toolElement.isNull() && toolElement.attribute(QStringLiteral("id")).toInt() != toolId) {
0817             toolElement = toolElement.nextSiblingElement();
0818         }
0819         return toolElement; // can return a null element
0820     }
0821 
0822     void appendTool(QDomElement toolElement)
0823     {
0824         toolElement = toolElement.cloneNode().toElement();
0825         toolElement.setAttribute(QStringLiteral("id"), ++m_toolsCount);
0826         m_toolsDefinition.documentElement().appendChild(toolElement);
0827     }
0828 
0829     bool updateTool(QDomElement newToolElement, int toolId)
0830     {
0831         QDomElement toolElement = tool(toolId);
0832         if (toolElement.isNull()) {
0833             return false;
0834         }
0835         newToolElement = newToolElement.cloneNode().toElement();
0836         newToolElement.setAttribute(QStringLiteral("id"), toolId);
0837         QDomNode oldTool = m_toolsDefinition.documentElement().replaceChild(newToolElement, toolElement);
0838         return !oldTool.isNull();
0839     }
0840 
0841     int findToolId(const QString &type)
0842     {
0843         int toolId = -1;
0844         if (type.isEmpty()) {
0845             return -1;
0846         }
0847         // FIXME: search from left. currently searching from right side as a workaround to avoid matching
0848         // straight line tools to the arrow tool, which is also of type straight-line
0849         QDomElement toolElement = m_toolsDefinition.documentElement().lastChildElement();
0850         while (!toolElement.isNull() && toolElement.attribute(QStringLiteral("type")) != type) {
0851             toolElement = toolElement.previousSiblingElement();
0852         }
0853         if (!toolElement.isNull() && toolElement.hasAttribute(QStringLiteral("id"))) {
0854             bool ok;
0855             toolId = toolElement.attribute(QStringLiteral("id")).toInt(&ok);
0856             if (!ok) {
0857                 return -1;
0858             }
0859         }
0860         return toolId;
0861     }
0862 
0863 private:
0864     QDomDocument m_toolsDefinition;
0865     int m_toolsCount;
0866 };
0867 
0868 /** PageViewAnnotator **/
0869 const int PageViewAnnotator::STAMP_TOOL_ID = 14;
0870 
0871 PageViewAnnotator::PageViewAnnotator(PageView *parent, Okular::Document *storage)
0872     : QObject(parent)
0873     , m_document(storage)
0874     , m_pageView(parent)
0875     , m_actionHandler(nullptr)
0876     , m_engine(nullptr)
0877     , m_builtinToolsDefinition(nullptr)
0878     , m_quickToolsDefinition(nullptr)
0879     , m_continuousMode(true)
0880     , m_constrainRatioAndAngle(false)
0881     , m_signatureMode(false)
0882     , m_lastToolsDefinition(nullptr)
0883     , m_lastToolId(-1)
0884     , m_lockedItem(nullptr)
0885 {
0886     reparseConfig();
0887     reparseBuiltinToolsConfig();
0888     reparseQuickToolsConfig();
0889     connect(Okular::Settings::self(), &Okular::Settings::builtinAnnotationToolsChanged, this, &PageViewAnnotator::reparseBuiltinToolsConfig);
0890     connect(Okular::Settings::self(), &Okular::Settings::quickAnnotationToolsChanged, this, &PageViewAnnotator::reparseQuickToolsConfig, Qt::QueuedConnection);
0891 }
0892 
0893 void PageViewAnnotator::reparseConfig()
0894 {
0895     m_continuousMode = Okular::Settings::annotationContinuousMode();
0896 
0897     if (Okular::Settings::identityAuthor().isEmpty()) {
0898         detachAnnotation();
0899     }
0900 }
0901 
0902 void PageViewAnnotator::reparseBuiltinToolsConfig()
0903 {
0904     // Read tool list from configuration. It's a list of XML <tool></tool> elements
0905     if (!m_builtinToolsDefinition) {
0906         m_builtinToolsDefinition = new AnnotationTools();
0907     }
0908     m_builtinToolsDefinition->setTools(Okular::Settings::builtinAnnotationTools());
0909 
0910     if (m_actionHandler) {
0911         m_actionHandler->reparseBuiltinToolsConfig();
0912     }
0913 }
0914 
0915 void PageViewAnnotator::reparseQuickToolsConfig()
0916 {
0917     // Read tool list from configuration. It's a list of XML <tool></tool> elements
0918     if (!m_quickToolsDefinition) {
0919         m_quickToolsDefinition = new AnnotationTools();
0920     }
0921     m_quickToolsDefinition->setTools(Okular::Settings::quickAnnotationTools());
0922 
0923     if (m_actionHandler) {
0924         m_actionHandler->reparseQuickToolsConfig();
0925     }
0926 }
0927 
0928 PageViewAnnotator::~PageViewAnnotator()
0929 {
0930     delete m_engine;
0931     delete m_builtinToolsDefinition;
0932     delete m_quickToolsDefinition;
0933 }
0934 
0935 void PageViewAnnotator::setSignatureMode(bool enabled)
0936 {
0937     m_signatureMode = enabled;
0938 }
0939 
0940 bool PageViewAnnotator::signatureMode() const
0941 {
0942     return m_signatureMode;
0943 }
0944 
0945 bool PageViewAnnotator::active() const
0946 {
0947     return (m_engine != nullptr) || m_signatureMode;
0948 }
0949 
0950 bool PageViewAnnotator::annotating() const
0951 {
0952     return active() && m_lockedItem;
0953 }
0954 
0955 QCursor PageViewAnnotator::cursor() const
0956 {
0957     return m_engine ? m_engine->cursor() : Qt::CrossCursor;
0958 }
0959 
0960 QRect PageViewAnnotator::performRouteMouseOrTabletEvent(const AnnotatorEngine::EventType eventType, const AnnotatorEngine::Button button, const AnnotatorEngine::Modifiers modifiers, const QPointF pos, PageViewItem *item)
0961 {
0962     // creationCompleted is intended to be set by event(), handled subsequently by end(), and cleared within end().
0963     // If it's set here, we recursed for some reason (e.g., stacked event loop).
0964     // Just bail out, all we want to do is already on stack.
0965     if (m_engine && m_engine->creationCompleted()) {
0966         return QRect();
0967     }
0968 
0969     // if the right mouse button was pressed, we simply do nothing. In this way, we are still editing the annotation
0970     // and so this function will receive and process the right mouse button release event too. If we detach now the annotation tool,
0971     // the release event will be processed by the PageView class which would create the annotation property widget, and we do not want this.
0972     if (button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Press) {
0973         return QRect();
0974     } else if (button == AnnotatorEngine::Right && eventType == AnnotatorEngine::Release) {
0975         detachAnnotation();
0976         return QRect();
0977     }
0978 
0979     if (signatureMode() && eventType == AnnotatorEngine::Press) {
0980         m_engine = new PickPointEngineSignature(m_document, m_pageView);
0981     }
0982 
0983     // 1. lock engine to current item
0984     if (!m_lockedItem && eventType == AnnotatorEngine::Press) {
0985         m_lockedItem = item;
0986         m_engine->setItem(m_lockedItem);
0987     }
0988     if (!m_lockedItem) {
0989         return QRect();
0990     }
0991 
0992     // find out normalized mouse coords inside current item
0993     const QRect &itemRect = m_lockedItem->uncroppedGeometry();
0994     const QPointF eventPos = m_pageView->contentAreaPoint(pos);
0995     const double nX = qBound(0.0, m_lockedItem->absToPageX(eventPos.x()), 1.0);
0996     const double nY = qBound(0.0, m_lockedItem->absToPageY(eventPos.y()), 1.0);
0997 
0998     QRect modifiedRect;
0999 
1000     // 2. use engine to perform operations
1001     const QRect paintRect = m_engine->event(eventType, button, modifiers, nX, nY, itemRect.width(), itemRect.height(), m_lockedItem->page());
1002 
1003     // 3. update absolute extents rect and send paint event(s)
1004     if (paintRect.isValid()) {
1005         // 3.1. unite old and new painting regions
1006         QRegion compoundRegion(m_lastDrawnRect);
1007         m_lastDrawnRect = paintRect;
1008         m_lastDrawnRect.translate(itemRect.left(), itemRect.top());
1009         // 3.2. decompose paint region in rects and send paint events
1010         const QRegion rgn = compoundRegion.united(m_lastDrawnRect);
1011         const QPoint areaPos = m_pageView->contentAreaPosition();
1012         for (const QRect &r : rgn) {
1013             m_pageView->viewport()->update(r.translated(-areaPos));
1014         }
1015         modifiedRect = compoundRegion.boundingRect() | m_lastDrawnRect;
1016     }
1017 
1018     // 4. if engine has finished, apply Annotation to the page
1019     if (m_engine->creationCompleted()) {
1020         // apply engine data to the Annotation's and reset engine
1021         const QList<Okular::Annotation *> annotations = m_engine->end();
1022         // attach the newly filled annotations to the page
1023         for (Okular::Annotation *annotation : annotations) {
1024             if (!annotation) {
1025                 continue;
1026             }
1027 
1028             annotation->setCreationDate(QDateTime::currentDateTime());
1029             annotation->setModificationDate(QDateTime::currentDateTime());
1030             annotation->setAuthor(Okular::Settings::identityAuthor());
1031             m_document->addPageAnnotation(m_lockedItem->pageNumber(), annotation);
1032 
1033             if (annotation->openDialogAfterCreation()) {
1034                 m_pageView->openAnnotationWindow(annotation, m_lockedItem->pageNumber());
1035             }
1036         }
1037 
1038         if (signatureMode()) {
1039             auto signEngine = static_cast<PickPointEngineSignature *>(m_engine);
1040             if (signEngine->isAccepted()) {
1041                 const QString newFilePath = SignaturePartUtils::getFileNameForNewSignedFile(m_pageView, m_document);
1042 
1043                 if (!newFilePath.isEmpty()) {
1044                     const bool success = static_cast<PickPointEngineSignature *>(m_engine)->sign(newFilePath);
1045                     if (success) {
1046                         Q_EMIT requestOpenFile(newFilePath, m_lockedItem->pageNumber() + 1);
1047                     } else {
1048                         KMessageBox::error(m_pageView, i18nc("%1 is a file path", "Could not sign. Invalid certificate password or could not write to '%1'", newFilePath));
1049                     }
1050                 }
1051                 // Exit the signature mode.
1052                 setSignatureMode(false);
1053                 selectBuiltinTool(-1, ShowTip::No);
1054             } else if (signEngine->userWantsToStartOver()) {
1055                 delete m_engine;
1056                 m_engine = new PickPointEngineSignature(m_document, m_pageView);
1057                 return {};
1058             } else if (signEngine->isAborted()) {
1059                 // Exit the signature mode.
1060                 setSignatureMode(false);
1061                 selectBuiltinTool(-1, ShowTip::No);
1062             }
1063             m_continuousMode = false;
1064         }
1065 
1066         if (m_continuousMode) {
1067             selectLastTool();
1068         } else {
1069             detachAnnotation();
1070         }
1071     }
1072 
1073     return modifiedRect;
1074 }
1075 
1076 QRect PageViewAnnotator::routeMouseEvent(QMouseEvent *e, PageViewItem *item)
1077 {
1078     AnnotatorEngine::EventType eventType;
1079     AnnotatorEngine::Button button;
1080     AnnotatorEngine::Modifiers modifiers;
1081 
1082     // figure out the event type and button
1083     AnnotatorEngine::decodeEvent(e, &eventType, &button);
1084 
1085     // Constrain angle if action checked XOR shift button pressed.
1086     modifiers.constrainRatioAndAngle = (bool(constrainRatioAndAngleActive()) != bool(e->modifiers() & Qt::ShiftModifier));
1087 
1088     return performRouteMouseOrTabletEvent(eventType, button, modifiers, e->position(), item);
1089 }
1090 
1091 QRect PageViewAnnotator::routeTabletEvent(QTabletEvent *e, PageViewItem *item, const QPoint localOriginInGlobal)
1092 {
1093     // Unlike routeMouseEvent, routeTabletEvent must explicitly ignore events it doesn't care about so that
1094     // the corresponding mouse event will later be delivered.
1095     if (!item) {
1096         e->ignore();
1097         return QRect();
1098     }
1099 
1100     AnnotatorEngine::EventType eventType;
1101     AnnotatorEngine::Button button;
1102     AnnotatorEngine::Modifiers modifiers;
1103 
1104     // figure out the event type and button
1105     AnnotatorEngine::decodeEvent(e, &eventType, &button);
1106 
1107     // Constrain angle if action checked XOR shift button pressed.
1108     modifiers.constrainRatioAndAngle = (bool(constrainRatioAndAngleActive()) != bool(e->modifiers() & Qt::ShiftModifier));
1109 
1110     const QPointF globalPosF = e->globalPosition();
1111     const QPointF localPosF = globalPosF - localOriginInGlobal;
1112     return performRouteMouseOrTabletEvent(eventType, button, modifiers, localPosF, item);
1113 }
1114 
1115 bool PageViewAnnotator::routeKeyEvent(QKeyEvent *event)
1116 {
1117     if (event->key() == Qt::Key_Escape) {
1118         detachAnnotation();
1119         return true;
1120     }
1121     return false;
1122 }
1123 
1124 bool PageViewAnnotator::routePaints(const QRect wantedRect) const
1125 {
1126     return m_engine && wantedRect.intersects(m_lastDrawnRect) && m_lockedItem;
1127 }
1128 
1129 void PageViewAnnotator::routePaint(QPainter *painter, const QRect paintRect)
1130 {
1131     // if there's no locked item, then there's no decided place to draw on
1132     if (!m_lockedItem) {
1133         return;
1134     }
1135 
1136 #ifndef NDEBUG
1137     // [DEBUG] draw the paint region if enabled
1138     if (Okular::Settings::debugDrawAnnotationRect()) {
1139         painter->drawRect(paintRect);
1140     }
1141 #endif
1142     // move painter to current itemGeometry rect
1143     const QRect &itemRect = m_lockedItem->uncroppedGeometry();
1144     painter->save();
1145     painter->translate(itemRect.topLeft());
1146     // TODO: Clip annotation painting to cropped page.
1147 
1148     // transform cliprect from absolute to item relative coords
1149     QRect annotRect = paintRect.intersected(m_lastDrawnRect);
1150     annotRect.translate(-itemRect.topLeft());
1151 
1152     // use current engine for painting (in virtual page coordinates)
1153     m_engine->paint(painter, m_lockedItem->uncroppedWidth(), m_lockedItem->uncroppedHeight(), annotRect);
1154     painter->restore();
1155 }
1156 
1157 void PageViewAnnotator::selectBuiltinTool(int toolId, ShowTip showTip)
1158 {
1159     selectTool(m_builtinToolsDefinition, toolId, showTip);
1160 }
1161 
1162 void PageViewAnnotator::selectQuickTool(int toolId)
1163 {
1164     selectTool(m_quickToolsDefinition, toolId, ShowTip::Yes);
1165 }
1166 
1167 void PageViewAnnotator::selectTool(AnnotationTools *toolsDefinition, int toolId, ShowTip showTip)
1168 {
1169     // ask for Author's name if not already set
1170     if (toolId > 0 && Okular::Settings::identityAuthor().isEmpty()) {
1171         // get default username from the kdelibs/kdecore/KUser
1172         KUser currentUser;
1173         QString userName = currentUser.property(KUser::FullName).toString();
1174         // ask the user for confirmation/change
1175         if (userName.isEmpty()) {
1176             bool ok = false;
1177             userName = QInputDialog::getText(nullptr, i18n("Author name"), i18n("Author name for the annotation:"), QLineEdit::Normal, QString(), &ok);
1178 
1179             if (!ok) {
1180                 detachAnnotation();
1181                 return;
1182             }
1183         }
1184         // save the name
1185         Okular::Settings::setIdentityAuthor(userName);
1186         Okular::Settings::self()->save();
1187     }
1188 
1189     // terminate any previous operation
1190     if (m_engine) {
1191         delete m_engine;
1192         m_engine = nullptr;
1193     }
1194     m_lockedItem = nullptr;
1195     if (m_lastDrawnRect.isValid()) {
1196         m_pageView->viewport()->update(m_lastDrawnRect.translated(-m_pageView->contentAreaPosition()));
1197         m_lastDrawnRect = QRect();
1198     }
1199 
1200     // store current tool for later usage
1201     m_lastToolId = toolId;
1202     m_lastToolsDefinition = toolsDefinition;
1203 
1204     // handle tool deselection
1205     if (toolId == -1) {
1206         m_pageView->displayMessage(QString());
1207         m_pageView->updateCursor();
1208         Q_EMIT toolActive(false);
1209         return;
1210     }
1211 
1212     // for the selected tool create the Engine
1213     QDomElement toolElement = toolsDefinition->tool(toolId);
1214     if (!toolElement.isNull()) {
1215         // parse tool properties
1216         QDomElement engineElement = toolElement.firstChildElement(QStringLiteral("engine"));
1217         if (!engineElement.isNull()) {
1218             // create the AnnotatorEngine
1219             QString type = engineElement.attribute(QStringLiteral("type"));
1220             if (type == QLatin1String("SmoothLine")) {
1221                 m_engine = new SmoothPathEngine(engineElement);
1222             } else if (type == QLatin1String("PickPoint")) {
1223                 m_engine = new PickPointEngine(engineElement);
1224             } else if (type == QLatin1String("PolyLine")) {
1225                 m_engine = new PolyLineEngine(engineElement);
1226             } else if (type == QLatin1String("TextSelector")) {
1227                 m_engine = new TextSelectorEngine(engineElement, m_pageView);
1228             } else {
1229                 qCWarning(OkularUiDebug).nospace() << "tools.xml: engine type:'" << type << "' is not defined!";
1230             }
1231 
1232             if (showTip == ShowTip::Yes) {
1233                 // display the tooltip
1234                 const QString annotType = toolElement.attribute(QStringLiteral("type"));
1235                 QString tip;
1236 
1237                 if (annotType == QLatin1String("ellipse")) {
1238                     tip = i18nc("Annotation tool", "Draw an ellipse (drag to select a zone)");
1239                 } else if (annotType == QLatin1String("highlight")) {
1240                     tip = i18nc("Annotation tool", "Highlight text");
1241                 } else if (annotType == QLatin1String("ink")) {
1242                     tip = i18nc("Annotation tool", "Draw a freehand line");
1243                 } else if (annotType == QLatin1String("note-inline")) {
1244                     tip = i18nc("Annotation tool", "Inline Text Annotation (drag to select a zone)");
1245                 } else if (annotType == QLatin1String("note-linked")) {
1246                     tip = i18nc("Annotation tool", "Put a pop-up note");
1247                 } else if (annotType == QLatin1String("polygon")) {
1248                     tip = i18nc("Annotation tool", "Draw a polygon (click on the first point to close it)");
1249                 } else if (annotType == QLatin1String("rectangle")) {
1250                     tip = i18nc("Annotation tool", "Draw a rectangle");
1251                 } else if (annotType == QLatin1String("squiggly")) {
1252                     tip = i18nc("Annotation tool", "Squiggle text");
1253                 } else if (annotType == QLatin1String("stamp")) {
1254                     tip = i18nc("Annotation tool", "Put a stamp symbol");
1255                 } else if (annotType == QLatin1String("straight-line")) {
1256                     tip = i18nc("Annotation tool", "Draw a straight line");
1257                 } else if (annotType == QLatin1String("strikeout")) {
1258                     tip = i18nc("Annotation tool", "Strike out text");
1259                 } else if (annotType == QLatin1String("underline")) {
1260                     tip = i18nc("Annotation tool", "Underline text");
1261                 } else if (annotType == QLatin1String("typewriter")) {
1262                     tip = i18nc("Annotation tool", "Typewriter Annotation (drag to select a zone)");
1263                 }
1264 
1265                 if (!tip.isEmpty()) {
1266                     m_pageView->displayMessage(tip, QString(), PageViewMessage::Annotation);
1267                 }
1268             }
1269         }
1270 
1271         // consistency warning
1272         if (!m_engine) {
1273             qCWarning(OkularUiDebug) << "tools.xml: couldn't find good engine description. check xml.";
1274         }
1275 
1276         m_pageView->updateCursor();
1277     }
1278 
1279     Q_EMIT toolActive(true);
1280 }
1281 
1282 void PageViewAnnotator::selectLastTool()
1283 {
1284     selectTool(m_lastToolsDefinition, m_lastToolId, ShowTip::No);
1285 }
1286 
1287 void PageViewAnnotator::selectStampTool(const QString &stampSymbol)
1288 {
1289     QDomElement toolElement = builtinTool(STAMP_TOOL_ID);
1290     QDomElement engineElement = toolElement.firstChildElement(QStringLiteral("engine"));
1291     QDomElement annotationElement = engineElement.firstChildElement(QStringLiteral("annotation"));
1292     engineElement.setAttribute(QStringLiteral("hoverIcon"), stampSymbol);
1293     annotationElement.setAttribute(QStringLiteral("icon"), stampSymbol);
1294     saveBuiltinAnnotationTools();
1295     selectBuiltinTool(STAMP_TOOL_ID, ShowTip::Yes);
1296 }
1297 
1298 void PageViewAnnotator::detachAnnotation()
1299 {
1300     if (m_lastToolId == -1) {
1301         return;
1302     }
1303     selectBuiltinTool(-1, ShowTip::No);
1304     if (!signatureMode()) {
1305         if (m_actionHandler) {
1306             m_actionHandler->deselectAllAnnotationActions();
1307         }
1308     } else {
1309         m_pageView->displayMessage(QString());
1310         setSignatureMode(false);
1311     }
1312 }
1313 
1314 QString PageViewAnnotator::defaultToolName(const QDomElement &toolElement)
1315 {
1316     const QString annotType = toolElement.attribute(QStringLiteral("type"));
1317 
1318     if (annotType == QLatin1String("ellipse")) {
1319         return i18n("Ellipse");
1320     } else if (annotType == QLatin1String("highlight")) {
1321         return i18n("Highlighter");
1322     } else if (annotType == QLatin1String("ink")) {
1323         return i18n("Freehand Line");
1324     } else if (annotType == QLatin1String("note-inline")) {
1325         return i18n("Inline Note");
1326     } else if (annotType == QLatin1String("note-linked")) {
1327         return i18n("Pop-up Note");
1328     } else if (annotType == QLatin1String("polygon")) {
1329         return i18n("Polygon");
1330     } else if (annotType == QLatin1String("rectangle")) {
1331         return i18n("Rectangle");
1332     } else if (annotType == QLatin1String("squiggly")) {
1333         return i18n("Squiggle");
1334     } else if (annotType == QLatin1String("stamp")) {
1335         return i18n("Stamp");
1336     } else if (annotType == QLatin1String("straight-line")) {
1337         return i18n("Straight Line");
1338     } else if (annotType == QLatin1String("strikeout")) {
1339         return i18n("Strike out");
1340     } else if (annotType == QLatin1String("underline")) {
1341         return i18n("Underline");
1342     } else if (annotType == QLatin1String("typewriter")) {
1343         return i18n("Typewriter");
1344     } else {
1345         return QString();
1346     }
1347 }
1348 
1349 QPixmap PageViewAnnotator::makeToolPixmap(const QDomElement &toolElement)
1350 {
1351     QPixmap pixmap(32 * qApp->devicePixelRatio(), 32 * qApp->devicePixelRatio());
1352     pixmap.setDevicePixelRatio(qApp->devicePixelRatio());
1353     const QString annotType = toolElement.attribute(QStringLiteral("type"));
1354 
1355     // Load HiDPI variant on HiDPI screen
1356     QString imageVariant;
1357     if (qApp->devicePixelRatio() > 1.05) {
1358         imageVariant = QStringLiteral("@2x");
1359     }
1360 
1361     // Load base pixmap. We'll draw on top of it
1362     pixmap.load(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-base-okular") + imageVariant + QStringLiteral(".png")));
1363 
1364     /* Parse color, innerColor and icon (if present) */
1365     QColor engineColor, innerColor, textColor, annotColor;
1366     QString icon;
1367     QDomNodeList engineNodeList = toolElement.elementsByTagName(QStringLiteral("engine"));
1368     if (engineNodeList.size() > 0) {
1369         QDomElement engineEl = engineNodeList.item(0).toElement();
1370         if (!engineEl.isNull() && engineEl.hasAttribute(QStringLiteral("color"))) {
1371             engineColor = QColor(engineEl.attribute(QStringLiteral("color")));
1372         }
1373     }
1374     QDomNodeList annotationNodeList = toolElement.elementsByTagName(QStringLiteral("annotation"));
1375     if (annotationNodeList.size() > 0) {
1376         QDomElement annotationEl = annotationNodeList.item(0).toElement();
1377         if (!annotationEl.isNull()) {
1378             if (annotationEl.hasAttribute(QStringLiteral("color"))) {
1379                 annotColor = annotationEl.attribute(QStringLiteral("color"));
1380             }
1381             if (annotationEl.hasAttribute(QStringLiteral("innerColor"))) {
1382                 innerColor = QColor(annotationEl.attribute(QStringLiteral("innerColor")));
1383             }
1384             if (annotationEl.hasAttribute(QStringLiteral("textColor"))) {
1385                 textColor = QColor(annotationEl.attribute(QStringLiteral("textColor")));
1386             }
1387             if (annotationEl.hasAttribute(QStringLiteral("icon"))) {
1388                 icon = annotationEl.attribute(QStringLiteral("icon"));
1389             }
1390         }
1391     }
1392 
1393     QPainter p(&pixmap);
1394 
1395     if (annotType == QLatin1String("ellipse")) {
1396         p.setRenderHint(QPainter::Antialiasing);
1397         if (innerColor.isValid()) {
1398             p.setBrush(innerColor);
1399         }
1400         p.setPen(QPen(engineColor, 2));
1401         p.drawEllipse(2, 7, 21, 14);
1402     } else if (annotType == QLatin1String("highlight")) {
1403         QImage overlay(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-highlighter-okular-colorizable") + imageVariant + QStringLiteral(".png")));
1404         QImage colorizedOverlay = overlay;
1405         GuiUtils::colorizeImage(colorizedOverlay, engineColor);
1406 
1407         p.drawImage(QPoint(0, 0), colorizedOverlay);   // Trail
1408         p.drawImage(QPoint(0, -32), overlay);          // Text + Shadow (uncolorized)
1409         p.drawImage(QPoint(0, -64), colorizedOverlay); // Pen
1410     } else if (annotType == QLatin1String("ink")) {
1411         QImage overlay(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-ink-okular-colorizable") + imageVariant + QStringLiteral(".png")));
1412         QImage colorizedOverlay = overlay;
1413         GuiUtils::colorizeImage(colorizedOverlay, engineColor);
1414 
1415         p.drawImage(QPoint(0, 0), colorizedOverlay);   // Trail
1416         p.drawImage(QPoint(0, -32), overlay);          // Shadow (uncolorized)
1417         p.drawImage(QPoint(0, -64), colorizedOverlay); // Pen
1418     } else if (annotType == QLatin1String("note-inline")) {
1419         QImage overlay(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-note-inline-okular-colorizable") + imageVariant + QStringLiteral(".png")));
1420         GuiUtils::colorizeImage(overlay, engineColor);
1421         p.drawImage(QPoint(0, 0), overlay);
1422     } else if (annotType == QLatin1String("note-linked")) {
1423         QImage overlay(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-note-okular-colorizable") + imageVariant + QStringLiteral(".png")));
1424         GuiUtils::colorizeImage(overlay, engineColor);
1425         p.drawImage(QPoint(0, 0), overlay);
1426     } else if (annotType == QLatin1String("polygon")) {
1427         QPainterPath path;
1428         path.moveTo(0, 7);
1429         path.lineTo(19, 7);
1430         path.lineTo(19, 14);
1431         path.lineTo(23, 14);
1432         path.lineTo(23, 20);
1433         path.lineTo(0, 20);
1434         if (innerColor.isValid()) {
1435             p.setBrush(innerColor);
1436         }
1437         p.setPen(QPen(engineColor, 1));
1438         p.drawPath(path);
1439     } else if (annotType == QLatin1String("rectangle")) {
1440         p.setRenderHint(QPainter::Antialiasing);
1441         if (innerColor.isValid()) {
1442             p.setBrush(innerColor);
1443         }
1444         p.setPen(QPen(engineColor, 2));
1445         p.drawRect(2, 7, 21, 14);
1446     } else if (annotType == QLatin1String("squiggly")) {
1447         QPen pen(engineColor, 1);
1448         pen.setDashPattern(QVector<qreal>() << 1 << 1);
1449         p.setPen(pen);
1450         p.drawLine(1, 13, 16, 13);
1451         p.drawLine(2, 14, 15, 14);
1452         p.drawLine(0, 20, 19, 20);
1453         p.drawLine(1, 21, 18, 21);
1454     } else if (annotType == QLatin1String("stamp")) {
1455         QPixmap stamp = Okular::AnnotationUtils::loadStamp(icon, 16, false /* keepAspectRatio */);
1456         p.setRenderHint(QPainter::Antialiasing);
1457         p.drawPixmap(16, 14, stamp);
1458     } else if (annotType == QLatin1String("straight-line")) {
1459         QPainterPath path;
1460         path.moveTo(1, 8);
1461         path.lineTo(20, 8);
1462         path.lineTo(1, 27);
1463         path.lineTo(20, 27);
1464         p.setRenderHint(QPainter::Antialiasing);
1465         p.setPen(QPen(engineColor, 1));
1466         p.drawPath(path); // TODO To be discussed: This is not a straight line!
1467     } else if (annotType == QLatin1String("strikeout")) {
1468         p.setPen(QPen(engineColor, 1));
1469         p.drawLine(1, 10, 16, 10);
1470         p.drawLine(0, 17, 19, 17);
1471     } else if (annotType == QLatin1String("underline")) {
1472         p.setPen(QPen(engineColor, 1));
1473         p.drawLine(1, 13, 16, 13);
1474         p.drawLine(0, 20, 19, 20);
1475     } else if (annotType == QLatin1String("typewriter")) {
1476         QImage overlay(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/tool-typewriter-okular-colorizable") + imageVariant + QStringLiteral(".png")));
1477         GuiUtils::colorizeImage(overlay, textColor);
1478         p.drawImage(QPoint(-2, 2), overlay);
1479     } else {
1480         /* Unrecognized annotation type -- It shouldn't happen */
1481         p.setPen(QPen(engineColor));
1482         p.drawText(QPoint(20, 31), QStringLiteral("?"));
1483     }
1484 
1485     return pixmap;
1486 }
1487 
1488 void PageViewAnnotator::setupActions(KActionCollection *ac)
1489 {
1490     if (!m_actionHandler) {
1491         m_actionHandler = new AnnotationActionHandler(this, ac);
1492     }
1493 }
1494 
1495 void PageViewAnnotator::setupActionsPostGUIActivated()
1496 {
1497     m_actionHandler->setupAnnotationToolBarVisibilityAction();
1498 }
1499 
1500 bool PageViewAnnotator::continuousMode()
1501 {
1502     return m_continuousMode;
1503 }
1504 
1505 void PageViewAnnotator::setContinuousMode(bool enabled)
1506 {
1507     m_continuousMode = enabled;
1508     Okular::Settings::setAnnotationContinuousMode(enabled);
1509     Okular::Settings::self()->save();
1510 }
1511 
1512 bool PageViewAnnotator::constrainRatioAndAngleActive()
1513 {
1514     return m_constrainRatioAndAngle;
1515 }
1516 
1517 void PageViewAnnotator::setConstrainRatioAndAngle(bool enabled)
1518 {
1519     m_constrainRatioAndAngle = enabled;
1520 }
1521 
1522 void PageViewAnnotator::setToolsEnabled(bool enabled)
1523 {
1524     if (m_actionHandler) {
1525         m_actionHandler->setToolsEnabled(enabled);
1526     }
1527 }
1528 
1529 void PageViewAnnotator::setTextToolsEnabled(bool enabled)
1530 {
1531     if (m_actionHandler) {
1532         m_actionHandler->setTextToolsEnabled(enabled);
1533     }
1534 }
1535 
1536 void PageViewAnnotator::saveBuiltinAnnotationTools()
1537 {
1538     Okular::Settings::setBuiltinAnnotationTools(m_builtinToolsDefinition->toStringList());
1539     Okular::Settings::self()->save();
1540 }
1541 
1542 QDomElement PageViewAnnotator::builtinTool(int toolId)
1543 {
1544     return m_builtinToolsDefinition->tool(toolId);
1545 }
1546 
1547 QDomElement PageViewAnnotator::quickTool(int toolId)
1548 {
1549     return m_quickToolsDefinition->tool(toolId);
1550 }
1551 
1552 QDomElement PageViewAnnotator::currentEngineElement()
1553 {
1554     return m_builtinToolsDefinition->tool(m_lastToolId).firstChildElement(QStringLiteral("engine"));
1555 }
1556 
1557 QDomElement PageViewAnnotator::currentAnnotationElement()
1558 {
1559     return currentEngineElement().firstChildElement(QStringLiteral("annotation"));
1560 }
1561 
1562 void PageViewAnnotator::setAnnotationWidth(double width)
1563 {
1564     currentAnnotationElement().setAttribute(QStringLiteral("width"), QString::number(width));
1565     saveBuiltinAnnotationTools();
1566     selectLastTool();
1567 }
1568 
1569 void PageViewAnnotator::setAnnotationColor(const QColor &color)
1570 {
1571     currentEngineElement().setAttribute(QStringLiteral("color"), color.name(QColor::HexRgb));
1572     QDomElement annotationElement = currentAnnotationElement();
1573     QString annotType = annotationElement.attribute(QStringLiteral("type"));
1574     if (annotType == QLatin1String("Typewriter")) {
1575         annotationElement.setAttribute(QStringLiteral("textColor"), color.name(QColor::HexRgb));
1576     } else {
1577         annotationElement.setAttribute(QStringLiteral("color"), color.name(QColor::HexRgb));
1578     }
1579     saveBuiltinAnnotationTools();
1580     selectLastTool();
1581 }
1582 
1583 void PageViewAnnotator::setAnnotationInnerColor(const QColor &color)
1584 {
1585     QDomElement annotationElement = currentAnnotationElement();
1586     if (color == Qt::transparent) {
1587         annotationElement.removeAttribute(QStringLiteral("innerColor"));
1588     } else {
1589         annotationElement.setAttribute(QStringLiteral("innerColor"), color.name(QColor::HexRgb));
1590     }
1591     saveBuiltinAnnotationTools();
1592     selectLastTool();
1593 }
1594 
1595 void PageViewAnnotator::setAnnotationOpacity(double opacity)
1596 {
1597     currentAnnotationElement().setAttribute(QStringLiteral("opacity"), QString::number(opacity));
1598     saveBuiltinAnnotationTools();
1599     selectLastTool();
1600 }
1601 
1602 void PageViewAnnotator::setAnnotationFont(const QFont &font)
1603 {
1604     currentAnnotationElement().setAttribute(QStringLiteral("font"), font.toString());
1605     saveBuiltinAnnotationTools();
1606     selectLastTool();
1607 }
1608 
1609 void PageViewAnnotator::addToQuickAnnotations()
1610 {
1611     QDomElement sourceToolElement = m_builtinToolsDefinition->tool(m_lastToolId);
1612     if (sourceToolElement.isNull()) {
1613         return;
1614     }
1615 
1616     // set custom name for quick annotation
1617     bool ok = false;
1618     QString itemText = QInputDialog::getText(nullptr, i18n("Add favorite annotation"), i18n("Custom annotation name:"), QLineEdit::Normal, defaultToolName(sourceToolElement), &ok);
1619     if (!ok) {
1620         return;
1621     }
1622 
1623     QDomElement toolElement = sourceToolElement.cloneNode().toElement();
1624     // store name attribute only if the user specified a customized name
1625     if (!itemText.isEmpty()) {
1626         toolElement.setAttribute(QStringLiteral("name"), itemText);
1627     }
1628     m_quickToolsDefinition->appendTool(toolElement);
1629     Okular::Settings::setQuickAnnotationTools(m_quickToolsDefinition->toStringList());
1630     Okular::Settings::self()->save();
1631 }
1632 
1633 void PageViewAnnotator::slotAdvancedSettings()
1634 {
1635     QDomElement toolElement = m_builtinToolsDefinition->tool(m_lastToolId);
1636 
1637     EditAnnotToolDialog t(nullptr, toolElement, true);
1638     if (t.exec() != QDialog::Accepted) {
1639         return;
1640     }
1641 
1642     QDomElement toolElementUpdated = t.toolXml().documentElement();
1643     int toolId = toolElement.attribute(QStringLiteral("id")).toInt();
1644     m_builtinToolsDefinition->updateTool(toolElementUpdated, toolId);
1645     saveBuiltinAnnotationTools();
1646     selectLastTool();
1647 }
1648 
1649 /* kate: replace-tabs on; indent-width 4; */