diff --git a/pdf/CMakeLists.txt b/pdf/CMakeLists.txt index b7d6d7c8..fdc5789c 100644 --- a/pdf/CMakeLists.txt +++ b/pdf/CMakeLists.txt @@ -12,6 +12,7 @@ set(pdfplugin_SRCS pdflinkarea.cpp pdfsearchmodel.cpp pdfselection.cpp + pdfannotation.cpp ) add_library(sailfishofficepdfplugin MODULE ${pdfplugin_SRCS}) diff --git a/pdf/pdfannotation.cpp b/pdf/pdfannotation.cpp new file mode 100644 index 00000000..7b1a0e89 --- /dev/null +++ b/pdf/pdfannotation.cpp @@ -0,0 +1,346 @@ +/* -*- c-basic-offset: 4 -*- */ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "pdfannotation.h" + +static QString currentAuthor; + +class PDFAnnotation::Private +{ +public: + Private(Poppler::Annotation *annotation, PDFDocument *document = nullptr, int page = -1) + : owner(document == nullptr || page < 0) + , m_annotation(annotation) + , m_document(document) + , m_page(page) + { + } + ~Private() + { + if (owner) + delete m_annotation; + } + bool owner; + Poppler::Annotation *m_annotation; + PDFDocument *m_document; + int m_page; +}; + +PDFAnnotation::PDFAnnotation(Poppler::Annotation *annotation, QObject *parent) + : QObject(parent), d(new PDFAnnotation::Private(annotation)) +{ + if (annotation && !currentAuthor.isEmpty()) + annotation->setAuthor(currentAuthor); +} + +PDFAnnotation::PDFAnnotation(Poppler::Annotation *annotation, + PDFDocument *document, int ipage, QObject *parent) + : QObject(parent), d(new PDFAnnotation::Private(annotation, document, ipage)) +{ +} + +PDFAnnotation::~PDFAnnotation() +{ + delete d; +} + +PDFDocument* PDFAnnotation::document() const +{ + return d->m_document; +} + +int PDFAnnotation::page() const +{ + return d->m_page; +} + +QRectF PDFAnnotation::boundary() const +{ + return d->m_annotation->boundary(); +} + +void PDFAnnotation::attachOnce(PDFDocument *document, int page) +{ + /* Attach annotation only once. */ + if (d->m_document != nullptr || document == nullptr) + return; + if (d->m_page >= 0 || page < 0 || page >= document->pageCount()) + return; + + d->m_document = document; + d->m_page = page; +} + +void PDFAnnotation::attach(PDFDocument *document, PDFSelection *selection) +{ + if (!selection || selection->count() == 0) + return; + + QPair word = selection->rectAt(0); + attachOnce(document, word.first); + + QRectF bounds = word.second; + for (int i = 1; i < selection->count(); i++) + bounds.united(selection->rectAt(i).second); + + d->m_annotation->setBoundary(bounds); + d->m_document->addAnnotation(d->m_annotation, d->m_page); + emit attached(); +} + +void PDFAnnotation::remove() +{ + if (d->m_document && d->m_annotation) { + d->m_document->removeAnnotation(d->m_annotation, d->m_page); + d->m_annotation = nullptr; + } + + deleteLater(); +} + +QString PDFAnnotation::author() const +{ + return d->m_annotation->author(); +} + +void PDFAnnotation::setAuthor(const QString &value) +{ + // Store last given author to use it as default for + // newly created annotations. + currentAuthor = value; + + if (d->m_annotation->author() == value) + return; + d->m_annotation->setAuthor(value); + emit authorChanged(); + + d->m_annotation->setModificationDate(QDateTime()); + emit modificationDateChanged(); + + if (d->m_document != nullptr) + d->m_document->setDocumentModified(); +} + +QString PDFAnnotation::contents() const +{ + return d->m_annotation->contents(); +} + +void PDFAnnotation::setContents(const QString &value) +{ + if (d->m_annotation->contents() == value) + return; + + d->m_annotation->setContents(value); + emit contentsChanged(); + + d->m_annotation->setModificationDate(QDateTime()); + emit modificationDateChanged(); + + if (d->m_document != nullptr) + d->m_document->setDocumentModified(); +} + +QDateTime PDFAnnotation::creationDate() const +{ + return d->m_annotation->creationDate(); +} + +QDateTime PDFAnnotation::modificationDate() const +{ + return d->m_annotation->modificationDate(); +} + +QColor PDFAnnotation::color() const +{ + return d->m_annotation->style().color(); +} + +void PDFAnnotation::setColor(const QColor &value) +{ + if (color() == value) + return; + + Poppler::Annotation::Style style = d->m_annotation->style(); + style.setColor(value); + d->m_annotation->setStyle(style); + emit colorChanged(); + + if (d->m_document != nullptr && d->m_page >= 0) + d->m_document->onPageModified(d->m_page, d->m_annotation->boundary()); +} + +PDFAnnotation::SubType PDFAnnotation::type() const +{ + return PDFAnnotation::SubType(d->m_annotation->subType()); +} + + +static QStringList iconNames = (QStringList() << "Note" << "Comment" << "Key" << "Help" << "NewParagraph" << "Paragraph" << "Insert" << "Cross" << "Circle"); + +PDFTextAnnotation::PDFTextAnnotation(QObject *parent) + : PDFAnnotation(new Poppler::TextAnnotation(Poppler::TextAnnotation::Linked), parent) +{ +} + +PDFTextAnnotation::PDFTextAnnotation(Poppler::TextAnnotation *annotation, + PDFDocument *document, int ipage, QObject *parent) + : PDFAnnotation(annotation, document, ipage, parent) +{ +} + +PDFTextAnnotation::~PDFTextAnnotation() +{ +} + +void PDFTextAnnotation::attach(PDFDocument *document, PDFSelection *selection) +{ + if (!selection) + return; + + QPair word = selection->rectAt(0); + attachAt(document, word.first, word.second.x(), word.second.y()); +} + +void PDFTextAnnotation::attachAt(PDFDocument *document, + unsigned int page, qreal x, qreal y) +{ + attachOnce(document, page); + + // The size of the icon used to render the note on the document + // is hard-coded in Poppler to 24 1/72 of inch, see AnnotText::draw() + // of poppler/Annot.cc file. So we use the same size for the bounding box. + d->m_annotation->setBoundary(QRectF(x, y, 24., 24.)); + d->m_document->addAnnotation(d->m_annotation, d->m_page, true); + emit attached(); +} + +PDFTextAnnotation::IconType PDFTextAnnotation::icon() const +{ + Poppler::TextAnnotation *anno = static_cast(d->m_annotation); + return PDFTextAnnotation::IconType(iconNames.indexOf(anno->textIcon())); +} + +void PDFTextAnnotation::setIcon(PDFTextAnnotation::IconType value) +{ + if (icon() == value) + return; + + int index = int(value); + if (index > iconNames.length() || index < 0) { + qWarning() << QStringLiteral("Wrong icon for text annotation (%1).").arg(index); + return; + } + + Poppler::TextAnnotation *anno = static_cast(d->m_annotation); + anno->setTextIcon(iconNames[value]); + emit iconChanged(); + + if (d->m_document != nullptr && d->m_page >= 0) + d->m_document->onPageModified(d->m_page, d->m_annotation->boundary()); +} + +PDFCaretAnnotation::PDFCaretAnnotation(QObject *parent) + : PDFAnnotation(new Poppler::CaretAnnotation(), parent) +{ + Poppler::CaretAnnotation *anno = static_cast(d->m_annotation); + anno->setCaretSymbol(Poppler::CaretAnnotation::CaretSymbol::P); +} + +PDFCaretAnnotation::PDFCaretAnnotation(Poppler::CaretAnnotation *annotation, + PDFDocument *document, int ipage, QObject *parent) + : PDFAnnotation(annotation, document, ipage, parent) +{ +} + +PDFCaretAnnotation::~PDFCaretAnnotation() +{ +} + +PDFHighlightAnnotation::PDFHighlightAnnotation(QObject *parent) + : PDFAnnotation(new Poppler::HighlightAnnotation(), parent) +{ + setColor(QColor(255, 255, 0, 255)); +} + +PDFHighlightAnnotation::PDFHighlightAnnotation(Poppler::HighlightAnnotation *annotation, + PDFDocument *document, int ipage, + QObject *parent) + : PDFAnnotation(annotation, document, ipage, parent) +{ +} + +PDFHighlightAnnotation::~PDFHighlightAnnotation() +{ +} + +PDFHighlightAnnotation::HighlightType PDFHighlightAnnotation::style() const +{ + Poppler::HighlightAnnotation *anno = static_cast(d->m_annotation); + return PDFHighlightAnnotation::HighlightType(anno->highlightType()); +} + +void PDFHighlightAnnotation::setStyle(PDFHighlightAnnotation::HighlightType value) +{ + if (style() == value) + return; + + Poppler::HighlightAnnotation *anno = static_cast(d->m_annotation); + anno->setHighlightType(Poppler::HighlightAnnotation::HighlightType(value)); + emit styleChanged(); + + if (d->m_document != nullptr && d->m_page >= 0) + d->m_document->onPageModified(d->m_page, d->m_annotation->boundary()); +} + +void PDFHighlightAnnotation::attach(PDFDocument *document, PDFSelection *selection) +{ + if (!selection) + return; + + QPair word = selection->rectAt(0); + attachOnce(document, word.first); + + QRectF bounds = word.second; + QList quads; + int count = selection->count(); + + for (int i = 0; i < count; i++) { + word = selection->rectAt(i); + Poppler::HighlightAnnotation::Quad quad; + if (word.first == d->m_page) { + quad.capEnd = (i == count - 1); + quad.capStart = (i == 0); + quad.feather = 0.; + quad.points[0] = QPointF(word.second.left(), word.second.top()); + quad.points[1] = QPointF(word.second.right(), word.second.top()); + quad.points[2] = QPointF(word.second.right(), word.second.bottom()); + quad.points[3] = QPointF(word.second.left(), word.second.bottom()); + quads.append(quad); + bounds |= word.second; + } + } + + Poppler::HighlightAnnotation *annotation = + static_cast(d->m_annotation); + annotation->setHighlightQuads(quads); + d->m_annotation->setBoundary(bounds); + d->m_document->addAnnotation(d->m_annotation, d->m_page); + emit attached(); +} diff --git a/pdf/pdfannotation.h b/pdf/pdfannotation.h new file mode 100644 index 00000000..8729b856 --- /dev/null +++ b/pdf/pdfannotation.h @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef PDFANNOTATION_H +#define PDFANNOTATION_H + +#include + +#include "pdfdocument.h" +#include "pdfselection.h" + +class PDFTextAnnotation; +class PDFHighlightAnnotation; + +class PDFAnnotation : public QObject +{ + Q_OBJECT + + Q_ENUMS(SubType) + + Q_PROPERTY(PDFDocument* document READ document NOTIFY attached) + Q_PROPERTY(int page READ page NOTIFY attached) + Q_PROPERTY(QRectF boundary READ boundary NOTIFY attached) + + Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged) + Q_PROPERTY(QString contents READ contents WRITE setContents NOTIFY contentsChanged) + Q_PROPERTY(QDateTime creationDate READ creationDate CONSTANT) + Q_PROPERTY(QDateTime modificationDate READ modificationDate NOTIFY modificationDateChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + Q_PROPERTY(SubType type READ type CONSTANT) + +public: + enum SubType { + Base, + Text, + Line, + GeometricFigure, + Highlight, + Stamp, + InkPath, + Link, + Caret, + FileAttachment, + Sound, + Movie, + Screen, + Widget + }; + + PDFAnnotation(QObject *parent = 0); + PDFAnnotation(Poppler::Annotation *annotation, QObject *parent = 0); + PDFAnnotation(Poppler::Annotation *annotation, + PDFDocument *document, int ipage, QObject *parent = 0); + ~PDFAnnotation(); + + SubType type() const; + + PDFDocument* document() const; + int page() const; + QRectF boundary() const; + Q_INVOKABLE void attach(PDFDocument *document, PDFSelection *selection); + Q_INVOKABLE void remove(); + + QString author() const; + void setAuthor(const QString &value); + + QString contents() const; + void setContents(const QString &value); + + QDateTime creationDate() const; + + QDateTime modificationDate() const; + + QColor color() const; + void setColor(const QColor &value); + +Q_SIGNALS: + void attached(); + void authorChanged(); + void contentsChanged(); + void creationDateChanged(); + void modificationDateChanged(); + void colorChanged(); + +protected: + class Private; + Private *d; + + void attachOnce(PDFDocument *document, int page); +}; + +class PDFTextAnnotation : public PDFAnnotation +{ + Q_OBJECT + + Q_ENUMS(IconType) + + Q_PROPERTY(IconType icon READ icon WRITE setIcon NOTIFY iconChanged) + +public: + enum IconType { + Note, + Comment, + Key, + Help, + NewParagraph, + Paragraph, + Insert, + Cross, + Circle + }; + + PDFTextAnnotation(QObject *parent = 0); + PDFTextAnnotation(Poppler::TextAnnotation *annotation, + PDFDocument *document, int ipage, QObject *parent = 0); + ~PDFTextAnnotation(); + + Q_INVOKABLE void attach(PDFDocument *document, PDFSelection *selection); + Q_INVOKABLE void attachAt(PDFDocument *document, + unsigned int page, qreal x, qreal y); + + IconType icon() const; + void setIcon(IconType value); + +Q_SIGNALS: + void iconChanged(); +}; + +class PDFCaretAnnotation : public PDFAnnotation +{ + Q_OBJECT + +public: + PDFCaretAnnotation(QObject *parent = 0); + PDFCaretAnnotation(Poppler::CaretAnnotation *annotation, + PDFDocument *document, int ipage, QObject *parent = 0); + ~PDFCaretAnnotation(); +}; + +class PDFHighlightAnnotation : public PDFAnnotation +{ + Q_OBJECT + + Q_ENUMS(HighlightType) + + Q_PROPERTY(HighlightType style READ style WRITE setStyle NOTIFY styleChanged) + +public: + enum HighlightType { + Highlight, + Squiggly, + Underline, + StrikeOut + }; + + PDFHighlightAnnotation(QObject *parent = 0); + PDFHighlightAnnotation(Poppler::HighlightAnnotation *annotation, + PDFDocument *document, int ipage, QObject *parent = 0); + ~PDFHighlightAnnotation(); + + Q_INVOKABLE void attach(PDFDocument *document, PDFSelection *selection); + + HighlightType style() const; + void setStyle(HighlightType value); + +Q_SIGNALS: + void styleChanged(); +}; + +#endif // PDFANNOTATION_H diff --git a/pdf/pdfcanvas.cpp b/pdf/pdfcanvas.cpp index 815a8e20..b9f51908 100644 --- a/pdf/pdfcanvas.cpp +++ b/pdf/pdfcanvas.cpp @@ -31,6 +31,8 @@ #include "pdfrenderthread.h" #include "pdfdocument.h" +typedef QPair Patch; + struct PDFPage { PDFPage() : index(-1) @@ -45,9 +47,12 @@ struct PDFPage { bool requested; int renderWidth; + QRect textureArea; QSGTexture *texture; + QList patches; + QList > links; }; @@ -67,6 +72,11 @@ class PDFCanvas::Private , linkWiggle(4.f) { } + enum TextureType{ + RootTexture, + PatchTexture + }; + PDFCanvas *q; QHash pages; @@ -101,6 +111,11 @@ class PDFCanvas::Private texturesToClean << page.texture; page.texture = nullptr; } + for (QList::iterator it = page.patches.begin(); + it != page.patches.end(); it++) { + texturesToClean << it->second; + } + page.patches.clear(); } void cleanTextures() @@ -109,6 +124,27 @@ class PDFCanvas::Private delete texture; texturesToClean.clear(); } + + void deleteAllTextures() { + // Delete textures that are not stored anymore in any pages. + cleanTextures(); + // Delete textures currently stored by pages. + for (int i = 0; i < pageCount; ++i) { + PDFPage &page = pages[i]; + + if (page.texture) { + page.texture->deleteLater(); + page.texture = nullptr; + } + for (QList::iterator it = page.patches.begin(); + it != page.patches.end(); it++) { + if (it->second) + it->second->deleteLater(); + } + page.patches.clear(); + page.requested = false; + } + } }; @@ -125,15 +161,7 @@ PDFCanvas::PDFCanvas(QQuickItem *parent) PDFCanvas::~PDFCanvas() { - for (int i = 0; i < d->pageCount; ++i) { - PDFPage &page = d->pages[i]; - - if (page.texture) { - page.texture->deleteLater(); - page.texture = nullptr; - } - } - + d->deleteAllTextures(); delete d->resizeTimer; delete d; } @@ -177,6 +205,7 @@ void PDFCanvas::setDocument(PDFDocument *doc) connect(d->document, &PDFDocument::pageFinished, this, &PDFCanvas::pageFinished); connect(d->document, &PDFDocument::pageSizesFinished, this, &PDFCanvas::pageSizesFinished); connect(d->document, &PDFDocument::documentLockedChanged, this, &PDFCanvas::documentLoaded); + connect(d->document, &PDFDocument::pageModified, this, &PDFCanvas::pageModified); if (d->document->isLoaded()) documentLoaded(); @@ -280,6 +309,7 @@ void PDFCanvas::layout() page.renderWidth = d->pages.value(i).renderWidth; page.textureArea = d->pages.value(i).textureArea; page.texture = d->pages.value(i).texture; + page.patches = d->pages.value(i).patches; } d->pages.insert(i, page); @@ -350,6 +380,57 @@ QPair PDFCanvas::urlAtPoint(const QPointF &point) c return QPair(); } +QPair PDFCanvas::annotationAtPoint(const QPointF &point) const +{ + for (int i = 0; i < d->pageCount; ++i) { + const PDFPage &page = d->pages.value(i); + if (page.rect.contains(point)) { + qreal squaredDistanceMin = d->linkWiggle * d->linkWiggle; + Poppler::Annotation *result = nullptr; + QRectF at; + for (Poppler::Annotation *annotation : d->document->annotations(i)) { + switch (annotation->subType()) { + case (Poppler::Annotation::ALink): + // Ignore link annotation for the moment since + // real link are reported as annotation also. + break; + case (Poppler::Annotation::AHighlight): { + QList quads = + static_cast(annotation)->highlightQuads(); + for (QList::iterator quad = quads.begin(); + quad != quads.end(); quad++) { + // Assuming rectangular quad... + qreal squaredDistance = + squaredDistanceFromRect(page.rect, QRectF(quad->points[0], quad->points[2]), point); + + if (squaredDistance < squaredDistanceMin) { + result = annotation; + at = QRectF(quad->points[0], quad->points[2]); + squaredDistanceMin = squaredDistance; + } + } + break; + } + default: { + qreal squaredDistance = + squaredDistanceFromRect(page.rect, annotation->boundary(), point); + + if (squaredDistance < squaredDistanceMin) { + result = annotation; + at = annotation->boundary(); + squaredDistanceMin = squaredDistance; + } + break; + } + } + } + return QPair{result, {i, at}}; + } + } + + return QPair(); +} + QRectF PDFCanvas::fromPageToItem(int index, const QRectF &rect) const { if (index < 0 || index >= d->pageCount) @@ -372,17 +453,51 @@ QPointF PDFCanvas::fromPageToItem(int index, const QPointF &point) const point.y() * page.rect.height() + page.rect.y()); } +void PDFCanvas::pageModified(int id, const QRectF &subpart) +{ + PDFPage &page = d->pages[id]; + + if (subpart.isEmpty()) { + // Ask for a full page redraw in update by deleting + // the current texture of the page. + if (page.texture) { + d->texturesToClean << page.texture; + page.texture = 0; + } + if (page.requested) { + d->document->cancelPageRequest(id); + page.requested = false; + } + update(); + } else { + int buf = 10; + // Ask only for a patch on this page. + QRect request(int(subpart.x() * page.rect.width()) - buf, + int(subpart.y() * page.rect.height()) - buf, + qCeil(subpart.width() * page.rect.width()) + buf * 2, + qCeil(subpart.height() * page.rect.height()) + buf * 2); + d->document->requestPage(id, d->renderWidth, window(), + request, PDFCanvas::Private::PatchTexture); + } +} + void PDFCanvas::pageFinished(int id, int pageRenderWidth, - QRect subpart, QSGTexture *texture) + QRect subpart, QSGTexture *texture, int extraData) { PDFPage &page = d->pages[id]; - d->cleanPageTexturesLater(page); + if (PDFCanvas::Private::TextureType(extraData) == PDFCanvas::Private::RootTexture) { + d->cleanPageTexturesLater(page); - page.renderWidth = pageRenderWidth; - page.textureArea = subpart; - page.texture = texture; - page.requested = false; + page.renderWidth = pageRenderWidth; + page.textureArea = subpart; + page.texture = texture; + page.requested = false; + } else if (pageRenderWidth == page.renderWidth) { + page.patches.append(Patch(subpart, texture)); + } else { + delete texture; + } update(); } @@ -399,12 +514,21 @@ void PDFCanvas::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeom void PDFCanvas::sceneGraphInvalidated() { d->document->cancelPageRequest(-1); - d->cleanTextures(); - for (int i = 0; i < d->pageCount; ++i) { - PDFPage &page = d->pages[i]; - delete page.texture; - page.texture = nullptr; - page.requested = false; + d->deleteAllTextures(); +} + +static void putTexture(QSGSimpleTextureNode *tn, float pageWidth, int renderWidth, + QRect textureArea, QSGTexture *texture) +{ + tn->setTexture(texture); + if (int(pageWidth) == renderWidth) { + tn->setRect(textureArea); + } else { + float ratio = pageWidth / renderWidth; + tn->setRect(int(ratio * textureArea.x()), + int(ratio * textureArea.y()), + qCeil(ratio * textureArea.width()), + qCeil(ratio * textureArea.height())); } } @@ -483,7 +607,8 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa !page.textureArea.contains(showableArea))) { QRect request = (fullPageFit) ? QRect() : textureLimit; if (!page.requested) { - d->document->requestPage(i, d->renderWidth, window(), request); + d->document->requestPage(i, d->renderWidth, window(), + request, PDFCanvas::Private::RootTexture); page.requested = true; } priorityRequests << QPair >(i, QPair(d->renderWidth, request)); @@ -493,7 +618,8 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa textureLimit.moveTo(0, 0); // We preload full page only if they can fit into texture. if (textureLimit.contains(pageRect)) { - d->document->requestPage(i, d->renderWidth, window()); + d->document->requestPage(i, d->renderWidth, window(), + QRect(), PDFCanvas::Private::RootTexture); page.requested = true; } } else if (!loadPage) { @@ -509,6 +635,7 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa QSGTransformNode *t = static_cast(root->childAtIndex(i)); if (!t) { t = new QSGTransformNode; + t->setFlag(QSGNode::OwnedByParent); root->appendChildNode(t); } @@ -532,12 +659,14 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa // t // |-bg // | |-tn + // | | |- patch1... // | |-n // | | |- link1 // | | |- link2... QSGSimpleRectNode *bg = static_cast(t->firstChild()); if (!bg) { bg = new QSGSimpleRectNode; + bg->setFlag(QSGNode::OwnedByParent); bg->setColor(d->pagePlaceholderColor); t->appendChildNode(bg); } @@ -547,28 +676,42 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa QSGSimpleTextureNode *tn = static_cast(bg->firstChild()); if (!tn) { tn = new QSGSimpleTextureNode; + tn->setFlag(QSGNode::OwnedByParent); bg->appendChildNode(tn); } - tn->setTexture(page.texture); - if (int(width()) == page.renderWidth) { - tn->setRect(page.textureArea); + putTexture(tn, width(), page.renderWidth, page.textureArea, page.texture); + + if (!page.patches.empty()) { + QSGSimpleTextureNode *ptn = static_cast(tn->firstChild()); + for (QList::iterator it = page.patches.begin(); + it != page.patches.end(); it++) { + if (!ptn) { + ptn = new QSGSimpleTextureNode; + ptn->setFlag(QSGNode::OwnedByParent); + tn->appendChildNode(ptn); + } + + putTexture(ptn, width(), page.renderWidth, it->first, it->second); + + ptn = static_cast(ptn->nextSibling()); + } } else { - float ratio = width() / page.renderWidth; - tn->setRect(int(ratio * page.textureArea.x()), - int(ratio * page.textureArea.y()), - qCeil(ratio * page.textureArea.width()), - qCeil(ratio * page.textureArea.height())); + // Delete all previously registered patches. + for (QSGNode *child = tn->firstChild(); child; child = tn->firstChild()) + delete child; } QSGNode *n = tn->nextSibling(); if (!n) { n = new QSGNode; + n->setFlag(QSGNode::OwnedByParent); bg->appendChildNode(n); } QSGSimpleRectNode *rn = static_cast(n->firstChild()); for (int l = 0; l < page.links.count(); ++l) { if (!rn) { rn = new QSGSimpleRectNode; + rn->setFlag(QSGNode::OwnedByParent); n->appendChildNode(rn); } QRectF linkRect = page.links.value(l).first; @@ -583,10 +726,12 @@ QSGNode* PDFCanvas::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeDa rn = static_cast(rn->nextSibling()); } + } else { + delete bg->firstChild(); // delete the texture root here. + delete bg->firstChild(); // delete the link root here. } } else { delete t->firstChild(); - t->removeAllChildNodes(); } } diff --git a/pdf/pdfcanvas.h b/pdf/pdfcanvas.h index cfc57102..838ccff4 100644 --- a/pdf/pdfcanvas.h +++ b/pdf/pdfcanvas.h @@ -22,6 +22,8 @@ #include #include +#include + class PDFDocument; class PDFCanvas : public QQuickItem @@ -90,6 +92,7 @@ class PDFCanvas : public QQuickItem */ QPair urlAtPoint(const QPointF &point) const; QPair pageAtPoint(const QPointF &point) const; + QPair annotationAtPoint(const QPointF &point) const; /** * \return A rectangle in the canvas coordinates from a rectangle * in page coordinates. Index is the index of the page. @@ -121,8 +124,9 @@ class PDFCanvas : public QQuickItem virtual QSGNode* updatePaintNode(QSGNode *node, UpdatePaintNodeData*); private Q_SLOTS: + void pageModified(int id, const QRectF &subpart); void pageFinished(int id, int pageRenderWidth, - QRect subpart, QSGTexture *texture); + QRect subpart, QSGTexture *texture, int extraData); void documentLoaded(); void resizeTimeout(); void pageSizesFinished(const QList &sizes); diff --git a/pdf/pdfdocument.cpp b/pdf/pdfdocument.cpp index d6e09ebf..27e3b096 100644 --- a/pdf/pdfdocument.cpp +++ b/pdf/pdfdocument.cpp @@ -29,7 +29,7 @@ class PDFDocument::Private { public: - Private() : searching(false), completed(false) { } + Private() : searching(false), completed(false), modified(false) { } PDFRenderThread *thread; @@ -37,7 +37,9 @@ class PDFDocument::Private PDFSearchModel *searchModel; QString source; + QString autoSavePath; bool completed; + bool modified; }; PDFDocument::PDFDocument(QObject *parent) @@ -49,6 +51,7 @@ PDFDocument::PDFDocument(QObject *parent) connect(d->thread, &PDFRenderThread::loadFinished, this, &PDFDocument::loadFinished); connect(d->thread, &PDFRenderThread::jobFinished, this, &PDFDocument::jobFinished); connect(d->thread, &PDFRenderThread::searchFinished, this, &PDFDocument::searchFinished); + connect(d->thread, &PDFRenderThread::pageModified, this, &PDFDocument::onPageModified); d->searchModel = nullptr; } @@ -65,6 +68,11 @@ QString PDFDocument::source() const return d->source; } +QString PDFDocument::autoSavePath() const +{ + return d->autoSavePath; +} + int PDFDocument::pageCount() const { if (d->thread && d->thread->isLoaded()) { @@ -104,6 +112,11 @@ bool PDFDocument::isLocked() const return d->thread->isLocked(); } +bool PDFDocument::isModified() const +{ + return d->modified; +} + PDFDocument::LinkMap PDFDocument::linkTargets() const { return d->thread->linkTargets(); @@ -144,6 +157,46 @@ void PDFDocument::setSource(const QString &source) } } +void PDFDocument::setAutoSavePath(const QString &filename) +{ + if (d->autoSavePath != filename) { + d->autoSavePath = filename; + if (filename.startsWith("/")) + d->autoSavePath.prepend("file://"); + + emit autoSavePathChanged(); + + if (d->modified) + d->thread->setAutoSaveName(QUrl(d->autoSavePath).toLocalFile()); + } +} + +void PDFDocument::addAnnotation(Poppler::Annotation *annotation, int pageIndex, + bool normalizeSize) +{ + d->thread->addAnnotation(annotation, pageIndex, normalizeSize); +} + +QList PDFDocument::annotations(int page) const +{ + return d->thread->annotations(page); +} + +void PDFDocument::removeAnnotation(Poppler::Annotation *annotation, int pageIndex) +{ + d->thread->removeAnnotation(annotation, pageIndex); +} + +void PDFDocument::setDocumentModified() +{ + if (d->modified) + return; + + d->modified = true; + if (!d->autoSavePath.isEmpty()) + d->thread->setAutoSaveName(QUrl(d->autoSavePath).toLocalFile()); +} + void PDFDocument::requestUnLock(const QString &password) { if (!isLocked()) @@ -153,12 +206,13 @@ void PDFDocument::requestUnLock(const QString &password) d->thread->queueJob(job); } -void PDFDocument::requestPage(int index, int size, QQuickWindow *window, QRect subpart) +void PDFDocument::requestPage(int index, int size, QQuickWindow *window, + QRect subpart, int extraData) { if (!isLoaded() || isLocked()) return; - RenderPageJob* job = new RenderPageJob(index, size, window, subpart); + RenderPageJob* job = new RenderPageJob(index, size, window, subpart, extraData); d->thread->queueJob(job); } @@ -227,6 +281,12 @@ void PDFDocument::loadFinished() emit documentLockedChanged(); } +void PDFDocument::onPageModified(int page, const QRectF &subpart) +{ + setDocumentModified(); + emit pageModified(page, subpart); +} + void PDFDocument::jobFinished(PDFJob *job) { switch(job->type()) { @@ -237,7 +297,8 @@ void PDFDocument::jobFinished(PDFJob *job) } case PDFJob::RenderPageJob: { RenderPageJob* j = static_cast(job); - emit pageFinished(j->m_index, j->renderWidth(), j->m_subpart, j->m_page); + emit pageFinished(j->m_index, j->renderWidth(), j->m_subpart, + j->m_page, j->m_extraData); break; } case PDFJob::PageSizesJob: { diff --git a/pdf/pdfdocument.h b/pdf/pdfdocument.h index 95c61ec4..33df6e61 100644 --- a/pdf/pdfdocument.h +++ b/pdf/pdfdocument.h @@ -34,11 +34,13 @@ class PDFDocument : public QObject, public QQmlParserStatus { Q_OBJECT Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QString autoSavePath READ autoSavePath WRITE setAutoSavePath NOTIFY autoSavePathChanged) Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged) Q_PROPERTY(QObject* tocModel READ tocModel NOTIFY tocModelChanged) Q_PROPERTY(bool loaded READ isLoaded NOTIFY documentLoadedChanged) Q_PROPERTY(bool failure READ isFailed NOTIFY documentFailedChanged) Q_PROPERTY(bool locked READ isLocked NOTIFY documentLockedChanged) + Q_PROPERTY(bool modified READ isModified NOTIFY documentModifiedChanged) Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged) Q_PROPERTY(QObject* searchModel READ searchModel NOTIFY searchModelChanged) @@ -53,6 +55,7 @@ class PDFDocument : public QObject, public QQmlParserStatus typedef QList > TextList; QString source() const; + QString autoSavePath() const; int pageCount() const; QObject* tocModel() const; bool searching() const; @@ -64,14 +67,24 @@ class PDFDocument : public QObject, public QQmlParserStatus bool isLoaded() const; bool isFailed() const; bool isLocked() const; + bool isModified() const; + + void addAnnotation(Poppler::Annotation *annotation, int pageIndex, + bool normalizeSize = false); + QList annotations(int page) const; + void removeAnnotation(Poppler::Annotation *annotation, int pageIndex); + + void setDocumentModified(); virtual void classBegin(); virtual void componentComplete(); public Q_SLOTS: void setSource(const QString &source); + void setAutoSavePath(const QString &filename); void requestUnLock(const QString &password); - void requestPage(int index, int size, QQuickWindow *window, QRect subpart = QRect()); + void requestPage(int index, int size, QQuickWindow *window, + QRect subpart = QRect(), int extraData = 0); void prioritizeRequest(int index, int size, QRect subpart = QRect()); void cancelPageRequest(int index); void requestPageSizes(); @@ -80,18 +93,23 @@ public Q_SLOTS: void searchFinished(const QList> &matches); void loadFinished(); void jobFinished(PDFJob *job); + void onPageModified(int page, const QRectF &subpart); Q_SIGNALS: void sourceChanged(); + void autoSavePathChanged(); void pageCountChanged(); void tocModelChanged(); void searchingChanged(); void searchModelChanged(); + void pageModified(int index, const QRectF &subpart); void documentLoadedChanged(); void documentFailedChanged(); void documentLockedChanged(); - void pageFinished(int index, int resolution, QRect subpart, QSGTexture *page); + void documentModifiedChanged(); + void pageFinished(int index, int resolution, QRect subpart, + QSGTexture *page, int extraData); void pageSizesFinished(const QList &heights); private: diff --git a/pdf/pdfjob.cpp b/pdf/pdfjob.cpp index 80f27cea..2295d6a7 100644 --- a/pdf/pdfjob.cpp +++ b/pdf/pdfjob.cpp @@ -48,8 +48,9 @@ void UnLockDocumentJob::run() m_document->unlock(m_password.toUtf8(), m_password.toUtf8()); } -RenderPageJob::RenderPageJob(int index, uint width, QQuickWindow *window, QRect subpart) - : PDFJob(PDFJob::RenderPageJob), m_index(index), m_subpart(subpart), m_page(0), m_window(window), m_width(width) +RenderPageJob::RenderPageJob(int index, uint width, QQuickWindow *window, + QRect subpart, int extraData) + : PDFJob(PDFJob::RenderPageJob), m_index(index), m_subpart(subpart), m_page(0), m_extraData(extraData), m_window(window), m_width(width) { } @@ -66,7 +67,7 @@ void RenderPageJob::run() image = page->renderToImage(scale, scale); m_subpart.setCoords(0, 0, image.width(), image.height()); } else { - QRect pageRect = {0, 0, (int)m_width, qCeil(size.height() / size.width() * m_width)}; + QRect pageRect = {0, 0, int(m_width), qCeil(size.height() / size.width() * m_width)}; m_subpart = m_subpart.intersected(pageRect); image = page->renderToImage(scale, scale, m_subpart.x(), m_subpart.y(), diff --git a/pdf/pdfjob.h b/pdf/pdfjob.h index d6a3b17e..eb25a63c 100644 --- a/pdf/pdfjob.h +++ b/pdf/pdfjob.h @@ -84,13 +84,15 @@ class RenderPageJob : public PDFJob { Q_OBJECT public: - RenderPageJob(int index, uint width, QQuickWindow *window, QRect subpart = QRect()); + RenderPageJob(int index, uint width, QQuickWindow *window, + QRect subpart = QRect(), int extraData = 0); virtual void run(); int m_index; QRect m_subpart; QSGTexture *m_page; + int m_extraData; int renderWidth() const { return m_width; } void changeRenderWidth(int width) { m_width = width; } diff --git a/pdf/pdflinkarea.cpp b/pdf/pdflinkarea.cpp index 7db17c95..702ca69b 100644 --- a/pdf/pdflinkarea.cpp +++ b/pdf/pdflinkarea.cpp @@ -18,6 +18,7 @@ #include "pdflinkarea.h" #include "pdfcanvas.h" +#include "pdfselection.h" #include #include @@ -26,10 +27,15 @@ class PDFLinkArea::Private public: Private() : canvas(nullptr) + , selection(nullptr) , wiggleFactor(4) + , pressed(false) + , annotation(nullptr) + , clickOnSelection(false) { } PDFCanvas *canvas; + PDFSelection *selection; QPointF clickLocation; int wiggleFactor; @@ -37,7 +43,10 @@ class PDFLinkArea::Private QTimer pressTimer; bool pressed; PDFCanvas::ReducedBox pressedBox; + QUrl link; + Poppler::Annotation *annotation; + bool clickOnSelection; }; PDFLinkArea::PDFLinkArea(QQuickItem *parent) @@ -80,6 +89,19 @@ bool PDFLinkArea::pressed() const return d->pressed; } +PDFSelection* PDFLinkArea::selection() const +{ + return d->selection; +} + +void PDFLinkArea::setSelection(PDFSelection *newSelection) +{ + if (newSelection != d->selection) { + d->selection = newSelection; + emit selectionChanged(); + } +} + QRectF PDFLinkArea::clickedBox() const { if (d->canvas && !d->pressedBox.second.isEmpty()) @@ -101,26 +123,52 @@ void PDFLinkArea::mousePressEvent(QMouseEvent *event) // Nullify all handles. d->pressedBox.second = QRectF(); d->link.clear(); + d->annotation = nullptr; + d->clickOnSelection = false; d->clickLocation = event->pos(); d->pressTimer.start(); if (!d->canvas) return; + + // Click action logic in order. + // - click on selection; + // - unselect if selection is set; + // - click on annotation; + // - click on link; + // - click. + + if (d->selection) + d->clickOnSelection = d->selection->selectionAtPoint(d->clickLocation); + if (d->clickOnSelection || (d->selection && d->selection->count() > 0)) + return; + + QPair annotationAt = + d->canvas->annotationAtPoint(d->clickLocation); + d->annotation = annotationAt.first; + if (annotationAt.first != nullptr) { + d->pressedBox = annotationAt.second; + d->pressed = true; + emit pressedChanged(); + emit clickedBoxChanged(); + return; + } QPair urlAt = d->canvas->urlAtPoint(d->clickLocation); d->link = urlAt.first; if (!d->link.isEmpty()) { d->pressedBox = urlAt.second; + d->pressed = true; + emit pressedChanged(); + emit clickedBoxChanged(); + return; } - - d->pressed = true; - emit pressedChanged(); - emit clickedBoxChanged(); } void PDFLinkArea::mouseMoveEvent(QMouseEvent *event) { + emit positionChanged(QPointF(event->pos())); // Don't activate anything if the finger has moved too far QRect rect((d->clickLocation - QPointF(d->wiggleFactor, d->wiggleFactor)).toPoint(), QSize(d->wiggleFactor * 2, d->wiggleFactor * 2)); @@ -132,11 +180,42 @@ void PDFLinkArea::mouseMoveEvent(QMouseEvent *event) } } +PDFAnnotation* PDFLinkArea::newProxyForAnnotation() +{ + // proxy will be child of this object to avoid memory leak. + // Todo: transfer ownership to QML, if possible. Before that, + // all created proxy objects will be alive up to the moment + // the document object is released. + switch (d->annotation->subType()) { + case Poppler::Annotation::SubType::AText: { + return new PDFTextAnnotation + (static_cast(d->annotation), + d->canvas->document(), d->pressedBox.first, this); + } + case Poppler::Annotation::SubType::ACaret: { + return new PDFCaretAnnotation + (static_cast(d->annotation), + d->canvas->document(), d->pressedBox.first, this); + } + case Poppler::Annotation::SubType::AHighlight: { + return new PDFHighlightAnnotation + (static_cast(d->annotation), + d->canvas->document(), d->pressedBox.first, this); + } + default: { + return new PDFAnnotation + (d->annotation, d->canvas->document(), d->pressedBox.first, this); + } + } +} + void PDFLinkArea::mouseReleaseEvent(QMouseEvent *event) { d->pressed = false; emit pressedChanged(); + emit released(); + // Don't activate click if the longPress already fired. if (!d->pressTimer.isActive()) return; @@ -148,8 +227,21 @@ void PDFLinkArea::mouseReleaseEvent(QMouseEvent *event) if (!rect.contains(event->pos())) return; + // Click action logic in order. + // - click on selection; + // - unselect if selection is set; + // - click on annotation; + // - click on link; + // - click. + if (!d->canvas) { emit clicked(d->clickLocation); + } else if (d->clickOnSelection) { + emit selectionClicked(); + } else if (d->selection && d->selection->count() > 0) { + d->selection->unselect(); + } else if (d->annotation != nullptr) { + emit annotationClicked(newProxyForAnnotation()); } else if (d->link.isEmpty()) { emit clicked(d->clickLocation); } else if (d->link.isRelative() && d->link.hasQuery()) { @@ -183,5 +275,15 @@ void PDFLinkArea::mouseUngrabEvent() void PDFLinkArea::pressTimeout() { - emit longPress(d->clickLocation); + if (!d->canvas) + return; + + if (d->annotation) { + emit annotationLongPress(newProxyForAnnotation()); + } else if (d->selection && d->selection->selectAt(d->clickLocation)) { + return; + } else { + // Generic longPress. + emit longPress(d->clickLocation); + } } diff --git a/pdf/pdflinkarea.h b/pdf/pdflinkarea.h index 2108c50e..c3d18f66 100644 --- a/pdf/pdflinkarea.h +++ b/pdf/pdflinkarea.h @@ -20,8 +20,10 @@ #define LINKLAYER_H #include +#include "pdfannotation.h" class PDFCanvas; +class PDFSelection; class PDFLinkArea : public QQuickItem { @@ -29,6 +31,7 @@ class PDFLinkArea : public QQuickItem Q_PROPERTY(PDFCanvas* canvas READ canvas WRITE setCanvas NOTIFY canvasChanged) Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged) Q_PROPERTY(QRectF clickedBox READ clickedBox NOTIFY clickedBoxChanged) + Q_PROPERTY(PDFSelection* selection READ selection WRITE setSelection NOTIFY selectionChanged) public: PDFLinkArea(QQuickItem *parent = 0); @@ -38,17 +41,25 @@ class PDFLinkArea : public QQuickItem bool pressed() const; QRectF clickedBox() const; void setCanvas(PDFCanvas *newCanvas); + PDFSelection* selection() const; + void setSelection(PDFSelection *newSelection); Q_SIGNALS: void pressedChanged(); void clickedBoxChanged(); + void positionChanged(QPointF at); + void released(); void clicked(QPointF clickAt); void doubleClicked(); void linkClicked(QUrl linkTarget); void gotoClicked(int page, qreal top, qreal left); + void selectionClicked(); + void annotationClicked(PDFAnnotation *annotation); + void annotationLongPress(PDFAnnotation *annotation); void longPress(QPointF pressAt); void canvasChanged(); + void selectionChanged(); protected: virtual void mousePressEvent(QMouseEvent *event); @@ -64,6 +75,8 @@ private Q_SLOTS: private: class Private; Private *d; + + PDFAnnotation* newProxyForAnnotation(); }; #endif // LINKLAYER_H diff --git a/pdf/pdfrenderthread.cpp b/pdf/pdfrenderthread.cpp index 022584f7..db8d5171 100644 --- a/pdf/pdfrenderthread.cpp +++ b/pdf/pdfrenderthread.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "pdfjob.h" #include "pdftocmodel.h" @@ -112,6 +113,7 @@ class Thread : public QThread QThread::exec(); // Delete pending search that may use document. delete searchThread; + autoSaveTo(); delete document; delete tocModel; deleteLater(); @@ -129,9 +131,13 @@ class Thread : public QThread QMutex mutex; // Used for cleanup only + QString autoSaveFilename; Poppler::Document *document; PDFTocModel *tocModel; SearchThread *searchThread; + +private: + void autoSaveTo(); }; class PDFRenderThreadPrivate @@ -149,6 +155,10 @@ class PDFRenderThreadPrivate delete j->second; } } + for (QMap >::iterator i = annotations.begin(); + i != annotations.end(); i++) { + qDeleteAll(i.value()); + } } @@ -163,6 +173,7 @@ class PDFRenderThreadPrivate QMultiMap > linkTargets; QMap > > textBoxes; + QMap > annotations; void rescanDocumentLinks() { @@ -234,6 +245,21 @@ class PDFRenderThreadPrivate textBoxes.insert(i, lst); delete page; } + void retrieveAnnotations(int i) { + if (i < 0 || i >= document->numPages()) { + return; + } + if (annotations.contains(i)) + qDeleteAll(annotations.take(i)); + Poppler::Page* page = document->page(i); + QList annotationsAt = page->annotations(); + // Add all revisions of every annotation we just retrieved. + int nFirstLevel = annotationsAt.size(); + for (int j = 0; j < nFirstLevel; j++) + annotationsAt += annotationsAt[j]->revisions(); + annotations.insert(i, annotationsAt); + delete(page); + } }; class PDFRenderThreadQueue : public QObject, public QQueue @@ -325,6 +351,58 @@ QList > PDFRenderThread::textBoxesAtPage(int pa return d->textBoxes[page]; } +void PDFRenderThread::addAnnotation(Poppler::Annotation *annotation, int pageIndex, + bool normalizeSize) +{ + QMutexLocker(&d->thread->mutex); + if (!d->document) + return; + Poppler::Page *page = d->document->page(pageIndex); + if (normalizeSize) { + QSizeF pSize = page->pageSizeF(); + QRectF bounds = annotation->boundary(); + bounds.setWidth(bounds.width() / pSize.width()); + bounds.setHeight(bounds.height() / pSize.height()); + annotation->setBoundary(bounds); + } + page->addAnnotation(annotation); + delete page; + // annotation cannot be added to d->annotations of this page + // since the caller is the owner of the object. + if (d->annotations.contains(pageIndex)) + d->retrieveAnnotations(pageIndex); + emit pageModified(pageIndex, annotation->boundary()); +} + +QList PDFRenderThread::annotations(int pageIndex) const +{ + QMutexLocker(&d->thread->mutex); + if (!d->document) + return QList(); + if (!d->annotations.contains(pageIndex)) + d->retrieveAnnotations(pageIndex); + return d->annotations[pageIndex]; +} + +void PDFRenderThread::removeAnnotation(Poppler::Annotation *annotation, int pageIndex) +{ + QMutexLocker(&d->thread->mutex); + if (!d->document) + return; + if (d->annotations.contains(pageIndex)) + d->annotations[pageIndex].removeOne(annotation); + Poppler::Page *page = d->document->page(pageIndex); + page->removeAnnotation(annotation); + delete page; + emit pageModified(pageIndex, annotation->boundary()); +} + +void PDFRenderThread::setAutoSaveName(const QString &filename) +{ + QMutexLocker(&d->thread->mutex); + d->thread->autoSaveFilename = filename; +} + void PDFRenderThread::queueJob(PDFJob *job) { QMutexLocker locker(&d->thread->mutex); @@ -459,4 +537,27 @@ bool PDFRenderThreadQueue::event(QEvent *e) return QObject::event(e); } +void Thread::autoSaveTo() +{ + QMutexLocker locker(&mutex); + + if (autoSaveFilename.isEmpty()) + return; + + Q_ASSERT(document); + + QSaveFile destination(autoSaveFilename); + + Poppler::PDFConverter *converter = document->pdfConverter(); + converter->setOutputDevice(&destination); + converter->setPDFOptions(Poppler::PDFConverter::PDFOption::WithChanges); + bool success = converter->convert(); + delete converter; + + if (success) + destination.commit(); + else + qWarning() << QStringLiteral("PDF exportation failure to '%1' (error code %2)").arg(autoSaveFilename).arg(int(converter->lastError())); +} + #include "pdfrenderthread.moc" diff --git a/pdf/pdfrenderthread.h b/pdf/pdfrenderthread.h index 5cb95713..1b45598c 100644 --- a/pdf/pdfrenderthread.h +++ b/pdf/pdfrenderthread.h @@ -47,12 +47,20 @@ class PDFRenderThread : public QObject void search(const QString &search, uint startPage); void cancelSearch(); + void addAnnotation(Poppler::Annotation *annotation, int pageIndex, + bool normalizeSize); + QList annotations(int pageIndex) const; + void removeAnnotation(Poppler::Annotation *annotation, int pageIndex); + + void setAutoSaveName(const QString &filename); + void queueJob(PDFJob *job); void cancelRenderJob(int index); void prioritizeRenderJob(int index, int size, QRect subpart); Q_SIGNALS: void loadFinished(); + void pageModified(int page, const QRectF &subpart); void jobFinished(PDFJob *job); void searchFinished(const QList> &matches); diff --git a/pdf/pdfselection.cpp b/pdf/pdfselection.cpp index b54eea58..55b142b9 100644 --- a/pdf/pdfselection.cpp +++ b/pdf/pdfselection.cpp @@ -515,7 +515,7 @@ void PDFSelection::setHandle2(const QPointF &point) setStop(point); } -void PDFSelection::selectAt(const QPointF &point) +bool PDFSelection::selectAt(const QPointF &point) { if (d->pageIndexStart >= 0 && d->boxIndexStart >= 0 && d->pageIndexStop >= 0 && d->boxIndexStop >= 0) { @@ -525,7 +525,7 @@ void PDFSelection::selectAt(const QPointF &point) int pageIndex, boxIndex; d->textBoxAtPoint(point, PDFSelection::Private::At, &pageIndex, &boxIndex); if (pageIndex < 0 || boxIndex < 0) - return; + return false; const PDFDocument::TextList &boxes = d->canvas->document()->textBoxesAtPage(pageIndex); QRectF box = boxes[boxIndex].first; @@ -546,6 +546,8 @@ void PDFSelection::selectAt(const QPointF &point) emit countChanged(); emit textChanged(); + + return true; } void PDFSelection::unselect() @@ -570,6 +572,34 @@ void PDFSelection::onLayoutChanged() QVector{Rect}); } +bool PDFSelection::selectionAtPoint(const QPointF &point) const +{ + if (d->pageIndexStart < 0 || d->pageIndexStop < 0 + || d->boxIndexStart < 0 || d->boxIndexStop < 0) { + return false; + } + + if (!d->canvas) + return false; + PDFDocument *doc = d->canvas->document(); + if (!doc) + return false; + + QPair at = d->canvas->pageAtPoint(point); + if (at.first < 0) + return false; + QPointF reducedCoordPoint {point.x() / at.second.width(), + (point.y() - at.second.y()) / at.second.height()}; + const PDFDocument::TextList &boxes = doc->textBoxesAtPage(at.first); + for (int i = ((at.first == d->pageIndexStart) ? d->boxIndexStart : 0); + i < ((at.first == d->pageIndexStop) ? d->boxIndexStop + 1 : boxes.length()); + i++) { + if (boxes.value(i).first.contains(reducedCoordPoint)) + return true; + } + return false; +} + QString PDFSelection::text() const { QString out; diff --git a/pdf/pdfselection.h b/pdf/pdfselection.h index b53b74c7..929e5670 100644 --- a/pdf/pdfselection.h +++ b/pdf/pdfselection.h @@ -60,9 +60,14 @@ class PDFSelection : public QAbstractListModel * If there is no word at point, the selection is invalidated (ie. count is set to * zero). */ - Q_INVOKABLE void selectAt(const QPointF &point); + Q_INVOKABLE bool selectAt(const QPointF &point); Q_INVOKABLE void unselect(); + /** + * Check if point is inside selection. + */ + bool selectionAtPoint(const QPointF &point) const; + /** * Return a point for the start handle of the selection in canvas coordinates. * This handle can be dragged later and become the stop handle. diff --git a/pdf/sailfishofficepdfplugin.cpp b/pdf/sailfishofficepdfplugin.cpp index 057a39e6..42c9b76d 100644 --- a/pdf/sailfishofficepdfplugin.cpp +++ b/pdf/sailfishofficepdfplugin.cpp @@ -22,6 +22,7 @@ #include "pdfcanvas.h" #include "pdflinkarea.h" #include "pdfselection.h" +#include "pdfannotation.h" SailfishOfficePDFPlugin::SailfishOfficePDFPlugin(QObject *parent) : QQmlExtensionPlugin(parent) @@ -35,4 +36,8 @@ void SailfishOfficePDFPlugin::registerTypes(const char *uri) qmlRegisterType(uri, 1, 0, "Canvas"); qmlRegisterType(uri, 1, 0, "LinkArea"); qmlRegisterType(uri, 1, 0, "Selection"); + qmlRegisterType(uri, 1, 0, "Annotation"); + qmlRegisterType(uri, 1, 0, "TextAnnotation"); + qmlRegisterType(uri, 1, 0, "CaretAnnotation"); + qmlRegisterType(uri, 1, 0, "HighlightAnnotation"); } diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt index 8ed4eb13..df180dea 100755 --- a/plugin/CMakeLists.txt +++ b/plugin/CMakeLists.txt @@ -14,6 +14,11 @@ install(FILES ContextMenuHook.qml DocumentPage.qml DocumentsSharingList.qml + PDFAnnotationEdit.qml + PDFAnnotationNew.qml + PDFContextMenuHighlight.qml + PDFContextMenuLinks.qml + PDFContextMenuText.qml PDFDocumentPage.qml PDFDocumentToCPage.qml PDFView.qml diff --git a/plugin/ContextMenuHook.qml b/plugin/ContextMenuHook.qml index ac13a70f..669d6ffa 100644 --- a/plugin/ContextMenuHook.qml +++ b/plugin/ContextMenuHook.qml @@ -30,13 +30,13 @@ Item { property real _flickableContentHeight property bool _opened: _menu ? _menu._open : false - property variant _menu + property var _menu // Used to emulate the MouseArea that trigger a ContextMenu property bool pressed: true property bool preventStealing - signal positionChanged() - signal released() + signal positionChanged(point mouse) + signal released(bool mouse) function showMenu(menu) { _menu = menu diff --git a/plugin/PDFAnnotationEdit.qml b/plugin/PDFAnnotationEdit.qml new file mode 100644 index 00000000..393060f9 --- /dev/null +++ b/plugin/PDFAnnotationEdit.qml @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 as PDF + +Page { + id: root + + property variant annotation + + property bool _isText: annotation && (annotation.type == PDF.Annotation.Text + || annotation.type == PDF.Annotation.Caret) + + signal remove() + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: content.height + + PullDownMenu { + MenuItem { + //% "Delete" + text: qsTrId("sailfish-office-mi-delete-annotation") + onClicked: root.remove() + } + } + + Column { + id: content + width: parent.width + PageHeader { + id: pageHeader + title: annotation && annotation.author != "" + ? annotation.author + : (_isText + //% "Note" + ? qsTrId("sailfish-office-hd-text-annotation") + //% "Comment" + : qsTrId("sailfish-office-hd-comment-annotation")) + } + TextArea { + id: areaContents + width: parent.width + height: Math.max(flickable.height - pageHeader.height, implicitHeight) + background: null + focus: false + text: annotation ? annotation.contents : "" + placeholderText: _isText + //% "Write a note…" + ? qsTrId("sailfish-office-ta-text-annotation-edit") + //% "Write a comment…" + : qsTrId("sailfish-office-ta-comment-annotation-edit") + onTextChanged: { + if (annotation) { + annotation.contents = text + } + } + } + } + VerticalScrollDecorator { flickable: flickable } + } +} diff --git a/plugin/PDFAnnotationNew.qml b/plugin/PDFAnnotationNew.qml new file mode 100644 index 00000000..fd611730 --- /dev/null +++ b/plugin/PDFAnnotationNew.qml @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Dialog { + id: root + property alias text: areaContents.text + property bool isTextAnnotation + + SilicaFlickable { + id: flickable + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + DialogHeader { + id: dialogHeader + //% "Save" + acceptText: qsTrId("sailfish-office-he-txt-anno-save") + //% "Cancel" + cancelText: qsTrId("sailfish-office-he-txt-anno-cancel") + } + TextArea { + id: areaContents + width: parent.width + height: Math.max(flickable.height - dialogHeader.height, implicitHeight) + placeholderText: isTextAnnotation + //% "Write a note…" + ? qsTrId("sailfish-office-ta-text-annotation") + //% "Write a comment…" + : qsTrId("sailfish-office-ta-comment-annotation") + background: null + focus: true + } + } + VerticalScrollDecorator { flickable: flickable } + } +} diff --git a/plugin/PDFContextMenuHighlight.qml b/plugin/PDFContextMenuHighlight.qml new file mode 100644 index 00000000..5d30eccc --- /dev/null +++ b/plugin/PDFContextMenuHighlight.qml @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 + +ContextMenu { + id: contextMenuHighlight + property Annotation annotation + + InfoLabel { + id: infoContents + visible: infoContents.text != "" + width: parent.width + height: implicitHeight + 2 * Theme.paddingSmall + font.pixelSize: Theme.fontSizeSmall + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 2 + color: Theme.highlightColor + opacity: .6 + text: { + if (contextMenuHighlight.annotation + && contextMenuHighlight.annotation.contents != "") { + return (contextMenuHighlight.annotation.author != "" + ? "(" + contextMenuHighlight.annotation.author + ") " : "") + + contextMenuHighlight.annotation.contents + } else { + return "" + } + } + } + Row { + height: Theme.itemSizeExtraSmall + Repeater { + id: colors + model: ["#db431c", "#ffff00", "#8afa72", "#00ffff", + "#3828f9", "#a328c7", "#ffffff", "#989898", + "#000000"] + delegate: Rectangle { + width: contextMenuHighlight.width / colors.model.length + height: parent.height + color: modelData + MouseArea { + anchors.fill: parent + onClicked: { + contextMenuHighlight.hide() + contextMenuHighlight.annotation.color = color + highlightColorConfig.value = modelData + } + } + } + } + } + Row { + height: Theme.itemSizeExtraSmall + Repeater { + id: styles + model: [{"style": HighlightAnnotation.Highlight, + "label": "abc"}, + {"style": HighlightAnnotation.Squiggly, + "label": "a̰b̰c̰"}, + {"style": HighlightAnnotation.Underline, + "label": "abc"}, + {"style": HighlightAnnotation.StrikeOut, + "label": "abc"}] + delegate: BackgroundItem { + id: bgStyle + width: contextMenuHighlight.width / styles.model.length + height: parent.height + onClicked: { + contextMenuHighlight.hide() + contextMenuHighlight.annotation.style = modelData["style"] + highlightStyleConfig.value = highlightStyleConfig.fromEnum(modelData["style"]) + } + Label { + anchors.centerIn: parent + text: modelData["label"] + textFormat: Text.RichText + color: bgStyle.highlighted + || (contextMenuHighlight.annotation + && contextMenuHighlight.annotation.style == modelData["style"]) + ? Theme.highlightColor : Theme.primaryColor + Rectangle { + visible: modelData["style"] == HighlightAnnotation.Highlight + anchors.fill: parent + color: bgStyle.highlighted ? Theme.highlightColor : Theme.primaryColor + opacity: 0.4 + z: -1 + } + } + } + } + } + MenuItem { + visible: contextMenuHighlight.annotation + text: contextMenuHighlight.annotation + && contextMenuHighlight.annotation.contents == "" + //% "Add a comment" + ? qsTrId("sailfish-office-me-pdf-hl-anno-comment") + //% "Edit the comment" + : qsTrId("sailfish-office-me-pdf-hl-anno-comment-edit") + onClicked: { + if (contextMenuHighlight.annotation.contents == "") { + pdfDocument.create(contextMenuHighlight.annotation) + } else { + pdfDocument.edit(contextMenuHighlight.annotation) + } + } + } + MenuItem { + //% "Clear" + text: qsTrId("sailfish-office-me-pdf-hl-anno-clear") + onClicked: contextMenuHighlight.annotation.remove() + } +} diff --git a/plugin/PDFContextMenuLinks.qml b/plugin/PDFContextMenuLinks.qml new file mode 100644 index 00000000..da2126c4 --- /dev/null +++ b/plugin/PDFContextMenuLinks.qml @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +ContextMenu { + id: contextMenuLinks + property alias url: linkTarget.text + + InfoLabel { + id: linkTarget + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 4 + color: Theme.highlightColor + opacity: .6 + } + MenuItem { + text: (contextMenuLinks.url.indexOf("http:") === 0 + || contextMenuLinks.url.indexOf("https:") === 0) + //% "Open in browser" + ? qsTrId("sailfish-office-me-pdf-open-browser") + //% "Open in external application" + : qsTrId("sailfish-office-me-pdf-open-external") + onClicked: Qt.openUrlExternally(contextMenuLinks.url) + } + MenuItem { + //% "Copy to clipboard" + text: qsTrId("sailfish-office-me-pdf-copy-link") + onClicked: Clipboard.text = contextMenuLinks.url + } +} diff --git a/plugin/PDFContextMenuText.qml b/plugin/PDFContextMenuText.qml new file mode 100644 index 00000000..b4d6a6cd --- /dev/null +++ b/plugin/PDFContextMenuText.qml @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import Sailfish.Silica 1.0 +import Sailfish.Office.PDF 1.0 + +ContextMenu { + id: contextMenuText + property Annotation annotation + property point at + + MenuItem { + visible: !contextMenuText.annotation + //% "Add note" + text: qsTrId("sailfish-office-me-pdf-txt-anno-add") + onClicked: { + var annotation = textComponent.createObject(contextMenuText) + annotation.color = "#202020" + pdfDocument.create(annotation, + function() { + var at = view.getPositionAt(contextMenuText.at) + annotation.attachAt(pdfDocument, at[0], at[2], at[1]) + }) + } + Component { + id: textComponent + TextAnnotation { } + } + } + MenuItem { + visible: contextMenuText.annotation + //% "Edit" + text: qsTrId("sailfish-office-me-pdf-txt-anno-edit") + onClicked: pdfDocument.edit(contextMenuText.annotation) + } + MenuItem { + visible: contextMenuText.annotation + //% "Delete" + text: qsTrId("sailfish-office-me-pdf-txt-anno-clear") + onClicked: contextMenuText.annotation.remove() + } +} diff --git a/plugin/PDFDocumentPage.qml b/plugin/PDFDocumentPage.qml index 66735515..71e3b650 100644 --- a/plugin/PDFDocumentPage.qml +++ b/plugin/PDFDocumentPage.qml @@ -20,6 +20,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import Sailfish.Office.PDF 1.0 as PDF import org.nemomobile.configuration 1.0 +import org.nemomobile.notifications 1.0 import QtQuick.LocalStorage 2.0 import "PDFStorage.js" as PDFStorage @@ -27,6 +28,9 @@ DocumentPage { id: base property var _settings // Handle save and restore the view settings using PDFStorage + property ContextMenu contextMenuLinks + property ContextMenu contextMenuText + property ContextMenu contextMenuHighlight busy: (!pdfDocument.loaded && !pdfDocument.failure) || pdfDocument.searching source: pdfDocument.source @@ -105,33 +109,132 @@ DocumentPage { onClicked: base.open = !base.open onLinkClicked: { base.open = false + if (!contextMenuLinks) { + contextMenuLinks = contextMenuLinksComponent.createObject(base) + } contextMenuLinks.url = linkTarget hook.showMenu(contextMenuLinks) } - clip: anchors.bottomMargin > 0 + onAnnotationClicked: { + base.open = false + switch (annotation.type) { + case PDF.Annotation.Highlight: + if (!contextMenuHighlight) { + contextMenuHighlight = contextMenuHighlightComponent.createObject(base) + } + contextMenuHighlight.annotation = annotation + hook.showMenu(contextMenuHighlight) + break + case PDF.Annotation.Caret: + case PDF.Annotation.Text: + pdfDocument.edit(annotation) + break + default: + } + } + onAnnotationLongPress: { + base.open = false + switch (annotation.type) { + case PDF.Annotation.Highlight: + if (!contextMenuHighlight) { + contextMenuHighlight = contextMenuHighlightComponent.createObject(base) + } + contextMenuHighlight.annotation = annotation + hook.showMenu(contextMenuHighlight) + break + case PDF.Annotation.Caret: + case PDF.Annotation.Text: + if (!contextMenuText) { + contextMenuText = contextMenuTextComponent.createObject(base) + } + contextMenuText.annotation = annotation + hook.showMenu(contextMenuText) + break + default: + } + } + onLongPress: { + base.open = false + if (!contextMenuText) { + contextMenuText = contextMenuTextComponent.createObject(base) + } + contextMenuText.at = pressAt + contextMenuText.annotation = null + hook.showMenu(contextMenuText) + } + clip: anchors.bottomMargin > 0 || base.status !== PageStatus.Active } ToolBar { id: toolbar + property Notification notice + width: parent.width - height: base.orientation == Orientation.Portrait || base.orientation == Orientation.InvertedPortrait + height: base.orientation == Orientation.Portrait + || base.orientation == Orientation.InvertedPortrait ? Theme.itemSizeLarge : Theme.itemSizeSmall anchors.top: view.bottom flickable: view forceHidden: base.open || pdfDocument.failure || pdfDocument.locked + || (contextMenuLinks && contextMenuLinks.active) + || (contextMenuHighlight && contextMenuHighlight.active) + || (contextMenuText && contextMenuText.active) autoShowHide: !row.active + function noticeShow(message) { + if (!notice) { + notice = noticeComponent.createObject(toolbar) + } + notice.show(message) + } + + Connections { + target: view.selection + onSelectedChanged: if (view.selection.selected) toolbar.show() + } + + Component { + id: noticeComponent + Notification { + property bool published + function show(info) { + previewSummary = info + if (published) close() + publish() + published = true + } + function hide() { + if (published) close() + published = false + } + } + } + // Toolbar contain. Row { id: row property bool active: pageCount.highlighted || search.highlighted || !search.iconized + || textButton.highlighted + || highlightButton.highlighted + || view.selection.selected + property Item activeItem property real itemWidth: toolbar.width / children.length height: parent.height + function toggle(item) { + if (toolbar.notice) toolbar.notice.hide() + view.selection.unselect() + if (row.activeItem === item) { + row.activeItem = null + } else { + row.activeItem = item + } + } + SearchBarItem { id: search width: toolbar.width @@ -144,6 +247,119 @@ DocumentPage { onRequestPreviousMatch: view.prevSearchMatch() onRequestNextMatch: view.nextSearchMatch() onRequestCancel: pdfDocument.cancelSearch() + onClicked: row.toggle(search) + } + BackgroundItem { + id: textTool + property bool first: true + + width: row.itemWidth + height: parent.height + highlighted: pressed || textButton.pressed + onClicked: { + row.toggle(textTool) + if (textTool.first) { + //% "Tap where you want to add a note" + toolbar.noticeShow(qsTrId("sailfish-office-la-notice-anno-text")) + textTool.first = false + } + } + IconButton { + id: textButton + anchors.centerIn: parent + highlighted: pressed || textTool.pressed || row.activeItem === textTool + icon.source: "image://theme/icon-m-notifications" + onClicked: textTool.clicked(mouse) + } + MouseArea { + parent: row.activeItem === textTool ? view : null + anchors.fill: parent + onClicked: { + var annotation = textComponent.createObject(textTool) + var pt = Qt.point(view.contentX + mouse.x, + view.contentY + mouse.y) + pdfDocument.create(annotation, + function() { + var at = view.getPositionAt(pt) + annotation.attachAt(pdfDocument, + at[0], at[2], at[1]) + }) + row.toggle(textTool) + } + Component { + id: textComponent + PDF.TextAnnotation { } + } + } + } + BackgroundItem { + id: highlightTool + property bool first: true + + function highlightSelection() { + var anno = highlightComponent.createObject(highlightTool) + anno.color = highlightColorConfig.value + anno.style = highlightStyleConfig.toEnum(highlightStyleConfig.value) + anno.attach(pdfDocument, view.selection) + toolbar.hide() + } + + width: row.itemWidth + height: parent.height + highlighted: pressed || highlightButton.pressed + onClicked: { + if (view.selection.selected) { + highlightSelection() + view.selection.unselect() + return + } + row.toggle(highlightTool) + if (highlightTool.first) { + //% "Tap and move your finger over the area" + toolbar.noticeShow(qsTrId("sailfish-office-la-notice-anno-highlight")) + highlightTool.first = false + } + } + + Component { + id: highlightComponent + PDF.HighlightAnnotation { } + } + + IconButton { + id: highlightButton + anchors.centerIn: parent + highlighted: pressed || highlightTool.pressed || row.activeItem === highlightTool + icon.source: "image://theme/icon-m-edit" + onClicked: highlightTool.clicked(mouse) + } + MouseArea { + parent: row.activeItem === highlightTool ? view : null + anchors.fill: parent + preventStealing: true + onPressed: { + view.selection.selectAt(Qt.point(view.contentX + mouse.x, + view.contentY + mouse.y)) + } + onPositionChanged: { + if (view.selection.count < 1) { + view.selection.selectAt(Qt.point(view.contentX + mouse.x, + view.contentY + mouse.y)) + } else { + view.selection.handle2 = Qt.point(view.contentX + mouse.x, + view.contentY + mouse.y) + } + } + onReleased: { + if (view.selection.selected) highlightTool.highlightSelection() + row.toggle(highlightTool) + } + Binding { + target: view + property: "selectionDraggable" + value: row.activeItem !== highlightTool + } + } } BackgroundItem { id: pageCount @@ -155,43 +371,37 @@ DocumentPage { color: pageCount.highlighted ? Theme.highlightColor : Theme.primaryColor text: view.currentPage + " | " + view.document.pageCount } - onClicked: base.pushAttachedPage() + onClicked: { + row.toggle(pageCount) + base.pushAttachedPage() + } } } } - ContextMenu { - id: contextMenuLinks - property alias url: linkTarget.text - - InfoLabel { - id: linkTarget - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - elide: Text.ElideRight - maximumLineCount: 4 - color: Theme.highlightColor - opacity: .6 - } - MenuItem { - text: (contextMenuLinks.url.indexOf("http:") === 0 - || contextMenuLinks.url.indexOf("https:") === 0) - //% "Open in browser" - ? qsTrId("sailfish-office-me-pdf-open-browser") - //% "Open in external application" - : qsTrId("sailfish-office-me-pdf-open-external") - onClicked: Qt.openUrlExternally(contextMenuLinks.url) - } - MenuItem { - //% "Copy to clipboard" - text: qsTrId("sailfish-office-me-pdf-copy-link") - onClicked: Clipboard.text = contextMenuLinks.url - } - } - PDF.Document { id: pdfDocument source: base.path + autoSavePath: base.path + + function create(annotation, callback) { + var isText = (annotation.type == PDF.Annotation.Text + || annotation.type == PDF.Annotation.Caret) + var dialog = pageStack.push(Qt.resolvedUrl("PDFAnnotationNew.qml"), + {"isTextAnnotation": isText}) + dialog.accepted.connect(function() { + annotation.contents = dialog.text + }) + if (callback !== undefined) dialog.accepted.connect(callback) + } + function edit(annotation) { + var edit = pageStack.push(Qt.resolvedUrl("PDFAnnotationEdit.qml"), + {"annotation": annotation}) + edit.remove.connect(function() { + pageStack.pop() + annotation.remove() + }) + } } Component { @@ -244,12 +454,65 @@ DocumentPage { } } + Component { + id: contextMenuLinksComponent + PDFContextMenuLinks { } + } + + Component { + id: contextMenuTextComponent + PDFContextMenuText { } + } + + Component { + id: contextMenuHighlightComponent + PDFContextMenuHighlight { } + } + ConfigurationValue { id: rememberPositionConfig key: "/apps/sailfish-office/settings/rememberPosition" defaultValue: true } + ConfigurationValue { + id: highlightColorConfig + key: "/apps/sailfish-office/settings/highlightColor" + defaultValue: "#ffff00" + } + ConfigurationValue { + id: highlightStyleConfig + key: "/apps/sailfish-office/settings/highlightStyle" + defaultValue: "highlight" + + function toEnum(configVal) { + if (configVal == "highlight") { + return PDF.HighlightAnnotation.Highlight + } else if (configVal == "squiggly") { + return PDF.HighlightAnnotation.Squiggly + } else if (configVal == "underline") { + return PDF.HighlightAnnotation.Underline + } else if (configVal == "strike") { + return PDF.HighlightAnnotation.StrikeOut + } else { + return PDF.HighlightAnnotation.Highlight + } + } + function fromEnum(enumVal) { + switch (enumVal) { + case PDF.HighlightAnnotation.Highlight: + return "highlight" + case PDF.HighlightAnnotation.Squiggly: + return "squiggly" + case PDF.HighlightAnnotation.Underline: + return "underline" + case PDF.HighlightAnnotation.StrikeOut: + return "strike" + default: + return "highlight" + } + } + } Timer { id: updateSourceSizeTimer diff --git a/plugin/PDFSelectionView.qml b/plugin/PDFSelectionView.qml index fff97fc8..a690b9c1 100644 --- a/plugin/PDFSelectionView.qml +++ b/plugin/PDFSelectionView.qml @@ -23,6 +23,7 @@ Repeater { id: root property Item flickable + property bool draggable: true property alias dragHandle1: handle1.dragged property alias dragHandle2: handle2.dragged @@ -40,6 +41,7 @@ Repeater { children: [ PDFSelectionHandle { id: handle1 + visible: root.draggable attachX: root.flickable !== undefined ? flickable.contentX : handle.x - Theme.itemSizeExtraLarge @@ -48,6 +50,7 @@ Repeater { }, PDFSelectionHandle { id: handle2 + visible: root.draggable attachX: root.flickable !== undefined ? flickable.contentX + flickable.width : handle.x + Theme.itemSizeExtraLarge diff --git a/plugin/PDFView.qml b/plugin/PDFView.qml index 13bae35d..475db795 100644 --- a/plugin/PDFView.qml +++ b/plugin/PDFView.qml @@ -31,12 +31,18 @@ SilicaFlickable { property alias itemHeight: pdfCanvas.height property alias document: pdfCanvas.document property alias currentPage: pdfCanvas.currentPage + property alias selection: pdfSelection + property alias selectionDraggable: selectionView.draggable property bool scaled: pdfCanvas.width != width property QtObject _feedbackEffect signal clicked() signal linkClicked(string linkTarget, Item hook) + signal selectionClicked(variant selection, Item hook) + signal annotationClicked(variant annotation, Item hook) + signal annotationLongPress(variant annotation, Item hook) + signal longPress(point pressAt, Item hook) signal pageSizesReady() signal updateSize(real newWidth, real newHeight) @@ -138,7 +144,7 @@ SilicaFlickable { } PDF.Selection { - id: selection + id: pdfSelection property bool dragging: drag1.pressed || drag2.pressed property bool selected: count > 0 @@ -214,17 +220,20 @@ SilicaFlickable { } canvas: pdfCanvas + selection: pdfSelection + onLinkClicked: base.linkClicked(linkTarget, contextHook) onGotoClicked: base.goToPage(page - 1, top, left, Theme.paddingLarge, Theme.paddingLarge) - onClicked: { - if (selection.text.length > 0) { - selection.unselect() - } else { - base.clicked() - } + onSelectionClicked: base.selectionClicked(selection, contextHook) + onAnnotationClicked: base.annotationClicked(annotation, contextHook) + onClicked: base.clicked() + onAnnotationLongPress: base.annotationLongPress(annotation, contextHook) + onLongPress: { + contextHook.y = pressAt.y + contextHook.hookHeight = Theme.itemSizeSmall / 2 + base.longPress(pressAt, contextHook) } - onLongPress: selection.selectAt(pressAt) } } @@ -267,7 +276,8 @@ SilicaFlickable { } PDFSelectionView { - model: selection + id: selectionView + model: pdfSelection flickable: base dragHandle1: drag1.pressed dragHandle2: drag2.pressed @@ -275,19 +285,29 @@ SilicaFlickable { } PDFSelectionDrag { id: drag1 - visible: selection.selected + visible: pdfSelection.selected && selectionView.draggable flickable: base - handle: selection.handle1 - onDragged: selection.handle1 = at + handle: pdfSelection.handle1 + onDragged: pdfSelection.handle1 = at } PDFSelectionDrag { id: drag2 - visible: selection.selected + visible: pdfSelection.selected && selectionView.draggable flickable: base - handle: selection.handle2 - onDragged: selection.handle2 = at + handle: pdfSelection.handle2 + onDragged: pdfSelection.handle2 = at + } + ContextMenuHook { + id: contextHook + Connections { + target: linkArea + onPositionChanged: if (contextHook.active) { + var local = linkArea.mapToItem(contextHook, at.x, at.y) + contextHook.positionChanged(Qt.point(local.x, local.y)) + } + onReleased: if (contextHook.active) contextHook.released(true) + } } - ContextMenuHook { id: contextHook } } children: [ @@ -333,4 +353,16 @@ SilicaFlickable { var left = (contentX - rect.x) / rect.width return [i, top, left] } + function getPositionAt(at) { + // Find the page that contains at + var i = Math.max(0, currentPage - 2) + var rect = pdfCanvas.pageRectangle( i ) + while ((rect.y + rect.height) < at.y + && i < pdfCanvas.document.pageCount) { + rect = pdfCanvas.pageRectangle( ++i ) + } + var top = Math.max(0, at.y - rect.y) / rect.height + var left = (at.x - rect.x) / rect.width + return [i, top, left] + } } diff --git a/plugin/SearchBarItem.qml b/plugin/SearchBarItem.qml index 036d9ab4..f3aeab75 100644 --- a/plugin/SearchBarItem.qml +++ b/plugin/SearchBarItem.qml @@ -93,7 +93,7 @@ BackgroundItem { highlighted: down || root.down || searchField.activeFocus onClicked: { - root.iconized = false + root.clicked(mouse) searchField.forceActiveFocus() } } @@ -241,4 +241,4 @@ BackgroundItem { } } } -} \ No newline at end of file +} diff --git a/plugin/ToolBar.qml b/plugin/ToolBar.qml index c5723555..85fd9ea1 100644 --- a/plugin/ToolBar.qml +++ b/plugin/ToolBar.qml @@ -30,6 +30,19 @@ PanelBackground { property bool _active property int _previousContentY + function show() { + if (forceHidden) { + return + } + autoHideTimer.stop() + _active = true + if (autoShowHide) autoHideTimer.restart() + } + function hide() { + _active = false + autoHideTimer.stop() + } + onAutoShowHideChanged: { if (autoShowHide) { if (_active) { @@ -37,8 +50,16 @@ PanelBackground { } } else { autoHideTimer.stop() - // Keep a transiting toolbar visible. - _active = (offset > 0) + // Keep a transiting (and a not transited yet) toolbar visible. + _active = _active || (offset > 0) + } + } + + onForceHiddenChanged: { + // Avoid showing back the toolbar when forceHidden becomes false again. + if (forceHidden && autoShowHide) { + _active = false + autoHideTimer.stop() } }