File indexing completed on 2024-05-19 04:35:39
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; */