1 /**
2  * Copyright (C) 2011-2012  Charlie Sharpsteen, Stefan Löffler
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the Free
6  * Software Foundation; either version 2, or (at your option) any later
7  * version.
8  *
9  * This program is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
12  * more details.
13  */
14 #include "PDFDocumentView.h"
15 
16 #if QT_VERSION_MAJOR >= 5
17   #include <QtConcurrent>
18 #endif
19 
20 // This has to be outside the namespace (according to Qt docs)
initResources()21 static void initResources()
22 {
23   Q_INIT_RESOURCE(QtPDF_trans);
24   Q_INIT_RESOURCE(QtPDF_icons);
25 }
26 
27 namespace QtPDF {
28 
29 // In static builds, we need to explicitly initialize the resources
30 // (translations).
31 // NOTE: In shared builds, this doesn't seem to hurt.
32 class ResourceInitializer
33 {
34 public:
35   // Call out-of-namespace function in constructor
ResourceInitializer()36   ResourceInitializer() { ::initResources(); }
37 };
38 ResourceInitializer _resourceInitializer;
39 
40 #ifdef DEBUG
41 #include <QDebug>
42 QTime stopwatch;
43 #endif
44 
45 // Some utility functions.
46 //
47 // **TODO:** _Find a better place to put these._
isPageItem(const QGraphicsItem * item)48 static bool isPageItem(const QGraphicsItem *item) { return ( item->type() == PDFPageGraphicsItem::Type ); }
49 
50 // PDFDocumentView
51 // ===============
52 QTranslator * PDFDocumentView::_translator = NULL;
53 QString PDFDocumentView::_translatorLanguage;
54 
55 // This class descends from `QGraphicsView` and is responsible for controlling
56 // and displaying the contents of a `Document` using a `QGraphicsScene`.
PDFDocumentView(QWidget * parent)57 PDFDocumentView::PDFDocumentView(QWidget *parent):
58   Super(parent),
59   _pdf_scene(NULL),
60   _zoomLevel(1.0),
61   _currentPage(-1),
62   _lastPage(-1),
63   _currentSearchResult(-1),
64   _useGrayScale(false),
65   _pageMode(PageMode_OneColumnContinuous),
66   _mouseMode(MouseMode_Move),
67   _armedTool(NULL)
68 {
69   initResources();
70   // FIXME: Allow to initialize with a specific language (in case the
71   // application uses a custom locale and switchInterfaceLocale() has not been
72   // called, yet (e.g., this is the first instance of PDFDocumentView that is
73   // created))
74   setBackgroundRole(QPalette::Dark);
75   setAlignment(Qt::AlignCenter);
76   setFocusPolicy(Qt::StrongFocus);
77 
78   QColor fillColor(Qt::darkYellow);
79   fillColor.setAlphaF(0.3);
80   _searchResultHighlightBrush = QBrush(fillColor);
81 
82   fillColor = QColor(Qt::yellow);
83   fillColor.setAlphaF(0.6);
84   _currentSearchResultHighlightBrush = QBrush(fillColor);
85 
86   // If _currentPage is not set to -1, the compiler may default to 0. In that
87   // case, `goFirst()` or `goToPage(0)` will fail because the view will think
88   // it is already looking at page 0.
89   _currentPage = -1;
90 
91   registerTool(new DocumentTool::ZoomIn(this));
92   registerTool(new DocumentTool::ZoomOut(this));
93   registerTool(new DocumentTool::MagnifyingGlass(this));
94   registerTool(new DocumentTool::MarqueeZoom(this));
95   registerTool(new DocumentTool::Move(this));
96   registerTool(new DocumentTool::ContextClick(this));
97   registerTool(new DocumentTool::Measure(this));
98   registerTool(new DocumentTool::Select(this));
99 
100   // Some tools (e.g., the Select tool) may need to be informed of mouse events
101   // (e.g., mouseMoveEvent) when armed, even before a mouse button is pressed
102   viewport()->setMouseTracking(true);
103 
104   // We deliberately set the mouse mode to a different value above so we can
105   // call setMouseMode (which bails out if the mouse mode is not changed), which
106   // in turn sets up other variables such as _toolAccessors
107   setMouseMode(MouseMode_MagnifyingGlass);
108 
109   connect(&_searchResultWatcher, SIGNAL(resultReadyAt(int)), this, SLOT(searchResultReady(int)));
110   connect(&_searchResultWatcher, SIGNAL(progressValueChanged(int)), this, SLOT(searchProgressValueChanged(int)));
111 }
112 
~PDFDocumentView()113 PDFDocumentView::~PDFDocumentView()
114 {
115   if (!_searchResultWatcher.isFinished())
116     _searchResultWatcher.cancel();
117 }
118 
119 // Accessors
120 // ---------
setScene(QSharedPointer<PDFDocumentScene> a_scene)121 void PDFDocumentView::setScene(QSharedPointer<PDFDocumentScene> a_scene)
122 {
123   // FIXME: Make setScene(QGraphicsScene*) (from parent class) invisible to the
124   // outside world
125   Super::setScene(a_scene.data());
126 
127   // disconnect us from the old scene (if any)
128   if (_pdf_scene) {
129     disconnect(_pdf_scene.data(), 0, this, 0);
130     _pdf_scene.clear();
131   }
132 
133   _pdf_scene = a_scene;
134 
135   // Reinitialize all scene-related data
136   // NB: This also resets any text selection and search results as it clears the
137   // page graphic items.
138   reinitializeFromScene();
139 
140   if (a_scene) {
141     // Respond to page jumps requested by the `PDFDocumentScene`.
142     //
143     // **TODO:**
144     // _May want to consider not doing this by default. It is conceivable to have
145     // a View that would ignore page jumps that other scenes would respond to._
146     connect(_pdf_scene.data(), SIGNAL(pageChangeRequested(int)), this, SLOT(goToPage(int)));
147     connect(_pdf_scene.data(), SIGNAL(pdfActionTriggered(const QtPDF::PDFAction*)), this, SLOT(pdfActionTriggered(const QtPDF::PDFAction*)));
148     connect(_pdf_scene.data(), SIGNAL(documentChanged(const QWeakPointer<QtPDF::Backend::Document>)), this, SLOT(reinitializeFromScene()));
149     // The connection PDFDocumentScene::documentChanged > PDFDocumentView::changedDocument
150     // must be last in this list to ensure all internal states are updated (e.g.
151     // in _lastPage in reinitializeFromScene()) before the signal is
152     // communicated on to the "outside world".
153     connect(_pdf_scene.data(), SIGNAL(documentChanged(const QWeakPointer<QtPDF::Backend::Document>)), this, SIGNAL(changedDocument(const QWeakPointer<QtPDF::Backend::Document>)));
154   }
155 
156   // ensure the zoom is reset if we load a new document
157   zoom100();
158 
159   // Ensure we're at the top left corner (we need to set _currentPage to -1 to
160   // ensure goToPage() actually does anything.
161   int page = _currentPage;
162   if (page >= 0) {
163     _currentPage = -1;
164     goToPage(page);
165   }
166 
167   // Ensure proper layout
168   setPageMode(_pageMode, true);
169 
170   if (_pdf_scene)
171     emit changedDocument(_pdf_scene->document());
172   else
173     emit changedDocument(QSharedPointer<Backend::Document>());
174 }
currentPage()175 int PDFDocumentView::currentPage() { return _currentPage; }
lastPage()176 int PDFDocumentView::lastPage()    { return _lastPage; }
177 
setPageMode(const PageMode pageMode,const bool forceRelayout)178 void PDFDocumentView::setPageMode(const PageMode pageMode, const bool forceRelayout /* = false */)
179 {
180   if (_pageMode == pageMode && !forceRelayout)
181     return;
182   if (!_pdf_scene) {
183     // If we don't have a scene (yet), save the setting for future use and return
184     _pageMode = pageMode;
185     emit changedPageMode(pageMode);
186     return;
187   }
188 
189   QGraphicsItem *currentPage = _pdf_scene->pageAt(_currentPage);
190   if (!currentPage)
191     return;
192 
193   // Save the current view relative to the current page so we can restore it
194   // after changing the mode
195   // **TODO:** Safeguard
196   QRectF viewRect(mapToScene(viewport()->rect()).boundingRect());
197   viewRect.translate(-currentPage->pos());
198 
199   // **TODO:** Avoid relayouting everything twice when switching from SinglePage
200   // to TwoColumnContinuous (once by setContinuous(), and a second time by
201   // setColumnCount() below)
202   switch (pageMode) {
203     case PageMode_SinglePage:
204     case PageMode_Presentation:
205       _pdf_scene->showOnePage(_currentPage);
206       _pdf_scene->pageLayout().setContinuous(false);
207       break;
208     case PageMode_OneColumnContinuous:
209       if (_pageMode == PageMode_SinglePage) {
210         _pdf_scene->pageLayout().setContinuous(true);
211         _pdf_scene->showAllPages();
212         // Reset the scene rect; causes it the encompass the whole scene
213         setSceneRect(QRectF());
214       }
215       _pdf_scene->pageLayout().setColumnCount(1, 0);
216       break;
217     case PageMode_TwoColumnContinuous:
218       if (_pageMode == PageMode_SinglePage) {
219         _pdf_scene->pageLayout().setContinuous(true);
220         _pdf_scene->showAllPages();
221         // Reset the scene rect; causes it the encompass the whole scene
222         setSceneRect(QRectF());
223       }
224       _pdf_scene->pageLayout().setColumnCount(2, 1);
225       break;
226   }
227 
228   // Ensure the background is black during presentation (independent of the
229   // current palette and background role)
230   if (pageMode == PageMode_Presentation)
231     setBackgroundBrush(QBrush(Qt::black));
232   else
233     setBackgroundBrush(Qt::NoBrush);
234 
235   _pageMode = pageMode;
236   _pdf_scene->pageLayout().relayout();
237 
238   // We might need to update the scene rect (when switching to single page mode)
239   maybeUpdateSceneRect();
240 
241   if (pageMode == PageMode_Presentation)
242     zoomFitWindow();
243   else {
244     // Restore the view from before as good as possible
245     viewRect.translate(_pdf_scene->pageAt(_currentPage)->pos());
246     ensureVisible(viewRect, 0, 0);
247   }
248 
249   emit changedPageMode(pageMode);
250 }
251 
dockWidget(const Dock type,QWidget * parent)252 QDockWidget * PDFDocumentView::dockWidget(const Dock type, QWidget * parent /* = NULL */)
253 {
254   QDockWidget * dock = new QDockWidget(QString(), parent);
255   Q_ASSERT(dock != NULL);
256 
257   PDFDocumentInfoWidget * infoWidget;
258   switch (type) {
259     case Dock_TableOfContents:
260     {
261       PDFToCInfoWidget * tocWidget = new PDFToCInfoWidget(dock);
262       connect(tocWidget, SIGNAL(actionTriggered(const QtPDF::PDFAction*)), this, SLOT(pdfActionTriggered(const QtPDF::PDFAction*)));
263       infoWidget = tocWidget;
264       break;
265     }
266     case Dock_MetaData:
267       infoWidget = new PDFMetaDataInfoWidget(dock);
268       break;
269     case Dock_Fonts:
270       infoWidget = new PDFFontsInfoWidget(dock);
271       break;
272     case Dock_Permissions:
273       infoWidget = new PDFPermissionsInfoWidget(dock);
274       break;
275     case Dock_Annotations:
276       infoWidget = new PDFAnnotationsInfoWidget(dock);
277       // TODO: possibility to jump to selected/activated annotation
278       break;
279     default:
280       infoWidget = NULL;
281       break;
282   }
283   if (!infoWidget) {
284     dock->deleteLater();
285     return NULL;
286   }
287   if (_pdf_scene && _pdf_scene->document())
288       infoWidget->initFromDocument(_pdf_scene->document());
289   connect(this, SIGNAL(changedDocument(const QWeakPointer<QtPDF::Backend::Document>)), infoWidget, SLOT(initFromDocument(const QWeakPointer<QtPDF::Backend::Document>)));
290 
291   dock->setWindowTitle(infoWidget->windowTitle());
292   dock->setObjectName(infoWidget->objectName() + QString::fromLatin1(".DockWidget"));
293   connect(infoWidget, SIGNAL(windowTitleChanged(const QString &)), dock, SLOT(setWindowTitle(const QString &)));
294 
295   // We don't want docks to (need to) take up a lot of space. If the infoWidget
296   // can't shrink, we thus put it into a scroll area that can
297   if (!(infoWidget->sizePolicy().horizontalPolicy() & QSizePolicy::ShrinkFlag) || \
298       !(infoWidget->sizePolicy().verticalPolicy() & QSizePolicy::ShrinkFlag)) {
299     QScrollArea * scrollArea = new QScrollArea(dock);
300     scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
301     scrollArea->setWidget(infoWidget);
302     dock->setWidget(scrollArea);
303   }
304   else
305     dock->setWidget(infoWidget);
306   return dock;
307 }
308 
309 // path in PDF coordinates
addHighlightPath(const unsigned int page,const QPainterPath & path,const QBrush & brush,const QPen & pen)310 QGraphicsPathItem * PDFDocumentView::addHighlightPath(const unsigned int page, const QPainterPath & path, const QBrush & brush, const QPen & pen /* = Qt::NoPen */)
311 {
312   if (!_pdf_scene)
313     return NULL;
314 
315   PDFPageGraphicsItem * pageItem = (PDFPageGraphicsItem*)(_pdf_scene->pageAt(page));
316   if (!pageItem || !isPageItem(pageItem))
317     return NULL;
318 
319   QGraphicsPathItem * highlightItem = new QGraphicsPathItem(path, pageItem);
320   highlightItem->setBrush(brush);
321   highlightItem->setPen(pen);
322   highlightItem->setTransform(pageItem->pointScale());
323   return highlightItem;
324 }
325 
fitInView(const QRectF & rect,Qt::AspectRatioMode aspectRatioMode)326 void PDFDocumentView::fitInView(const QRectF & rect, Qt::AspectRatioMode aspectRatioMode /* = Qt::IgnoreAspectRatio */)
327 {
328   const unsigned int ScaleDataSize = 4;
329   struct ScaleData {
330     qreal xratio, yratio;
331     bool horizontalScrollbar, verticalScrollbar;
332   } _scaleDat[ScaleDataSize];
333   qreal xratio, yratio;
334   qreal horizontalScrollbar, verticalScrollbar;
335   QRectF viewRect;
336   QRectF sceneRect;
337   Qt::ScrollBarPolicy oldHorizontalPolicy, oldVerticalPolicy;
338 
339   // This method is modeled closely after QGraphicsView::fitInView(), with two
340   // notable exceptions: 1) no arbitrary (hard-coded) margin is added, thus
341   // allowing page-scrolling without (slight) shifts. 2) we calculate zoom
342   // factors with and without scroll bars and pick the most suitable one; this
343   // way, cases where we would zoom out so much that, e.g., a horizontal scroll
344   // bar is no longer needed are handled properly
345 
346   // Ensure that we have a valid rect to fit into the view
347   if (rect.isNull())
348     return;
349 
350   // Save the current scroll bar policies so we can restore them later;
351   // NB: this method repeatedly changes the scroll bar policies to "simulate"
352   // cases without scroll bars as needed
353   oldHorizontalPolicy = horizontalScrollBarPolicy();
354   oldVerticalPolicy = verticalScrollBarPolicy();
355 
356   // Reset the view scale to 1:1.
357   QRectF unity = matrix().mapRect(QRectF(0, 0, 1, 1));
358   if (unity.isEmpty())
359     return;
360   scale(1 / unity.width(), 1 / unity.height());
361 
362   // 1) determine the potential scaling ratios for "only" fitting the given
363   // rect into the current viewport; the scroll bars remain as they are
364   _scaleDat[0].horizontalScrollbar = (horizontalScrollBar() ? horizontalScrollBar()->isVisible() : false);
365   _scaleDat[0].verticalScrollbar = (verticalScrollBar() ? verticalScrollBar()->isVisible() : false);
366   viewRect = viewport()->rect();
367   sceneRect = matrix().mapRect(rect);
368   if (!viewRect.isEmpty() && !sceneRect.isEmpty()) {
369     _scaleDat[0].xratio = viewRect.width() / sceneRect.width();
370     _scaleDat[0].yratio = viewRect.height() / sceneRect.height();
371   }
372   else {
373     _scaleDat[0].xratio = -1;
374     _scaleDat[0].yratio = -1;
375   }
376 
377   // 2) determine potential scaling ratios for fitting the whole scene width
378   // into the viewport (with disabled horizontal scroll bars if possible!)
379   setHorizontalScrollBarPolicy(oldHorizontalPolicy == Qt::ScrollBarAsNeeded ? Qt::ScrollBarAlwaysOff : oldHorizontalPolicy);
380   setVerticalScrollBarPolicy(oldVerticalPolicy);
381   _scaleDat[1].horizontalScrollbar = (horizontalScrollBar() ? horizontalScrollBar()->isVisible() : false);
382   _scaleDat[1].verticalScrollbar = (verticalScrollBar() ? verticalScrollBar()->isVisible() : false);
383 
384   viewRect = viewport()->rect();
385   setHorizontalScrollBarPolicy(oldHorizontalPolicy);
386   sceneRect.setLeft(matrix().mapRect(_pdf_scene->sceneRect()).left());
387   sceneRect.setRight(matrix().mapRect(_pdf_scene->sceneRect()).right());
388 
389   if (!viewRect.isEmpty() && !sceneRect.isEmpty()) {
390     _scaleDat[1].xratio = viewRect.width() / sceneRect.width();
391     _scaleDat[1].yratio = viewRect.height() / sceneRect.height();
392   }
393   else {
394     _scaleDat[1].xratio = -1;
395     _scaleDat[1].yratio = -1;
396   }
397 
398   // 3) determine potential scaling ratios for fitting the whole scene height
399   // into the viewport (with disabled vertical scroll bars if possible!)
400   setHorizontalScrollBarPolicy(oldHorizontalPolicy);
401   setVerticalScrollBarPolicy(oldVerticalPolicy == Qt::ScrollBarAsNeeded ? Qt::ScrollBarAlwaysOff : oldVerticalPolicy);
402   _scaleDat[2].horizontalScrollbar = (horizontalScrollBar() ? horizontalScrollBar()->isVisible() : false);
403   _scaleDat[2].verticalScrollbar = (verticalScrollBar() ? verticalScrollBar()->isVisible() : false);
404 
405   viewRect = viewport()->rect();
406   setVerticalScrollBarPolicy(oldVerticalPolicy);
407   sceneRect.setTop(matrix().mapRect(_pdf_scene->sceneRect()).top());
408   sceneRect.setBottom(matrix().mapRect(_pdf_scene->sceneRect()).bottom());
409 
410   if (!viewRect.isEmpty() && !sceneRect.isEmpty()) {
411     _scaleDat[2].xratio = viewRect.width() / sceneRect.width();
412     _scaleDat[2].yratio = viewRect.height() / sceneRect.height();
413   }
414   else {
415     _scaleDat[2].xratio = -1;
416     _scaleDat[2].yratio = -1;
417   }
418 
419   // 4) determine potential scaling ratios for fitting the whole scene
420   // into the viewport (with disabled scroll bars if possible!)
421   setHorizontalScrollBarPolicy(oldHorizontalPolicy == Qt::ScrollBarAsNeeded ? Qt::ScrollBarAlwaysOff : oldHorizontalPolicy);
422   setVerticalScrollBarPolicy(oldVerticalPolicy == Qt::ScrollBarAsNeeded ? Qt::ScrollBarAlwaysOff : oldVerticalPolicy);
423   _scaleDat[3].horizontalScrollbar = (horizontalScrollBar() ? horizontalScrollBar()->isVisible() : false);
424   _scaleDat[3].verticalScrollbar = (verticalScrollBar() ? verticalScrollBar()->isVisible() : false);
425 
426   viewRect = viewport()->rect();
427   setHorizontalScrollBarPolicy(oldHorizontalPolicy);
428   setVerticalScrollBarPolicy(oldVerticalPolicy);
429   sceneRect = matrix().mapRect(_pdf_scene->sceneRect());
430 
431   if (!viewRect.isEmpty() && !sceneRect.isEmpty()) {
432     _scaleDat[3].xratio = viewRect.width() / sceneRect.width();
433     _scaleDat[3].yratio = viewRect.height() / sceneRect.height();
434   }
435   else {
436     _scaleDat[3].xratio = -1;
437     _scaleDat[3].yratio = -1;
438   }
439 
440   // Determine the optimal (i.e., maximum) zoom factor
441   // Respect the aspect ratio mode.
442   switch (aspectRatioMode) {
443     case Qt::KeepAspectRatio:
444       xratio = yratio = -1;
445       for (unsigned int i = 0; i < ScaleDataSize; ++i) {
446         qreal r = qMin(_scaleDat[i].xratio, _scaleDat[i].yratio);
447         if (xratio < r) {
448           xratio = yratio = r;
449           horizontalScrollbar = _scaleDat[i].horizontalScrollbar;
450           verticalScrollbar = _scaleDat[i].verticalScrollbar;
451         }
452       }
453       break;
454     case Qt::KeepAspectRatioByExpanding:
455       xratio = yratio = -1;
456       for (unsigned int i = 0; i < ScaleDataSize; ++i) {
457         qreal r = qMax(_scaleDat[i].xratio, _scaleDat[i].yratio);
458         if (xratio < r) {
459           xratio = yratio = r;
460           horizontalScrollbar = _scaleDat[i].horizontalScrollbar;
461           verticalScrollbar = _scaleDat[i].verticalScrollbar;
462         }
463       }
464       break;
465     case Qt::IgnoreAspectRatio:
466       xratio = yratio = -1;
467       for (unsigned int i = 0; i < ScaleDataSize; ++i) {
468         if (xratio < _scaleDat[i].xratio) {
469           xratio = _scaleDat[i].xratio;
470           horizontalScrollbar = _scaleDat[i].horizontalScrollbar;
471         }
472         if (yratio < _scaleDat[i].yratio) {
473           yratio = _scaleDat[i].yratio;
474           verticalScrollbar = _scaleDat[i].verticalScrollbar;
475         }
476       }
477       break;
478   }
479 
480   if (xratio <= 0 || yratio <= 0)
481     return;
482 
483   // Set the scroll bar policy to what it will be in the end so `centerOn` works
484   // with the correct viewport
485   setHorizontalScrollBarPolicy(horizontalScrollbar ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
486   setVerticalScrollBarPolicy(verticalScrollbar ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
487 
488   // Scale and center on the center of \a rect.
489   scale(xratio, yratio);
490   centerOn(rect.center());
491 
492   // Reset the scroll bar policy to what it was before
493   setHorizontalScrollBarPolicy(oldHorizontalPolicy);
494   setVerticalScrollBarPolicy(oldVerticalPolicy);
495 }
496 
497 // Public Slots
498 // ------------
499 
goPrev()500 void PDFDocumentView::goPrev()  { goToPage(_currentPage - 1, Qt::AlignBottom); }
goNext()501 void PDFDocumentView::goNext()  { goToPage(_currentPage + 1, Qt::AlignTop); }
goFirst()502 void PDFDocumentView::goFirst() { goToPage(0); }
goLast()503 void PDFDocumentView::goLast()  { goToPage(_lastPage - 1); }
504 
goPrevViewRect()505 void PDFDocumentView::goPrevViewRect() {
506   if (_oldViewRects.empty())
507     return;
508   goToPDFDestination(_oldViewRects.pop(), false);
509 }
510 
511 
512 // `goToPage` will shift the view to a different page. If the `alignment`
513 // parameter is `Qt::AlignLeft | Qt::AlignTop` (the default), the view will
514 // ensure the top left corner of the page is visible and aligned with the top
515 // left corner of the viewport (if possible). Other alignments can be used in
516 // the same way. If `alignment` for a direction is not set the view will
517 // show the same portion of the new page as it did before with the old page.
goToPage(const int pageNum,const int alignment)518 void PDFDocumentView::goToPage(const int pageNum, const int alignment /* = Qt::AlignLeft | Qt::AlignTop */)
519 {
520   // We silently ignore any invalid page numbers.
521   if (!_pdf_scene || pageNum < 0 || pageNum >= _lastPage)
522     return;
523   if (pageNum == _currentPage)
524     return;
525 
526   goToPage((const PDFPageGraphicsItem*)_pdf_scene->pageAt(pageNum), alignment);
527 }
528 
goToPage(const int pageNum,const QPointF anchor,const int alignment)529 void PDFDocumentView::goToPage(const int pageNum, const QPointF anchor, const int alignment /* = Qt::AlignHCenter | Qt::AlignVCenter */)
530 {
531   // We silently ignore any invalid page numbers.
532   if (!_pdf_scene || pageNum < 0 || pageNum >= _lastPage)
533     return;
534   if (pageNum == _currentPage)
535     return;
536 
537   goToPage((const PDFPageGraphicsItem*)_pdf_scene->pageAt(pageNum), anchor, alignment);
538 }
539 
goToPDFDestination(const PDFDestination & dest,bool saveOldViewRect)540 void PDFDocumentView::goToPDFDestination(const PDFDestination & dest, bool saveOldViewRect /* = true */)
541 {
542   if (!dest.isValid())
543     return;
544 
545   Q_ASSERT(_pdf_scene != NULL);
546   Q_ASSERT(!_pdf_scene->document().isNull());
547   QSharedPointer<Backend::Document> doc(_pdf_scene->document().toStrongRef());
548   if (!doc)
549     return;
550 
551   PDFDestination finalDest;
552   if (!dest.isExplicit()) {
553     finalDest = doc->resolveDestination(dest);
554     if (!finalDest.isValid())
555       return;
556   }
557   else
558     finalDest = dest;
559 
560   Q_ASSERT(isPageItem(_pdf_scene->pageAt(_currentPage)));
561   PDFPageGraphicsItem * pageItem = static_cast<PDFPageGraphicsItem*>(_pdf_scene->pageAt(_currentPage));
562   Q_ASSERT(pageItem != NULL);
563 
564   // Get the current (=old) viewport in the current (=old) page's
565   // coordinate system
566   QRectF oldViewport = pageItem->mapRectFromScene(mapToScene(viewport()->rect()).boundingRect());
567   oldViewport = QRectF(pageItem->mapToPage(oldViewport.topLeft()), \
568                        pageItem->mapToPage(oldViewport.bottomRight()));
569   // Calculate the new viewport (in page coordinates)
570   QRectF view(finalDest.viewport(doc.data(), oldViewport, _zoomLevel));
571 
572   if (saveOldViewRect) {
573     PDFDestination origin(_currentPage);
574     origin.setType(PDFDestination::Destination_FitR);
575     origin.setRect(oldViewport);
576     _oldViewRects.push(origin);
577   }
578 
579   goToPage(static_cast<PDFPageGraphicsItem*>(_pdf_scene->pageAt(finalDest.page())), view, true);
580 }
581 
zoomBy(const qreal zoomFactor,const QGraphicsView::ViewportAnchor anchor)582 void PDFDocumentView::zoomBy(const qreal zoomFactor, const QGraphicsView::ViewportAnchor anchor /* = QGraphicsView::AnchorViewCenter */)
583 {
584   if (zoomFactor <= 0)
585     return;
586 
587   _zoomLevel *= zoomFactor;
588   // Set the transformation anchor to AnchorViewCenter so we always zoom out of
589   // the center of the view (rather than out of the upper left corner)
590   QGraphicsView::ViewportAnchor oldAnchor = transformationAnchor();
591   setTransformationAnchor(anchor);
592   this->scale(zoomFactor, zoomFactor);
593   setTransformationAnchor(oldAnchor);
594 
595   emit changedZoom(_zoomLevel);
596 }
597 
setZoomLevel(const qreal zoomLevel,const QGraphicsView::ViewportAnchor anchor)598 void PDFDocumentView::setZoomLevel(const qreal zoomLevel, const QGraphicsView::ViewportAnchor anchor /* = QGraphicsView::AnchorViewCenter */)
599 {
600   if (zoomLevel <= 0)
601     return;
602   zoomBy(zoomLevel / _zoomLevel, anchor);
603 }
604 
zoomIn(const QGraphicsView::ViewportAnchor anchor)605 void PDFDocumentView::zoomIn(const QGraphicsView::ViewportAnchor anchor /* = QGraphicsView::AnchorViewCenter */) { zoomBy(3.0/2.0, anchor); }
zoomOut(const QGraphicsView::ViewportAnchor anchor)606 void PDFDocumentView::zoomOut(const QGraphicsView::ViewportAnchor anchor /* = QGraphicsView::AnchorViewCenter */) { zoomBy(2.0/3.0, anchor); }
607 
zoomToRect(QRectF a_rect)608 void PDFDocumentView::zoomToRect(QRectF a_rect)
609 {
610   // NOTE: The argument, `a_rect`, is assumed to be in _scene coordinates_.
611   fitInView(a_rect, Qt::KeepAspectRatio);
612 
613   // Since we passed `Qt::KeepAspectRatio` to `fitInView` both x and y scaling
614   // factors were changed by the same amount. So we'll just take the x scale to
615   // be the new `_zoomLevel`.
616   _zoomLevel = transform().m11();
617   emit changedZoom(_zoomLevel);
618 }
619 
zoomFitWindow()620 void PDFDocumentView::zoomFitWindow()
621 {
622   if (!scene())
623     return;
624 
625   QGraphicsItem *currentPage = _pdf_scene->pageAt(_currentPage);
626   if (!currentPage)
627     return;
628 
629   QRectF rect(currentPage->sceneBoundingRect());
630   // Add a margin of half the inter-page spacing around the page rect so
631   // scrolling by one such rect will take us to exactly the same area on the
632   // next page
633   qreal dx = _pdf_scene->pageLayout().xSpacing();
634   qreal dy = _pdf_scene->pageLayout().ySpacing();
635   rect.adjust(-dx / 2, -dy / 2, dx / 2, dy / 2);
636 
637   zoomToRect(rect);
638 }
639 
640 
zoomFitWidth()641 void PDFDocumentView::zoomFitWidth()
642 {
643   if (!_pdf_scene)
644     return;
645 
646   QGraphicsItem *currentPage = _pdf_scene->pageAt(_currentPage);
647   if (!currentPage)
648     return;
649 
650   QRectF rect(currentPage->sceneBoundingRect());
651 
652   // Add a margin of half the inter-page spacing around the page rect so
653   // scrolling by one such rect will take us to exactly the same area on the
654   // next page
655   qreal dx = _pdf_scene->pageLayout().xSpacing();
656   rect.adjust(-dx / 2, 0, dx / 2, 0);
657 
658   // Store current y position so we can center on it later.
659   qreal ypos = mapToScene(viewport()->rect()).boundingRect().center().y();
660 
661   // Squash the rect to minimal height so its width will be limitting the zoom
662   // factor
663   rect.setTop(ypos - 1e-5);
664   rect.setBottom(ypos + 1e-5);
665 
666   zoomToRect(rect);
667 }
668 
zoom100()669 void PDFDocumentView::zoom100()
670 {
671   // Reset zoom level to 100%
672 
673   // Reset the view scale to 1:1.
674   QRectF unity = matrix().mapRect(QRectF(0, 0, 1, 1));
675   if (unity.isEmpty())
676       return;
677 
678   // Set the transformation anchor to AnchorViewCenter so we always zoom out of
679   // the center of the view (rather than out of the upper left corner)
680   QGraphicsView::ViewportAnchor anchor = transformationAnchor();
681   setTransformationAnchor(QGraphicsView::AnchorViewCenter);
682   scale(1 / unity.width(), 1 / unity.height());
683   setTransformationAnchor(anchor);
684 
685   _zoomLevel = transform().m11();
686   emit changedZoom(_zoomLevel);
687 }
688 
setMouseMode(const MouseMode newMode)689 void PDFDocumentView::setMouseMode(const MouseMode newMode)
690 {
691   if (_mouseMode == newMode)
692     return;
693 
694   // TODO: eventually make _toolAccessors configurable
695   _toolAccessors.clear();
696   _toolAccessors[Qt::ControlModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_ContextClick);
697   _toolAccessors[Qt::NoModifier + Qt::RightButton] = getToolByType(DocumentTool::AbstractTool::Tool_ContextMenu);
698   _toolAccessors[Qt::NoModifier + Qt::MiddleButton] = getToolByType(DocumentTool::AbstractTool::Tool_Move);
699   _toolAccessors[Qt::ShiftModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_ZoomIn);
700   _toolAccessors[Qt::AltModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_ZoomOut);
701   // Other tools: Tool_MagnifyingGlass, Tool_MarqueeZoom, Tool_Move
702 
703   disarmTool();
704 
705   switch (newMode) {
706     case MouseMode_Move:
707       armTool(DocumentTool::AbstractTool::Tool_Move);
708       _toolAccessors[Qt::NoModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_Move);
709       break;
710 
711     case MouseMode_MarqueeZoom:
712       armTool(DocumentTool::AbstractTool::Tool_MarqueeZoom);
713       _toolAccessors[Qt::NoModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_MarqueeZoom);
714       break;
715 
716     case MouseMode_MagnifyingGlass:
717       armTool(DocumentTool::AbstractTool::Tool_MagnifyingGlass);
718       _toolAccessors[Qt::NoModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_MagnifyingGlass);
719       break;
720 
721     case MouseMode_Measure:
722       armTool(DocumentTool::AbstractTool::Tool_Measure);
723       _toolAccessors[Qt::NoModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_Measure);
724       break;
725 
726     case MouseMode_Select:
727       armTool(DocumentTool::AbstractTool::Tool_Select);
728       _toolAccessors[Qt::NoModifier + Qt::LeftButton] = getToolByType(DocumentTool::AbstractTool::Tool_Select);
729       break;
730   }
731 
732   _mouseMode = newMode;
733 }
734 
setMagnifierShape(const DocumentTool::MagnifyingGlass::MagnifierShape shape)735 void PDFDocumentView::setMagnifierShape(const DocumentTool::MagnifyingGlass::MagnifierShape shape)
736 {
737   DocumentTool::MagnifyingGlass * magnifier = static_cast<DocumentTool::MagnifyingGlass*>(getToolByType(DocumentTool::AbstractTool::Tool_MagnifyingGlass));
738   if (magnifier)
739     magnifier->setMagnifierShape(shape);
740 }
741 
setMagnifierSize(const int size)742 void PDFDocumentView::setMagnifierSize(const int size)
743 {
744   DocumentTool::MagnifyingGlass * magnifier = static_cast<DocumentTool::MagnifyingGlass*>(getToolByType(DocumentTool::AbstractTool::Tool_MagnifyingGlass));
745   if (magnifier)
746     magnifier->setMagnifierSize(size);
747 }
748 
search(QString searchText,Backend::SearchFlags flags)749 void PDFDocumentView::search(QString searchText, Backend::SearchFlags flags /* = Backend::Search_CaseInsensitive */)
750 {
751   if ( not _pdf_scene )
752     return;
753 
754   // If `searchText` is the same as for the last search, focus on the next
755   // search result.
756   // Note: The primary use case for this is hitting `Enter` several times in the
757   // search box to go to the next result.
758   // On the other hand, with this there is no easy way to abort a long-running
759   // search (e.g., in a large document) and restarting it in another part by
760   // simply going there and hitting `Enter` again. As a workaround, one can
761   // change the search text in that case (e.g., to something meaningless and
762   // then back again to abort the previous search and restart at the new
763   // location).
764   if (searchText == _searchString) {
765     // FIXME: If flags changed we need to do a full search!
766     nextSearchResult();
767     return;
768   }
769 
770   clearSearchResults();
771 
772   // Construct a list of requests that can be passed to QtConcurrent::mapped()
773   QList<Backend::SearchRequest> requests;
774   int i;
775   for (i = _currentPage; i < _lastPage; ++i) {
776     Backend::SearchRequest request;
777     request.doc = _pdf_scene->document();
778     request.pageNum = i;
779     request.searchString = searchText;
780     request.flags = flags;
781     requests << request;
782   }
783   for (i = 0; i < _currentPage; ++i) {
784     Backend::SearchRequest request;
785     request.doc = _pdf_scene->document();
786     request.pageNum = i;
787     request.searchString = searchText;
788     request.flags = flags;
789     requests << request;
790   }
791 
792   // If another search is still running, cancel it---after all, the user wants
793   // to perform a new search
794   if (!_searchResultWatcher.isFinished()) {
795     _searchResultWatcher.cancel();
796     _searchResultWatcher.waitForFinished();
797   }
798 
799   _currentSearchResult = -1;
800   _searchString = searchText;
801   _searchResultWatcher.setFuture(QtConcurrent::mapped(requests, Backend::Page::executeSearch));
802 }
803 
nextSearchResult()804 void PDFDocumentView::nextSearchResult()
805 {
806   if ( not _pdf_scene || _searchResults.empty() )
807     return;
808 
809   // Note: _currentSearchResult is initially -1 if no result is selected
810   if (_currentSearchResult >= 0 && _searchResults[_currentSearchResult])
811     static_cast<QGraphicsPathItem*>(_searchResults[_currentSearchResult])->setBrush(_searchResultHighlightBrush);
812 
813   if ( (_currentSearchResult + 1) >= _searchResults.size() )
814     _currentSearchResult = 0;
815   else
816     ++_currentSearchResult;
817 
818   // FIXME: The rest of the code in this method is the same as in previousSearchResult()
819   // We should move this into its own private method
820 
821   QGraphicsPathItem* highlightPath = static_cast<QGraphicsPathItem*>(_searchResults[_currentSearchResult]);
822 
823   if (!highlightPath)
824     return;
825 
826   highlightPath->setBrush(_currentSearchResultHighlightBrush);
827   centerOn(highlightPath);
828 
829   PDFPageGraphicsItem * pageItem = static_cast<PDFPageGraphicsItem *>(highlightPath->parentItem());
830   if (pageItem) {
831     QSharedPointer<Backend::Page> page = pageItem->page().toStrongRef();
832     // FIXME: shape subpath coordinates seem to be in upside down pdf coordinates. We should find a better place to construct the proper transform (e.g., in PDFPageGraphicsItem)
833     if (page)
834       emit searchResultHighlighted(pageItem->pageNum(), highlightPath->shape().toSubpathPolygons(QTransform::fromTranslate(0, page->pageSizeF().height()).scale(1, -1)));
835   }
836 }
837 
previousSearchResult()838 void PDFDocumentView::previousSearchResult()
839 {
840   if ( not _pdf_scene || _searchResults.empty() )
841     return;
842 
843   if (_currentSearchResult >= 0 && _searchResults[_currentSearchResult])
844     static_cast<QGraphicsPathItem*>(_searchResults[_currentSearchResult])->setBrush(_searchResultHighlightBrush);
845 
846   if ( (_currentSearchResult - 1) < 0 )
847     _currentSearchResult = _searchResults.size() - 1;
848   else
849     --_currentSearchResult;
850 
851   // FIXME: The rest of the code in this method is the same as in previousSearchResult()
852   // We should move this into its own private method
853 
854   QGraphicsPathItem* highlightPath = static_cast<QGraphicsPathItem*>(_searchResults[_currentSearchResult]);
855 
856   if (!highlightPath)
857     return;
858 
859   highlightPath->setBrush(_currentSearchResultHighlightBrush);
860   centerOn(highlightPath);
861 
862   PDFPageGraphicsItem * pageItem = static_cast<PDFPageGraphicsItem *>(highlightPath->parentItem());
863   if (pageItem) {
864     QSharedPointer<Backend::Page> page = pageItem->page().toStrongRef();
865     // FIXME: shape subpath coordinates seem to be in upside down pdf coordinates. We should find a better place to construct the proper transform (e.g., in PDFPageGraphicsItem)
866     if (page)
867       emit searchResultHighlighted(pageItem->pageNum(), highlightPath->shape().toSubpathPolygons(QTransform::fromTranslate(0, page->pageSizeF().height()).scale(1, -1)));
868   }
869 }
870 
clearSearchResults()871 void PDFDocumentView::clearSearchResults()
872 {
873   if ( not _pdf_scene || _searchResults.empty() )
874     return;
875 
876   foreach( QGraphicsItem *item, _searchResults )
877     _pdf_scene->removeItem(item);
878 
879   _searchResults.clear();
880 }
881 
setSearchResultHighlightBrush(const QBrush & brush)882 void PDFDocumentView::setSearchResultHighlightBrush(const QBrush & brush)
883 {
884   _searchResultHighlightBrush = brush;
885   for (int i = 0; i < _searchResults.size(); ++i) {
886     if (i == _currentSearchResult || !_searchResults[i])
887       continue;
888     static_cast<QGraphicsPathItem*>(_searchResults[i])->setBrush(brush);
889   }
890 }
891 
setCurrentSearchResultHighlightBrush(const QBrush & brush)892 void PDFDocumentView::setCurrentSearchResultHighlightBrush(const QBrush & brush)
893 {
894   _currentSearchResultHighlightBrush = brush;
895   if (_currentSearchResult >= 0 && _currentSearchResult < _searchResults.size() && _searchResults[_currentSearchResult])
896     static_cast<QGraphicsPathItem*>(_searchResults[_currentSearchResult])->setBrush(brush);
897 }
898 
899 
900 // Protected Slots
901 // --------------
searchResultReady(int index)902 void PDFDocumentView::searchResultReady(int index)
903 {
904   // Convert the search result to highlight boxes
905   foreach( Backend::SearchResult result, _searchResultWatcher.future().resultAt(index) )
906     _searchResults << addHighlightPath(result.pageNum, result.bbox, _searchResultHighlightBrush);
907 
908   // If this is the first result that becomes available in a new search, center
909   // on the first result
910   if (_currentSearchResult == -1)
911     nextSearchResult();
912 
913   // Inform the rest of the world of our progress (in %, and how many
914   // occurrences were found so far).
915   emit searchProgressChanged(100 * (_searchResultWatcher.progressValue() - _searchResultWatcher.progressMinimum()) / (_searchResultWatcher.progressMaximum() - _searchResultWatcher.progressMinimum()), _searchResults.count());
916 }
917 
searchProgressValueChanged(int progressValue)918 void PDFDocumentView::searchProgressValueChanged(int progressValue)
919 {
920   // Inform the rest of the world of our progress (in %, and how many
921   // occurrences were found so far)
922   // NOTE: the searchProgressValueChanged slot is not necessarily synchronized
923   // with the searchResultReady slot. I.e., it can happen that
924   // searchProgressValueChanged reports 100% with 0 search results (before
925   // searchResultReady is called for the first time). Thus, searchResultReady
926   // is also set up to emit searchProgressChanged. In summary,
927   // searchProgressValueChanged is intended primarily for informing the user
928   // of the progress when no matches are found, whereas searchResultReady is
929   // primarily intended for informing the user of the progress when matches are
930   // found.
931   if (_searchResultWatcher.progressMaximum() == _searchResultWatcher.progressMinimum())
932     emit searchProgressChanged(100, _searchResults.count());
933   else
934     emit searchProgressChanged(100 * (progressValue - _searchResultWatcher.progressMinimum()) / (_searchResultWatcher.progressMaximum() - _searchResultWatcher.progressMinimum()), _searchResults.count());
935 }
936 
maybeUpdateSceneRect()937 void PDFDocumentView::maybeUpdateSceneRect() {
938   if (!_pdf_scene || (_pageMode != PageMode_SinglePage && _pageMode != PageMode_Presentation))
939     return;
940 
941   // Set the scene rect of the view, i.e., the rect accessible via the scroll
942   // bars. In single page mode, this must be the rect of the current page
943   PDFPageGraphicsItem * pageItem = static_cast<PDFPageGraphicsItem *>(_pdf_scene->pageAt(_currentPage));
944   if (pageItem)
945     setSceneRect(pageItem->sceneBoundingRect());
946 }
947 
maybeArmTool(uint modifiers)948 void PDFDocumentView::maybeArmTool(uint modifiers)
949 {
950   // Arms the tool corresponding to `modifiers` if one is available.
951   DocumentTool::AbstractTool * t = _toolAccessors.value(modifiers, NULL);
952   if (t != _armedTool) {
953     disarmTool();
954     armTool(t);
955   }
956 }
957 
goToPage(const PDFPageGraphicsItem * page,const int alignment)958 void PDFDocumentView::goToPage(const PDFPageGraphicsItem * page, const int alignment /* = Qt::AlignLeft | Qt::AlignTop */)
959 {
960   int pageNum;
961 
962   if (!_pdf_scene || !page || !isPageItem(page))
963     return;
964   pageNum = _pdf_scene->pageNumFor(page);
965   if (pageNum == _currentPage)
966     return;
967 
968   PDFPageGraphicsItem *oldPage = (PDFPageGraphicsItem*)_pdf_scene->pageAt(_currentPage);
969 
970   if (_pageMode != PageMode_Presentation) {
971     QRectF viewRect(mapToScene(QRect(QPoint(0, 0), viewport()->size())).boundingRect());
972 
973     // Note: This function must work if oldPage == NULL (e.g., during start up)
974     if (oldPage && isPageItem(oldPage))
975       viewRect = oldPage->mapRectFromScene(viewRect);
976     else {
977       // If we don't have an oldPage for whatever reason (e.g., during start up)
978       // we default to the top left corner of newPage instead
979       viewRect = page->mapRectFromScene(viewRect);
980       viewRect.moveTopLeft(QPointF(0, 0));
981     }
982 
983     switch (alignment & Qt::AlignHorizontal_Mask) {
984       case Qt::AlignLeft:
985         viewRect.moveLeft(page->boundingRect().left());
986         break;
987       case Qt::AlignRight:
988         viewRect.moveRight(page->boundingRect().right());
989         break;
990       case Qt::AlignHCenter:
991         viewRect.moveCenter(QPointF(page->boundingRect().center().x(), viewRect.center().y()));
992         break;
993       default:
994         // without (valid) alignment, we don't do anything
995         break;
996     }
997     switch (alignment & Qt::AlignVertical_Mask) {
998       case Qt::AlignTop:
999         viewRect.moveTop(page->boundingRect().top());
1000         break;
1001       case Qt::AlignBottom:
1002         viewRect.moveBottom(page->boundingRect().bottom());
1003         break;
1004       case Qt::AlignVCenter:
1005         viewRect.moveCenter(QPointF(viewRect.center().x(), page->boundingRect().center().y()));
1006         break;
1007       default:
1008         // without (valid) alignment, we don't do anything
1009         break;
1010     }
1011 
1012     if (_pageMode == PageMode_SinglePage) {
1013       _pdf_scene->showOnePage(page);
1014       maybeUpdateSceneRect();
1015     }
1016 
1017     viewRect = page->mapRectToScene(viewRect);
1018     // Note: ensureVisible seems to have a small glitch. Even if the passed
1019     // `viewRect` is identical, the result may depend on the view's previous state
1020     // if the margins are not -1. However, -1 margins don't work during the
1021     // initialization when the viewport doesn't have its final size yet (for
1022     // whatever reasons, the end result is a view centered on the scene).
1023     // So we use centerOn for now which should give the same result since
1024     // viewRect has the same size as the viewport.
1025   //  ensureVisible(viewRect, -1, -1);
1026     centerOn(viewRect.center());
1027     _currentPage = pageNum;
1028   }
1029   else { // _pageMode != PageMode_Presentation
1030     double oldXres = QApplication::desktop()->physicalDpiX() * _zoomLevel;
1031     double oldYres = QApplication::desktop()->physicalDpiY() * _zoomLevel;
1032     _pdf_scene->showOnePage(page);
1033     _currentPage = pageNum;
1034     maybeUpdateSceneRect();
1035     zoomFitWindow();
1036     double xres = QApplication::desktop()->physicalDpiX() * _zoomLevel;
1037     double yres = QApplication::desktop()->physicalDpiY() * _zoomLevel;
1038     QSharedPointer<Backend::Page> backendPage(page->page().toStrongRef());
1039 
1040     if (backendPage && backendPage->transition()) {
1041       backendPage->transition()->reset();
1042       // Setting listener = NULL in calls to getTileImage to force synchronous
1043       // rendering
1044       if (oldPage) {
1045         QSharedPointer<Backend::Page> oldBackendPage(oldPage->page().toStrongRef());
1046         backendPage->transition()->start(*(oldBackendPage->getTileImage(NULL, oldXres, oldYres)), *(backendPage->getTileImage(NULL, xres, yres)));
1047       }
1048     }
1049   }
1050   emit changedPage(_currentPage);
1051 }
1052 
1053 // TODO: Test
1054 // deprecated/unused/unmaintained
1055 // missing: oldPage handling/checks, presentation mode
goToPage(const PDFPageGraphicsItem * page,const QPointF anchor,const int alignment)1056 void PDFDocumentView::goToPage(const PDFPageGraphicsItem * page, const QPointF anchor, const int alignment /* = Qt::AlignHCenter | Qt::AlignVCenter */)
1057 {
1058   int pageNum;
1059 
1060   if (!_pdf_scene || !page || !isPageItem(page))
1061     return;
1062   pageNum = _pdf_scene->pageNumFor(page);
1063   if (pageNum == _currentPage)
1064     return;
1065 
1066   QRectF viewRect(mapToScene(QRect(QPoint(0, 0), viewport()->size())).boundingRect());
1067 
1068   // Transform to item coordinates
1069   viewRect = page->mapRectFromScene(viewRect);
1070 
1071   switch (alignment & Qt::AlignHorizontal_Mask) {
1072     case Qt::AlignLeft:
1073       viewRect.moveLeft(anchor.x());
1074       break;
1075     case Qt::AlignRight:
1076       viewRect.moveRight(anchor.x());
1077       break;
1078     case Qt::AlignHCenter:
1079     default:
1080       viewRect.moveCenter(QPointF(anchor.x(), viewRect.center().y()));
1081       break;
1082   }
1083   switch (alignment & Qt::AlignVertical_Mask) {
1084     case Qt::AlignTop:
1085       viewRect.moveTop(anchor.y());
1086       break;
1087     case Qt::AlignBottom:
1088       viewRect.moveBottom(anchor.y());
1089       break;
1090     case Qt::AlignVCenter:
1091     default:
1092       viewRect.moveCenter(QPointF(viewRect.center().x(), anchor.y()));
1093       break;
1094   }
1095 
1096   if (_pageMode == PageMode_SinglePage) {
1097     _pdf_scene->showOnePage(page);
1098     maybeUpdateSceneRect();
1099   }
1100 
1101   viewRect = page->mapRectToScene(viewRect);
1102   // Note: ensureVisible seems to have a small glitch. Even if the passed
1103   // `viewRect` is identical, the result may depend on the view's previous state
1104   // if the margins are not -1. However, -1 margins don't work during the
1105   // initialization when the viewport doesn't have its final size yet (for
1106   // whatever reasons, the end result is a view centered on the scene).
1107   // So we use centerOn for now which should give the same result since
1108   // viewRect has the same size as the viewport.
1109 //  ensureVisible(viewRect, -1, -1);
1110   centerOn(viewRect.center());
1111 
1112   _currentPage = pageNum;
1113   emit changedPage(_currentPage);
1114 }
1115 
goToPage(const PDFPageGraphicsItem * page,const QRectF view,const bool mayZoom)1116 void PDFDocumentView::goToPage(const PDFPageGraphicsItem * page, const QRectF view, const bool mayZoom /* = false */)
1117 {
1118   if (!page || page->page().isNull())
1119     return;
1120 
1121   // We must check if rect is valid, not view, as the latter usually has
1122   // negative height due to the inverted pdf coordinate system (y axis is up,
1123   // not down)
1124   QRectF rect(page->mapRectToScene(QRectF(page->mapFromPage(view.topLeft()), \
1125                                           page->mapFromPage(view.bottomRight()))));
1126   if (!rect.isValid())
1127     return;
1128 
1129   if (_pageMode == PageMode_Presentation) {
1130     _pdf_scene->showOnePage(page);
1131     maybeUpdateSceneRect();
1132     zoomFitWindow();
1133     // view is ignored in presentation mode as we always zoom to fit the window
1134   }
1135   else {
1136     if (_pageMode == PageMode_SinglePage) {
1137       _pdf_scene->showOnePage(page);
1138       maybeUpdateSceneRect();
1139     }
1140 
1141     if (mayZoom) {
1142       fitInView(rect, Qt::KeepAspectRatio);
1143       _zoomLevel = transform().m11();
1144       emit changedZoom(_zoomLevel);
1145     }
1146     else
1147       centerOn(rect.center());
1148   }
1149 
1150   if (_currentPage != page->pageNum()) {
1151     _currentPage = page->pageNum();
1152     emit changedPage(_currentPage);
1153   }
1154 }
1155 
pdfActionTriggered(const PDFAction * action)1156 void PDFDocumentView::pdfActionTriggered(const PDFAction * action)
1157 {
1158   if (!action)
1159     return;
1160 
1161   // Propagate link signals so that the outside world doesn't have to care about
1162   // our internal implementation (document/view structure, etc.)
1163   switch (action->type()) {
1164     case PDFAction::ActionTypeGoTo:
1165       {
1166         const PDFGotoAction * actionGoto = static_cast<const PDFGotoAction*>(action);
1167         // TODO: Possibly handle other properties of destination() (e.g.,
1168         // viewport settings, zoom level, etc.)
1169         // Note: if this action requires us to open other files (possible
1170         // security issue) or to create a new window, we need to propagate this
1171         // up the hierarchy. Otherwise we can handle it ourselves here.
1172         if (actionGoto->isRemote() || actionGoto->openInNewWindow())
1173           emit requestOpenPdf(actionGoto->filename(), actionGoto->destination(), actionGoto->openInNewWindow());
1174         else {
1175           Q_ASSERT(_pdf_scene != NULL);
1176           Q_ASSERT(!_pdf_scene->document().isNull());
1177           QSharedPointer<Backend::Document> doc(_pdf_scene->document().toStrongRef());
1178           if (!doc)
1179             break;
1180           PDFDestination dest = doc->resolveDestination(actionGoto->destination());
1181           goToPDFDestination(dest);
1182         }
1183       }
1184       break;
1185     case PDFAction::ActionTypeURI:
1186       {
1187         const PDFURIAction * actionURI = static_cast<const PDFURIAction*>(action);
1188         emit requestOpenUrl(actionURI->url());
1189       }
1190       break;
1191     case PDFAction::ActionTypeLaunch:
1192       {
1193         const PDFLaunchAction * actionLaunch = static_cast<const PDFLaunchAction*>(action);
1194         emit requestExecuteCommand(actionLaunch->command());
1195       }
1196       break;
1197     // **TODO:**
1198     // We don't handle other actions yet, but the ActionTypes Quit, Presentation,
1199     // EndPresentation, Find, GoToPage, Close, and Print should be propagated to
1200     // the outside world
1201     default:
1202       // All other link types are currently not supported
1203       break;
1204   }
1205 }
1206 
switchInterfaceLocale(const QLocale & newLocale)1207 void PDFDocumentView::switchInterfaceLocale(const QLocale & newLocale)
1208 {
1209   // TODO: Allow for a custom directory for .qm files (i.e., one in the
1210   // filesystem, instead of the embedded resources)
1211   // Avoid (re-)installing the same translator multiple times (e.g., if several
1212   // PDFDocumentView objects are used in the same application simultaneously
1213   if (_translatorLanguage == newLocale.name())
1214     return;
1215 
1216   // Remove the old translator (if any)
1217   if (_translator) {
1218     QApplication::instance()->removeTranslator(_translator);
1219     _translator->deleteLater();
1220     _translator = NULL;
1221   }
1222 
1223   _translatorLanguage = newLocale.name();
1224 
1225   _translator = new QTranslator();
1226   if (_translator->load(QString::fromUtf8("QtPDF_%1").arg(newLocale.name()), QString::fromUtf8(":/trans")))
1227     QApplication::instance()->installTranslator(_translator);
1228   else {
1229     _translator->deleteLater();
1230     _translator = NULL;
1231   }
1232 
1233   // The language and translator are currently not used but are accessed here so
1234   // they show up in the .ts files.
1235   QString lang = QString::fromUtf8(QT_TRANSLATE_NOOP("QtPDF", "[language name]"));
1236   QString translator = QString::fromUtf8(QT_TRANSLATE_NOOP("QtPDF", "[translator's name/email]"));
1237 }
1238 
reinitializeFromScene()1239 void PDFDocumentView::reinitializeFromScene()
1240 {
1241   if (_pdf_scene) {
1242     _lastPage = _pdf_scene->lastPage();
1243     if (_lastPage <= 0)
1244       _currentPage = -1;
1245     else {
1246       if (_currentPage < 0)
1247         _currentPage = 0;
1248       if (_currentPage >= _lastPage)
1249         _currentPage = _lastPage - 1;
1250     }
1251   }
1252   else {
1253     _lastPage = -1;
1254     _currentPage = -1;
1255   }
1256   // Ensure the text selection marker is reset (if any) as it holds pointers to
1257   // page items (highlight path, boxes) that are now changed and/or destroyed.
1258   DocumentTool::Select * selectTool = static_cast<DocumentTool::Select*>(getToolByType(DocumentTool::AbstractTool::Tool_Select));
1259   if (selectTool)
1260     selectTool->pageDestroyed();
1261   // Ensure (old) search data is destroyed as well
1262   if (!_searchResultWatcher.isFinished())
1263     _searchResultWatcher.cancel();
1264   _searchResults.clear();
1265   _currentSearchResult = -1;
1266   // Also reset _searchString. Otherwise the next search for the same string
1267   // will assume the search has already been run (without results as
1268   // _searchResults is empty) and won't run it again on the new scene data.
1269   _searchString = QString();
1270 }
1271 
1272 
registerTool(DocumentTool::AbstractTool * tool)1273 void PDFDocumentView::registerTool(DocumentTool::AbstractTool * tool)
1274 {
1275   int i;
1276 
1277   if (!tool)
1278     return;
1279 
1280   // Remove any identical tools
1281   for (i = 0; i < _tools.size(); ++i) {
1282     if (_tools[i] && *_tools[i] == *tool) {
1283       delete _tools[i];
1284       _tools.remove(i);
1285       --i;
1286     }
1287   }
1288   // Add the new tool
1289   _tools.append(tool);
1290 }
1291 
getToolByType(const DocumentTool::AbstractTool::Type type)1292 DocumentTool::AbstractTool* PDFDocumentView::getToolByType(const DocumentTool::AbstractTool::Type type)
1293 {
1294   foreach(DocumentTool::AbstractTool * tool, _tools) {
1295     if (tool && tool->type() == type)
1296       return tool;
1297   }
1298   return NULL;
1299 }
1300 
1301 
1302 
1303 // Event Handlers
1304 // --------------
1305 
1306 // Keep track of the current page by overloading the widget paint event.
paintEvent(QPaintEvent * event)1307 void PDFDocumentView::paintEvent(QPaintEvent *event)
1308 {
1309   Super::paintEvent(event);
1310 
1311   // After `QGraphicsView` has taken care of updates to this widget, find the
1312   // currently displayed page. We do this by grabbing all items that are
1313   // currently within the bounds of the viewport's top half. We take the
1314   // first item found to be the "current page".
1315   if (_pdf_scene) {
1316     QRect pageBbox = viewport()->rect();
1317     pageBbox.setHeight(0.5 * pageBbox.height());
1318     int nextCurrentPage = _pdf_scene->pageNumAt(mapToScene(pageBbox));
1319 
1320     if ( nextCurrentPage != _currentPage && nextCurrentPage >= 0 && nextCurrentPage < _lastPage )
1321     {
1322       _currentPage = nextCurrentPage;
1323       emit changedPage(_currentPage);
1324     }
1325   }
1326 
1327   if (_armedTool)
1328     _armedTool->paintEvent(event);
1329 }
1330 
keyPressEvent(QKeyEvent * event)1331 void PDFDocumentView::keyPressEvent(QKeyEvent *event)
1332 {
1333   // FIXME: No moving while tools are active?
1334   switch ( event->key() )
1335   {
1336     case Qt::Key_Home:
1337       goFirst();
1338       event->accept();
1339       break;
1340 
1341     case Qt::Key_End:
1342       goLast();
1343       event->accept();
1344       break;
1345 
1346     case Qt::Key_PageUp:
1347     case Qt::Key_PageDown:
1348     case Qt::Key_Up:
1349     case Qt::Key_Down:
1350     case Qt::Key_Left:
1351     case Qt::Key_Right:
1352       // Check to see if we need to jump to the next page in single page mode.
1353       if ( pageMode() == PageMode_SinglePage || pageMode() == PageMode_Presentation ) {
1354         int scrollStep, scrollPos = verticalScrollBar()->value();
1355 
1356         if ( event->key() == Qt::Key_PageUp || event->key() == Qt::Key_PageDown )
1357           scrollStep = verticalScrollBar()->pageStep();
1358         else
1359           scrollStep = verticalScrollBar()->singleStep();
1360 
1361 
1362         // Take no action on the first and last page so that PageUp/Down can
1363         // move the view right up to the page boundary.
1364         if (
1365           (event->key() == Qt::Key_PageUp || event->key() == Qt::Key_Up) &&
1366           (scrollPos - scrollStep) <= verticalScrollBar()->minimum() &&
1367           _currentPage > 0
1368         ) {
1369           goPrev();
1370           event->accept();
1371           break;
1372         } else if (
1373           (event->key() == Qt::Key_PageDown || event->key() == Qt::Key_Down) &&
1374           (scrollPos + scrollStep) >= verticalScrollBar()->maximum() &&
1375           _currentPage < _lastPage
1376         ) {
1377           goNext();
1378           event->accept();
1379           break;
1380         }
1381       }
1382 
1383       // Deliberate fall-through; we only override the movement keys if a tool is
1384       // currently in use or the view is in single page mode and the movement
1385       // would cross a page boundary.
1386     default:
1387       Super::keyPressEvent(event);
1388       break;
1389   }
1390   // If we have an armed tool, pass the event on to it
1391   // Note: by default, PDFDocumentTool::keyPressEvent() calls maybeArmTool() if
1392   // it doesn't handle the event
1393   if (_armedTool)
1394     _armedTool->keyPressEvent(event);
1395   // If there is no currently armed tool, maybe we can arm one now
1396   else
1397     maybeArmTool(Qt::LeftButton + event->modifiers());
1398 }
1399 
keyReleaseEvent(QKeyEvent * event)1400 void PDFDocumentView::keyReleaseEvent(QKeyEvent *event)
1401 {
1402   // If we have an armed tool, pass the event on to it
1403   // Note: by default, PDFDocumentTool::keyReleaseEvent() calls maybeArmTool() if
1404   // it doesn't handle the event
1405   if(_armedTool)
1406     _armedTool->keyReleaseEvent(event);
1407   else
1408     maybeArmTool(Qt::LeftButton + event->modifiers());
1409 }
1410 
mousePressEvent(QMouseEvent * event)1411 void PDFDocumentView::mousePressEvent(QMouseEvent * event)
1412 {
1413   Super::mousePressEvent(event);
1414 
1415   // Don't do anything if the event was handled elsewhere (e.g., by a
1416   // PDFLinkGraphicsItem)
1417   if (event->isAccepted())
1418     return;
1419 
1420   // Maybe arm a new tool, depending on the mouse buttons and keyboard modifiers.
1421   // This is particularly relevant if the current widget is not the focussed
1422   // widget. In that case, mousePressEvent() will focus this widget, but any
1423   // keyboard modifiers may require a new tool to fire.
1424   // In the typical case, the correct tool will already be armed, so the call to
1425   // maybeArmTool will effectively be a no-op.
1426   maybeArmTool(event->buttons() | event->modifiers());
1427 
1428   DocumentTool::AbstractTool * oldArmed = _armedTool;
1429 
1430   if(_armedTool)
1431     _armedTool->mousePressEvent(event);
1432 
1433   // This mouse event may have armed a new tool (either explicitly, or because
1434   // the previously armed tool passed it on to maybeArmTool). In that case, we
1435   // need to pass it on to the newly armed tool
1436   if (_armedTool && _armedTool != oldArmed)
1437     _armedTool->mousePressEvent(event);
1438 }
1439 
mouseMoveEvent(QMouseEvent * event)1440 void PDFDocumentView::mouseMoveEvent(QMouseEvent * event)
1441 {
1442   if(_armedTool)
1443     _armedTool->mouseMoveEvent(event);
1444   Super::mouseMoveEvent(event);
1445 }
1446 
mouseReleaseEvent(QMouseEvent * event)1447 void PDFDocumentView::mouseReleaseEvent(QMouseEvent * event)
1448 {
1449   Super::mouseReleaseEvent(event);
1450 
1451   if(_armedTool)
1452     _armedTool->mouseReleaseEvent(event);
1453   else
1454     maybeArmTool(event->buttons() | event->modifiers());
1455 }
1456 
wheelEvent(QWheelEvent * event)1457 void PDFDocumentView::wheelEvent(QWheelEvent * event)
1458 {
1459   int delta = event->delta();
1460 
1461   if (event->orientation() == Qt::Vertical && event->buttons() == Qt::NoButton && event->modifiers() == Qt::ControlModifier) {
1462     // TODO: Possibly make the Ctrl modifier configurable?
1463     // According to Qt docs, the resolution of delta() is not necessarily the
1464     // same for all mice. delta() returns the rotation in 1/8 degrees. Here, we
1465     // use a zoom factor of 1.5 every 15 degrees (= delta() == 120, which seems
1466     // to be a widespread default resolution).
1467     // TODO: for high-resolution mice, this may trigger many small zooms,
1468     // resulting in the rendering of many intermediate resolutions. This can
1469     // cause a lagging display and can potentially fill the pdf cache.
1470     zoomBy(pow(1.5, delta / 120.), QGraphicsView::AnchorUnderMouse);
1471     event->accept();
1472     return;
1473   }
1474   if (event->modifiers() == Qt::ShiftModifier) {
1475     // If "Shift" (and only that modifier) is pressed, swap orientations
1476     // (e.g., to allow horizontal scrolling)
1477     // TODO: Possibly make the Shift modifier configurable?
1478     event->accept();
1479     QWheelEvent newEvent(event->pos(), event->delta(), event->buttons(), Qt::NoModifier, (event->orientation() == Qt::Vertical ? Qt::Horizontal : Qt::Vertical));
1480     wheelEvent(&newEvent);
1481     return;
1482   }
1483   if (event->orientation() == Qt::Vertical && (pageMode() == PageMode_SinglePage || pageMode() == PageMode_Presentation)) {
1484     // In single page mode we need to flip to the next page if the scroll bar
1485     // is a the top or bottom of it's range.`
1486     int scrollPos = verticalScrollBar()->value();
1487     if ( delta < 0 && scrollPos == verticalScrollBar()->maximum() ) {
1488       goNext();
1489 
1490       event->accept();
1491       return;
1492     } else if ( delta > 0 && scrollPos == verticalScrollBar()->minimum() ) {
1493       goPrev();
1494 
1495       event->accept();
1496       return;
1497     }
1498   }
1499 
1500   Super::wheelEvent(event);
1501 }
1502 
changeEvent(QEvent * event)1503 void PDFDocumentView::changeEvent(QEvent * event)
1504 {
1505   if (event && event->type() == QEvent::LanguageChange) {
1506     if (_pdf_scene)
1507       _pdf_scene->retranslateUi();
1508   }
1509   Super::changeEvent(event);
1510 }
1511 
armTool(const DocumentTool::AbstractTool::Type toolType)1512 void PDFDocumentView::armTool(const DocumentTool::AbstractTool::Type toolType)
1513 {
1514   armTool(getToolByType(toolType));
1515 }
1516 
armTool(DocumentTool::AbstractTool * tool)1517 void PDFDocumentView::armTool(DocumentTool::AbstractTool * tool)
1518 {
1519   if (_armedTool == tool)
1520     return;
1521   if (_armedTool)
1522     disarmTool();
1523   if (tool)
1524     tool->arm();
1525   _armedTool = tool;
1526 }
1527 
disarmTool()1528 void PDFDocumentView::disarmTool()
1529 {
1530   if (!_armedTool)
1531     return;
1532   _armedTool->disarm();
1533   _armedTool = NULL;
1534 }
1535 
1536 
1537 // PDFDocumentMagnifierView
1538 // ========================
1539 //
PDFDocumentMagnifierView(PDFDocumentView * parent)1540 PDFDocumentMagnifierView::PDFDocumentMagnifierView(PDFDocumentView *parent /* = 0 */) :
1541   Super(parent),
1542   _parent_view(parent),
1543   _zoomLevel(1.0),
1544   _zoomFactor(2.0),
1545   _shape(DocumentTool::MagnifyingGlass::Magnifier_Circle),
1546   _size(300)
1547 {
1548   // the magnifier should initially be hidden
1549   hide();
1550 
1551   // suppress scrollbars
1552   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1553   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1554   // suppress any border styling (which doesn't work with a mask, e.g., for a
1555   // circular magnifier)
1556   setFrameShape(QFrame::NoFrame);
1557 
1558   if (parent) {
1559     // transfer some settings from the parent view
1560     setBackgroundRole(parent->backgroundRole());
1561     setAlignment(parent->alignment());
1562   }
1563 
1564   setShape(_shape);
1565 }
1566 
prepareToShow()1567 void PDFDocumentMagnifierView::prepareToShow()
1568 {
1569   qreal zoomLevel;
1570 
1571   if (!_parent_view)
1572     return;
1573 
1574   // Ensure we have the same scene
1575   if (_parent_view->scene() != scene())
1576     setScene(_parent_view->scene());
1577   // Fix the zoom
1578   zoomLevel = _parent_view->zoomLevel() * _zoomFactor;
1579   if (zoomLevel != _zoomLevel)
1580     scale(zoomLevel / _zoomLevel, zoomLevel / _zoomLevel);
1581   _zoomLevel = zoomLevel;
1582   // Ensure we have enough padding at the border that we can display the
1583   // magnifier even beyond the edge
1584   setSceneRect(_parent_view->sceneRect().adjusted(-width() / _zoomLevel, -height() / _zoomLevel, width() / _zoomLevel, height() / _zoomLevel));
1585 }
1586 
setZoomFactor(const qreal zoomFactor)1587 void PDFDocumentMagnifierView::setZoomFactor(const qreal zoomFactor)
1588 {
1589   _zoomFactor = zoomFactor;
1590   // Actual handling of zoom levels happens in prepareToShow, as the zoom level
1591   // of the parent cannot change while the magnifier is shown
1592 }
1593 
setPosition(const QPoint pos)1594 void PDFDocumentMagnifierView::setPosition(const QPoint pos)
1595 {
1596   move(pos.x() - width() / 2, pos.y() - height() / 2);
1597   centerOn(_parent_view->mapToScene(pos));
1598 }
1599 
setSizeAndShape(const int size,const DocumentTool::MagnifyingGlass::MagnifierShape shape)1600 void PDFDocumentMagnifierView::setSizeAndShape(const int size, const DocumentTool::MagnifyingGlass::MagnifierShape shape)
1601 {
1602   _size = size;
1603   _shape = shape;
1604 
1605   switch (shape) {
1606     case DocumentTool::MagnifyingGlass::Magnifier_Rectangle:
1607       setFixedSize(size * 4 / 3, size);
1608       clearMask();
1609       // There is a bug that affects masking of QAbstractScrollArea and its
1610       // subclasses:
1611       //
1612       //   https://bugreports.qt.nokia.com/browse/QTBUG-7150
1613       //
1614       // The workaround is to explicitly mask the viewport. As of Qt 4.7.4, this
1615       // bug is still present. As of Qt 5, it also seems to affect other
1616       // platforms
1617       viewport()->clearMask();
1618       break;
1619     case DocumentTool::MagnifyingGlass::Magnifier_Circle:
1620       setFixedSize(size, size);
1621       setMask(QRegion(rect(), QRegion::Ellipse));
1622       // Hack to fix QTBUG-7150
1623       viewport()->setMask(QRegion(rect(), QRegion::Ellipse));
1624       break;
1625   }
1626   _dropShadow = QPixmap();
1627 }
1628 
paintEvent(QPaintEvent * event)1629 void PDFDocumentMagnifierView::paintEvent(QPaintEvent * event)
1630 {
1631   Super::paintEvent(event);
1632 
1633   // Draw our custom border
1634   // Note that QGraphicsView is derived from QAbstractScrollArea, but we are not
1635   // asked to paint on that but on the widget it contains. Therefore, we can't
1636   // just say QPainter(this)
1637   QPainter painter(viewport());
1638 
1639   painter.setRenderHint(QPainter::Antialiasing);
1640 
1641   QPen pen(Qt::gray);
1642   pen.setWidth(2);
1643 
1644   QRect rect(this->rect());
1645 
1646   painter.setPen(pen);
1647   switch(_shape) {
1648     case DocumentTool::MagnifyingGlass::Magnifier_Rectangle:
1649       painter.drawRect(rect);
1650       break;
1651     case DocumentTool::MagnifyingGlass::Magnifier_Circle:
1652       // Ensure we're drawing where we should, regardless how the window system
1653       // handles masks
1654       painter.setClipRegion(mask());
1655       // **TODO:** It seems to be necessary to adjust the window rect by one pixel
1656       // to draw an evenly wide border; is there a better way?
1657       rect.adjust(1, 1, 0, 0);
1658       painter.drawEllipse(rect);
1659       break;
1660   }
1661 
1662   // **Note:** We don't/can't draw the drop-shadow here. The reason is that we
1663   // rely on Super::paintEvent to do the actual rendering, which constructs its
1664   // own QPainter so we can't do clipping. Resetting the mask is no option,
1665   // either, as that may trigger an update (recursion!).
1666   // Alternatively, we could fill the border with the background from the
1667   // underlying window. But _parent_view->render() no option, because it
1668   // requires QWidget::DrawChildren (apparently the QGraphicsItems are
1669   // implemented as child widgets) which would cause a recursion again (the
1670   // magnifier is also a child widget!). Calling scene()->render() is no option,
1671   // either, because then render requests for unmagnified images would originate
1672   // from here, which would break the current implementation of
1673   // PDFPageGraphicsItem::paint().
1674   // Instead, drop-shadows are drawn in PDFDocumentView::paintEvent(), invoking
1675   // PDFDocumentMagnifierView::dropShadow().
1676 }
1677 
1678 // Modelled after http://labs.qt.nokia.com/2009/10/07/magnifying-glass
dropShadow()1679 QPixmap& PDFDocumentMagnifierView::dropShadow()
1680 {
1681   if (!_dropShadow.isNull())
1682     return _dropShadow;
1683 
1684   int padding = 10;
1685   _dropShadow = QPixmap(width() + 2 * padding, height() + 2 * padding);
1686 
1687   _dropShadow.fill(Qt::transparent);
1688 
1689   switch(_shape) {
1690     case DocumentTool::MagnifyingGlass::Magnifier_Rectangle:
1691       {
1692         QPainterPath path;
1693         QRectF boundingRect(_dropShadow.rect().adjusted(0, 0, -1, -1));
1694         QLinearGradient gradient(boundingRect.center(), QPointF(0.0, boundingRect.center().y()));
1695         gradient.setSpread(QGradient::ReflectSpread);
1696         QGradientStops stops;
1697         QColor color(Qt::black);
1698         color.setAlpha(64);
1699         stops.append(QGradientStop(1.0 - padding * 2.0 / _dropShadow.width(), color));
1700         color.setAlpha(0);
1701         stops.append(QGradientStop(1.0, color));
1702 
1703         QPainter shadow(&_dropShadow);
1704         shadow.setRenderHint(QPainter::Antialiasing);
1705 
1706         // paint horizontal gradient
1707         gradient.setStops(stops);
1708 
1709         path = QPainterPath();
1710         path.moveTo(boundingRect.topLeft());
1711         path.lineTo(boundingRect.topLeft() + QPointF(padding, padding));
1712         path.lineTo(boundingRect.bottomRight() + QPointF(-padding, -padding));
1713         path.lineTo(boundingRect.bottomRight());
1714         path.lineTo(boundingRect.topRight());
1715         path.lineTo(boundingRect.topRight() + QPointF(-padding, padding));
1716         path.lineTo(boundingRect.bottomLeft() + QPointF(padding, -padding));
1717         path.lineTo(boundingRect.bottomLeft());
1718         path.closeSubpath();
1719 
1720         shadow.fillPath(path, gradient);
1721 
1722         // paint vertical gradient
1723         stops[0].first = 1.0 - padding * 2.0 / _dropShadow.height();
1724         gradient.setStops(stops);
1725 
1726         path = QPainterPath();
1727         path.moveTo(boundingRect.topLeft());
1728         path.lineTo(boundingRect.topLeft() + QPointF(padding, padding));
1729         path.lineTo(boundingRect.bottomRight() + QPointF(-padding, -padding));
1730         path.lineTo(boundingRect.bottomRight());
1731         path.lineTo(boundingRect.bottomLeft());
1732         path.lineTo(boundingRect.bottomLeft() + QPointF(padding, -padding));
1733         path.lineTo(boundingRect.topRight() + QPointF(-padding, padding));
1734         path.lineTo(boundingRect.topRight());
1735         path.closeSubpath();
1736 
1737         gradient.setFinalStop(QPointF(QRectF(_dropShadow.rect()).center().x(), 0.0));
1738         shadow.fillPath(path, gradient);
1739       }
1740       break;
1741     case DocumentTool::MagnifyingGlass::Magnifier_Circle:
1742       {
1743         QRadialGradient gradient(QRectF(_dropShadow.rect()).center(), _dropShadow.width() / 2.0, QRectF(_dropShadow.rect()).center());
1744         QColor color(Qt::black);
1745         color.setAlpha(0);
1746         gradient.setColorAt(1.0, color);
1747         color.setAlpha(64);
1748         gradient.setColorAt(1.0 - padding * 2.0 / _dropShadow.width(), color);
1749 
1750         QPainter shadow(&_dropShadow);
1751         shadow.setRenderHint(QPainter::Antialiasing);
1752         shadow.fillRect(_dropShadow.rect(), gradient);
1753       }
1754       break;
1755   }
1756   return _dropShadow;
1757 }
1758 
1759 
1760 // PDFDocumentScene
1761 // ================
1762 //
1763 // A large canvas that manages the layout of QGraphicsItem subclasses. The
1764 // primary items we are concerned with are PDFPageGraphicsItem and
1765 // PDFLinkGraphicsItem.
PDFDocumentScene(QSharedPointer<Backend::Document> a_doc,QObject * parent,const double dpiX,const double dpiY)1766 PDFDocumentScene::PDFDocumentScene(QSharedPointer<Backend::Document> a_doc, QObject *parent /* = 0 */, const double dpiX /* = -1 */, const double dpiY /* = -1 */):
1767   Super(parent),
1768   _doc(a_doc),
1769   _shownPageIdx(-2)
1770 {
1771   Q_ASSERT(a_doc != NULL);
1772   // We need to register a QList<PDFLinkGraphicsItem *> meta-type so we can
1773   // pass it through inter-thread (i.e., queued) connections
1774   qRegisterMetaType< QList<PDFLinkGraphicsItem *> >();
1775 
1776   _dpiX = (dpiX > 0 ? dpiX : QApplication::desktop()->physicalDpiX());
1777   _dpiY = (dpiY > 0 ? dpiY : QApplication::desktop()->physicalDpiY());
1778 
1779   connect(&_pageLayout, SIGNAL(layoutChanged(const QRectF)), this, SLOT(pageLayoutChanged(const QRectF)));
1780 
1781   // Initialize the unlock widget
1782   {
1783     _unlockWidget = new QWidget();
1784     QVBoxLayout * layout = new QVBoxLayout();
1785 
1786     _unlockWidgetLockIcon = new QLabel(_unlockWidget);
1787     _unlockWidgetLockIcon->setPixmap(QPixmap(QString::fromUtf8(":/QtPDF/icons/lock.png")));
1788     _unlockWidgetLockText = new QLabel(_unlockWidget);
1789     _unlockWidgetUnlockButton = new QPushButton(_unlockWidget);
1790 
1791     connect(_unlockWidgetUnlockButton, SIGNAL(clicked()), this, SLOT(doUnlockDialog()));
1792 
1793     layout->addWidget(_unlockWidgetLockIcon);
1794     layout->addWidget(_unlockWidgetLockText);
1795     layout->addSpacing(20);
1796     layout->addWidget(_unlockWidgetUnlockButton);
1797 
1798     layout->setAlignment(_unlockWidgetLockIcon, Qt::AlignHCenter);
1799     layout->setAlignment(_unlockWidgetLockText, Qt::AlignHCenter);
1800     layout->setAlignment(_unlockWidgetUnlockButton, Qt::AlignHCenter);
1801 
1802     _unlockWidget->setLayout(layout);
1803     retranslateUi();
1804   }
1805 
1806   // We must not respond to a QFileSystemWatcher::timeout() signal directly as
1807   // file operations need not be atomic. I.e., QFileSystemWatcher could fire
1808   // several times between the begging of a change to the file and its
1809   // completion. Hence, we use a timer to delay the call to reloadDocument(). If
1810   // the QFileSystemWatcher fires several times, the timer gets restarted every
1811   // time.
1812   _reloadTimer.setSingleShot(true);
1813   _reloadTimer.setInterval(500);
1814   connect(&_reloadTimer, SIGNAL(timeout()), this, SLOT(reloadDocument()));
1815   connect(&_fileWatcher, SIGNAL(fileChanged(const QString &)), &_reloadTimer, SLOT(start()));
1816   setWatchForDocumentChangesOnDisk(true);
1817 
1818   reinitializeScene();
1819 }
1820 
handleActionEvent(const PDFActionEvent * action_event)1821 void PDFDocumentScene::handleActionEvent(const PDFActionEvent * action_event)
1822 {
1823   if (!action_event || !action_event->action)
1824     return;
1825 
1826   switch (action_event->action->type() )
1827   {
1828     // Link types that we don't handle here but that may be of interest
1829     // elsewhere (note: ActionGoto will be handled by
1830     // PDFDocumentView::pdfActionTriggered)
1831     case PDFAction::ActionTypeGoTo:
1832     case PDFAction::ActionTypeURI:
1833     case PDFAction::ActionTypeLaunch:
1834       break;
1835     default:
1836       // All other link types are currently not supported
1837       return;
1838   }
1839   // Translate into a signal that can be handled by some other part of the
1840   // program, such as a `PDFDocumentView`.
1841   emit pdfActionTriggered(action_event->action);
1842 }
1843 
1844 
1845 // Accessors
1846 // ---------
1847 
document()1848 QWeakPointer<Backend::Document> PDFDocumentScene::document() { return _doc.toWeakRef(); }
pages()1849 QList<QGraphicsItem*> PDFDocumentScene::pages() { return _pages; };
1850 
1851 // Overloaded method that returns all page objects inside a given rectangular
1852 // area. First, `items` is used to grab all items inside the rectangle. This
1853 // list is then filtered by item type so that it contains only references to
1854 // `PDFPageGraphicsItem` objects.
pages(const QPolygonF & polygon)1855 QList<QGraphicsItem*> PDFDocumentScene::pages(const QPolygonF &polygon)
1856 {
1857   QList<QGraphicsItem*> pageList = items(polygon);
1858   QtConcurrent::blockingFilter(pageList, isPageItem);
1859 
1860   return pageList;
1861 };
1862 
1863 // Convenience function to avoid moving the complete list of pages around
1864 // between functions if only one page is needed
pageAt(const int idx)1865 QGraphicsItem* PDFDocumentScene::pageAt(const int idx)
1866 {
1867   if (idx < 0 || idx >= _pages.size())
1868     return NULL;
1869   return _pages[idx];
1870 }
1871 
1872 // Overloaded method that returns all page objects at a given point. First,
1873 // `items` is used to grab all items at the point. This list is then filtered by
1874 // item type so that it contains only references to `PDFPageGraphicsItem` objects.
pageAt(const QPointF & pt)1875 QGraphicsItem* PDFDocumentScene::pageAt(const QPointF &pt)
1876 {
1877   QList<QGraphicsItem*> pageList = items(pt);
1878   QtConcurrent::blockingFilter(pageList, isPageItem);
1879 
1880   if (pageList.isEmpty())
1881     return NULL;
1882   return pageList[0];
1883 }
1884 
1885 // This is a convenience function for returning the page number of the first
1886 // page item inside a given area of the scene. If no page is in the specified
1887 // area, -1 is returned.
pageNumAt(const QPolygonF & polygon)1888 int PDFDocumentScene::pageNumAt(const QPolygonF &polygon)
1889 {
1890   QList<QGraphicsItem*> p(pages(polygon));
1891   if (p.isEmpty())
1892     return -1;
1893   return _pages.indexOf(p.first());
1894 }
1895 
1896 // This is a convenience function for returning the page number of the first
1897 // page item at a given point. If no page is in the specified area, -1 is returned.
pageNumAt(const QPointF & pt)1898 int PDFDocumentScene::pageNumAt(const QPointF &pt)
1899 {
1900   return _pages.indexOf(pageAt(pt));
1901 }
1902 
pageNumFor(const PDFPageGraphicsItem * const graphicsItem) const1903 int PDFDocumentScene::pageNumFor(const PDFPageGraphicsItem * const graphicsItem) const
1904 {
1905   // Note: since we store QGraphicsItem* in _pages, we need to remove the const
1906   // or else indexOf() complains during compilation. Since we don't do anything
1907   // with the pointer, this should be safe to do while still remaining the
1908   // const'ness of `graphicsItem`, however.
1909   return _pages.indexOf(const_cast<PDFPageGraphicsItem * const>(graphicsItem));
1910 }
1911 
lastPage()1912 int PDFDocumentScene::lastPage() { return _lastPage; }
1913 
1914 // Event Handlers
1915 // --------------
1916 
1917 // We re-implement the main event handler for the scene so that we can
1918 // translate events generated by child items into signals that can be sent out
1919 // to the rest of the program.
event(QEvent * event)1920 bool PDFDocumentScene::event(QEvent *event)
1921 {
1922   if ( event->type() == PDFActionEvent::ActionEvent )
1923   {
1924     event->accept();
1925     // Cast to a pointer for `PDFActionEvent` so that we can access the `pageNum`
1926     // field.
1927     const PDFActionEvent *action_event = static_cast<const PDFActionEvent*>(event);
1928     handleActionEvent(action_event);
1929     return true;
1930   }
1931 
1932   return Super::event(event);
1933 }
1934 
1935 // Public Slots
1936 // --------------
doUnlockDialog()1937 void PDFDocumentScene::doUnlockDialog()
1938 {
1939   Q_ASSERT(!_doc.isNull());
1940 
1941   bool ok;
1942   // TODO: Maybe use some parent for QInputDialog (and QMessageBox below)
1943   // instead of NULL?
1944   QString password = QInputDialog::getText(NULL, trUtf8("Unlock PDF"), trUtf8("Please enter the password to unlock the PDF"), QLineEdit::Password, QString(), &ok);
1945   if (ok) {
1946     if (_doc->unlock(password)) {
1947       // FIXME: the program crashes in the QGraphicsView::mouseReleaseEvent
1948       // handler (presumably from clicking the "Unlock" button) when
1949       // reinitializeScene() is called immediately. To work around this, delay
1950       // it until control returns to the event queue. Problem: slots connected
1951       // to documentChanged() will receive the new doc, but the scene itself
1952       // will not have changed, yet.
1953       QTimer::singleShot(1, this, SLOT(finishUnlock()));
1954     }
1955     else
1956       QMessageBox::information(NULL, trUtf8("Incorrect password"), trUtf8("The password you entered was incorrect."));
1957   }
1958 }
1959 
retranslateUi()1960 void PDFDocumentScene::retranslateUi()
1961 {
1962   _unlockWidgetLockText->setText(trUtf8("This document is locked. You need a password to open it."));
1963   _unlockWidgetUnlockButton->setText(trUtf8("Unlock"));
1964 
1965   foreach (QGraphicsItem * i, items()) {
1966     if (!i)
1967       continue;
1968     switch (i->type()) {
1969     case PDFLinkGraphicsItem::Type:
1970     {
1971       PDFLinkGraphicsItem * gi = static_cast<PDFLinkGraphicsItem*>(i);
1972       gi->retranslateUi();
1973     }
1974       break;
1975     default:
1976       break;
1977     }
1978   }
1979 }
1980 
1981 // Protected Slots
1982 // --------------
pageLayoutChanged(const QRectF & sceneRect)1983 void PDFDocumentScene::pageLayoutChanged(const QRectF& sceneRect)
1984 {
1985   setSceneRect(sceneRect);
1986   emit pageLayoutChanged();
1987 }
1988 
reinitializeScene()1989 void PDFDocumentScene::reinitializeScene()
1990 {
1991   clear();
1992   _pages.clear();
1993   _pageLayout.clearPages();
1994 
1995   _lastPage = _doc->numPages();
1996   if (!_doc->isValid())
1997     return;
1998   if (_doc->isLocked()) {
1999     // FIXME: Deactivate "normal" user interaction, e.g., zooming, panning, etc.
2000     addWidget(_unlockWidget);
2001   }
2002   else {
2003     // Create a `PDFPageGraphicsItem` for each page in the PDF document and let
2004     // them be layed out by a `PDFPageLayout` instance.
2005     int i;
2006     PDFPageGraphicsItem *pagePtr;
2007 
2008     if (_shownPageIdx >= _lastPage)
2009       _shownPageIdx = _lastPage - 1;
2010 
2011     for (i = 0; i < _lastPage; ++i)
2012     {
2013       pagePtr = new PDFPageGraphicsItem(_doc->page(i), _dpiX, _dpiY);
2014       pagePtr->setVisible(i == _shownPageIdx || _shownPageIdx == -2);
2015       _pages.append(pagePtr);
2016       addItem(pagePtr);
2017       _pageLayout.addPage(pagePtr);
2018     }
2019     _pageLayout.relayout();
2020   }
2021 }
2022 
finishUnlock()2023 void PDFDocumentScene::finishUnlock()
2024 {
2025   reinitializeScene();
2026   emit documentChanged(_doc);
2027 }
2028 
reloadDocument()2029 void PDFDocumentScene::reloadDocument()
2030 {
2031   // If the file referenced by the document no longer exists, do nothing
2032   if(!QFile::exists(_doc->fileName()))
2033     return;
2034 
2035   _doc->reload();
2036   reinitializeScene();
2037   emit documentChanged(_doc.toWeakRef());
2038 }
2039 
2040 
2041 // Other
2042 // -----
showOnePage(const int pageIdx)2043 void PDFDocumentScene::showOnePage(const int pageIdx)
2044 {
2045   int i;
2046 
2047   for (i = 0; i < _pages.size(); ++i) {
2048     if (!isPageItem(_pages[i]))
2049       continue;
2050     if (i == pageIdx) {
2051       _pages[i]->setVisible(true);
2052       _shownPageIdx = pageIdx;
2053     }
2054     else
2055       _pages[i]->setVisible(false);
2056   }
2057 }
2058 
showOnePage(const PDFPageGraphicsItem * page)2059 void PDFDocumentScene::showOnePage(const PDFPageGraphicsItem * page)
2060 {
2061   int i;
2062 
2063   for (i = 0; i < _pages.size(); ++i) {
2064     if (!isPageItem(_pages[i]))
2065       continue;
2066     _pages[i]->setVisible(_pages[i] == page);
2067     if (_pages[i] == page) {
2068       _pages[i]->setVisible(true);
2069       _shownPageIdx = i;
2070     }
2071     else
2072       _pages[i]->setVisible(false);
2073   }
2074 }
2075 
showAllPages()2076 void PDFDocumentScene::showAllPages()
2077 {
2078   int i;
2079 
2080   for (i = 0; i < _pages.size(); ++i) {
2081     if (!isPageItem(_pages[i]))
2082       continue;
2083     _pages[i]->setVisible(true);
2084   }
2085   _shownPageIdx = -2;
2086 }
2087 
setWatchForDocumentChangesOnDisk(const bool doWatch)2088 void PDFDocumentScene::setWatchForDocumentChangesOnDisk(const bool doWatch /* = true */)
2089 {
2090   if (_fileWatcher.files().size() > 0)
2091     _fileWatcher.removePaths(_fileWatcher.files());
2092   if (doWatch) {
2093     _fileWatcher.addPath(_doc->fileName());
2094 #ifdef DEBUG
2095     qDebug() << "Watching" << _doc->fileName();
2096 #endif
2097   }
2098 }
2099 
setResolution(const double dpiX,const double dpiY)2100 void PDFDocumentScene::setResolution(const double dpiX, const double dpiY)
2101 {
2102   if (dpiX > 0)
2103     _dpiX = dpiX;
2104   if (dpiY > 0)
2105     _dpiY = dpiY;
2106 
2107   // FIXME: reinitializing everything seems like overkill
2108   reinitializeScene();
2109 }
2110 
2111 
2112 // PDFPageGraphicsItem
2113 // ===================
2114 
2115 // This class descends from `QGraphicsObject` and implements the on-screen
2116 // representation of `Page` objects.
PDFPageGraphicsItem(QWeakPointer<Backend::Page> a_page,const double dpiX,const double dpiY,QGraphicsItem * parent)2117 PDFPageGraphicsItem::PDFPageGraphicsItem(QWeakPointer<Backend::Page> a_page, const double dpiX, const double dpiY, QGraphicsItem *parent /* = 0 */):
2118   Super(parent),
2119   _page(a_page),
2120 
2121   _pageNum(-1),
2122   _linksLoaded(false),
2123   _annotationsLoaded(false),
2124   _zoomLevel(0.0)
2125 {
2126   _dpiX = (dpiX > 0 ? dpiX : QApplication::desktop()->physicalDpiX());
2127   _dpiY = (dpiY > 0 ? dpiY : QApplication::desktop()->physicalDpiY());
2128 
2129   // So we get information during paint events about what portion of the page
2130   // is visible.
2131   //
2132   // NOTE: This flag needs Qt 4.6 or newer.
2133   setFlags(QGraphicsItem::ItemUsesExtendedStyleOption);
2134 
2135   QSharedPointer<Backend::Page> page(_page.toStrongRef());
2136   if (page) {
2137     _pageNum = page->pageNum();
2138     // Create an empty pixmap that is the same size as the PDF page. This
2139     // allows us to delay the rendering of pages until they actually come into
2140     // view yet still know what the page size is.
2141     _pageSize = page->pageSizeF();
2142     _pageSize.setWidth(_pageSize.width() * _dpiX / 72.0);
2143     _pageSize.setHeight(_pageSize.height() * _dpiY / 72.0);
2144 
2145     // `_pageScale` holds a transformation matrix that can map between normalized
2146     // page coordinates (in the range 0...1) and the coordinate system for this
2147     // graphics item. `_pointScale` is similar, except it maps from coordinates
2148     // expressed in pixels at a resolution of 72 dpi.
2149     _pageScale = QTransform::fromScale(_pageSize.width(), _pageSize.height());
2150     _pointScale = QTransform::fromScale(_dpiX / 72.0, _dpiY / 72.0);
2151   }
2152 }
2153 
boundingRect() const2154 QRectF PDFPageGraphicsItem::boundingRect() const { return QRectF(QPointF(0.0, 0.0), _pageSize); }
type() const2155 int PDFPageGraphicsItem::type() const { return Type; }
2156 
mapFromPage(const QPointF & point) const2157 QPointF PDFPageGraphicsItem::mapFromPage(const QPointF & point) const
2158 {
2159   QSharedPointer<Backend::Page> page(_page.toStrongRef());
2160   if (!page)
2161     return QPointF();
2162   // item coordinates are in pixels
2163   return QPointF(_pageSize.width() * point.x() / page->pageSizeF().width(), \
2164     _pageSize.height() * (1.0 - point.y() / page->pageSizeF().height()));
2165 }
2166 
mapToPage(const QPointF & point) const2167 QPointF PDFPageGraphicsItem::mapToPage(const QPointF & point) const
2168 {
2169   QSharedPointer<Backend::Page> page(_page.toStrongRef());
2170   if (!page)
2171     return QPointF();
2172   // item coordinates are in pixels
2173   return QPointF(page->pageSizeF().width() * point.x() / _pageSize.width(), \
2174     page->pageSizeF().height() * (1.0 - point.y() / _pageSize.height()));
2175 }
2176 
2177 // An overloaded paint method allows us to handle rendering via asynchronous
2178 // calls to backend functions.
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)2179 void PDFPageGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
2180 {
2181   // Really, there is an X scaling factor and a Y scaling factor, but we assume
2182   // that the X scaling factor is equal to the Y scaling factor.
2183   qreal scaleFactor = painter->transform().m11();
2184   QTransform scaleT = QTransform::fromScale(scaleFactor, scaleFactor);
2185   QRect pageRect = scaleT.mapRect(boundingRect()).toAlignedRect(), pageTile;
2186   QSharedPointer<Backend::Page> page(_page.toStrongRef());
2187   QSharedPointer<QImage> renderedPage;
2188 
2189   if (!page)
2190     return;
2191 
2192   // If this is the first time this `PDFPageGraphicsItem` has come into view,
2193   // `_linksLoaded` will be `false`. We then load all of the links on the page.
2194   if ( not _linksLoaded )
2195   {
2196     page->asyncLoadLinks(this);
2197     _linksLoaded = true;
2198   }
2199 
2200   if (!_annotationsLoaded) {
2201     // FIXME: Load annotations asynchronously?
2202     addAnnotations(page->loadAnnotations());
2203     _annotationsLoaded = true;
2204   }
2205 
2206   if ( _zoomLevel != scaleFactor )
2207     _zoomLevel = scaleFactor;
2208 
2209   // get a pointer to the parent view (if any)
2210   PDFDocumentView * view = (widget ? qobject_cast<PDFDocumentView*>(widget->parent()) : NULL);
2211 
2212   painter->save();
2213 
2214   if (view && view->pageMode() == PDFDocumentView::PageMode_Presentation) {
2215     // NOTE: There is no point in clipping here as we always display the whole
2216     // page, anyway. Hence, the images all have the correct size (no tiling) and
2217     // are usually completely visible.
2218 
2219     // The transformation matrix of the `painter` object contains information
2220     // such as the current zoom level of the widget viewing this PDF page. We
2221     // throw away the scaling information because that has already been
2222     // applied during page rendering. (Note: we don't support rotation/skewing,
2223     // so we only care about the translational part)
2224     QTransform pageT = painter->transform();
2225     painter->setTransform(QTransform::fromTranslate(pageT.dx(), pageT.dy()));
2226     if (page->transition() && page->transition()->isRunning()) {
2227       // Get and draw the current frame of the transition
2228       // NOTE: In the (unlikely) case that the two pages we are transitioning
2229       // between are not the same size, the frame image will be padded to
2230       // encompass both pages. In that case, we (may) need to paint the image
2231       // outside the boundaries of this graphics item (unfortunately, that can't
2232       // be helped, but it should not cause too many problems as there should be
2233       // no other items visible below it).
2234       // NOTE: Don't use QRect::center() here to align the respective centers as
2235       // round-off errors can introduce a shift of +-1px.
2236       QImage img(page->transition()->getImage());
2237       QPoint offset((pageRect.width() - img.width()) / 2, (pageRect.height() - img.height()) / 2);
2238       painter->drawImage(offset, img);
2239       // Trigger an update as soon as possible (without recursion) to proceed
2240       // with the animation.
2241       if (widget) {
2242         QTimer::singleShot(1, widget, SLOT(update()));
2243       }
2244     }
2245     else {
2246       // render the whole page synchronously (we don't want "rendering" to show
2247       // up during presentations, and we don't need tiles as we always display
2248       // the full page, anyway).
2249       renderedPage = page->getTileImage(NULL, _dpiX * scaleFactor, _dpiY * scaleFactor);
2250       if (renderedPage)
2251         painter->drawImage(QPoint(0, 0), *renderedPage);
2252     }
2253   }
2254   else { // presentation mode
2255     // Clip to the exposed rectangle to prevent unnecessary drawing operations.
2256     // This can provide up to a 50% speedup depending on the size of the tile.
2257     painter->setClipRect(option->exposedRect);
2258 
2259     // The transformation matrix of the `painter` object contains information
2260     // such as the current zoom level of the widget viewing this PDF page. We
2261     // throw away the scaling information because that has already been
2262     // applied during page rendering. (Note: we don't support rotation/skewing,
2263     // so we only care about the translational part)
2264     QTransform pageT = painter->transform();
2265     painter->setTransform(QTransform::fromTranslate(pageT.dx(), pageT.dy()));
2266 #ifdef DEBUG
2267     // Pen style used to draw the outline of each tile for debugging purposes.
2268     QPen tilePen(Qt::darkGray);
2269     tilePen.setStyle(Qt::DashDotLine);
2270     painter->setPen(tilePen);
2271 #endif
2272 
2273     QRect visibleRect = scaleT.mapRect(option->exposedRect).toAlignedRect();
2274 
2275     int i, imin, imax;
2276     int j, jmin, jmax;
2277 
2278     imin = (visibleRect.left() - pageRect.left()) / TILE_SIZE;
2279     imax = (visibleRect.right() - pageRect.left());
2280     if (imax % TILE_SIZE == 0)
2281       imax /= TILE_SIZE;
2282     else
2283       imax = imax / TILE_SIZE + 1;
2284 
2285     jmin = (visibleRect.top() - pageRect.top()) / TILE_SIZE;
2286     jmax = (visibleRect.bottom() - pageRect.top());
2287     if (jmax % TILE_SIZE == 0)
2288       jmax /= TILE_SIZE;
2289     else
2290       jmax = jmax / TILE_SIZE + 1;
2291 
2292     for (j = jmin; j < jmax; ++j) {
2293       for (i = imin; i < imax; ++i) {
2294         QRect tile(i * TILE_SIZE, j * TILE_SIZE, TILE_SIZE, TILE_SIZE);
2295 
2296         bool useGrayScale = false;
2297         // If we are rendering a PDFDocumentView that has `useGrayScale` set
2298         // respect that setting.
2299         if (view && view->useGrayScale())
2300           useGrayScale = true;
2301         // If we are rendering a PDFDocumentMagnifierView who's parent
2302         // PDFDocumentView has `useGrayScale` set respect that setting.
2303         else if (widget && widget->parent() && widget->parent()->parent()) {
2304           PDFDocumentView * view = (widget ? qobject_cast<PDFDocumentView*>(widget->parent()->parent()) : NULL);
2305           if (view && view->useGrayScale())
2306             useGrayScale = true;
2307         }
2308 
2309         renderedPage = page->getTileImage(this, _dpiX * scaleFactor, _dpiY * scaleFactor, tile);
2310         // we don't want a finished render thread to change our image while we
2311         // draw it
2312         page->document()->pageCache().lock();
2313         // renderedPage as returned from getTileImage _should_ always be valid
2314         if ( renderedPage ) {
2315           if (useGrayScale) {
2316             // In gray scale mode, we need to obtain a deep copy of the rendered
2317             // page image to avoid altering the cached (color) image
2318             QImage postProcessed = renderedPage->copy();
2319             imageToGrayScale(postProcessed);
2320             painter->drawImage(tile.topLeft(), postProcessed);
2321           }
2322           else
2323             painter->drawImage(tile.topLeft(), *renderedPage);
2324         }
2325         page->document()->pageCache().unlock();
2326 #ifdef DEBUG
2327         painter->drawRect(tile);
2328 #endif
2329       }
2330     }
2331   }
2332   painter->restore();
2333 }
2334 
2335 //static
imageToGrayScale(QImage & img)2336 void PDFPageGraphicsItem::imageToGrayScale(QImage & img)
2337 {
2338   // Casting to QRgb* only works for 32bit images
2339   Q_ASSERT(img.depth() == 32);
2340   QRgb * data = (QRgb*)img.scanLine(0);
2341   int i, gray;
2342   for (i = 0; i < img.byteCount() / 4; ++i) {
2343     // Qt formula (qGray()): 0.34375 * r + 0.5 * g + 0.15625 * b
2344     // MuPDF formula (rgb_to_gray()): r * 0.3f + g * 0.59f + b * 0.11f;
2345     gray = qGray(data[i]);
2346     data[i] = qRgba(gray, gray, gray, qAlpha(data[i]));
2347   }
2348 }
2349 
2350 // Event Handlers
2351 // --------------
event(QEvent * event)2352 bool PDFPageGraphicsItem::event(QEvent *event)
2353 {
2354   // Look for callbacks from asynchronous page operations.
2355   if( event->type() == Backend::PDFLinksLoadedEvent::LinksLoadedEvent ) {
2356     event->accept();
2357 
2358     // Cast to a `PDFLinksLoaded` event so we can access the links.
2359     const Backend::PDFLinksLoadedEvent *links_loaded_event = static_cast<const Backend::PDFLinksLoadedEvent*>(event);
2360     addLinks(links_loaded_event->links);
2361 
2362     return true;
2363 
2364   } else if( event->type() == Backend::PDFPageRenderedEvent::PageRenderedEvent ) {
2365     event->accept();
2366 
2367     // FIXME: We're sort of misusing the render event here---it contains a copy
2368     // of the image data that we never touch. The assumption is that the page
2369     // cache now has new data, so we call `update` to trigger a repaint which
2370     // fetches stuff from the cache.
2371     //
2372     // Perhaps there should be a separate event for when the cache is updated.
2373     update();
2374 
2375     return true;
2376   }
2377 
2378   // Otherwise, pass event to default handler.
2379   return Super::event(event);
2380 }
2381 
2382 // This method causes the `PDFPageGraphicsItem` to create `PDFLinkGraphicsItem`
2383 // objects for a list of asynchronously generated `PDFLinkAnnotation` objects.
2384 // The page item also takes ownership the objects created.  Calling
2385 // `setParentItem` causes the link objects to be added to the scene that owns
2386 // the page object. `update` is then called to ensure all links are drawn at
2387 // once.
addLinks(QList<QSharedPointer<Annotation::Link>> links)2388 void PDFPageGraphicsItem::addLinks(QList< QSharedPointer<Annotation::Link> > links)
2389 {
2390   PDFLinkGraphicsItem *linkItem;
2391 #ifdef DEBUG
2392   stopwatch.start();
2393 #endif
2394   foreach( QSharedPointer<Annotation::Link> link, links ){
2395     linkItem = new PDFLinkGraphicsItem(link);
2396     // Map the link from pdf coordinates to scene coordinates
2397     linkItem->setTransform(QTransform::fromTranslate(0, _pageSize.height()).scale(_dpiX / 72., -_dpiY / 72.));
2398     linkItem->setParentItem(this);
2399   }
2400 #ifdef DEBUG
2401   qDebug() << "Added links in: " << stopwatch.elapsed() << " milliseconds";
2402 #endif
2403 
2404   update();
2405 }
2406 
addAnnotations(QList<QSharedPointer<Annotation::AbstractAnnotation>> annotations)2407 void PDFPageGraphicsItem::addAnnotations(QList< QSharedPointer<Annotation::AbstractAnnotation> > annotations)
2408 {
2409   PDFMarkupAnnotationGraphicsItem *markupAnnotItem;
2410 #ifdef DEBUG
2411   stopwatch.start();
2412 #endif
2413   foreach( QSharedPointer<Annotation::AbstractAnnotation> annot, annotations ){
2414     // We currently only handle popups
2415     if (!annot->isMarkup())
2416       continue;
2417     QSharedPointer<Annotation::Markup> markupAnnot = annot.staticCast<Annotation::Markup>();
2418     markupAnnotItem = new PDFMarkupAnnotationGraphicsItem(markupAnnot);
2419     // Map the link from pdf coordinates to scene coordinates
2420     markupAnnotItem->setTransform(QTransform::fromTranslate(0, _pageSize.height()).scale(_dpiX / 72., -_dpiY / 72.));
2421     markupAnnotItem->setParentItem(this);
2422   }
2423 #ifdef DEBUG
2424   qDebug() << "Added annotations in: " << stopwatch.elapsed() << " milliseconds";
2425 #endif
2426 
2427   update();
2428 }
2429 
2430 
2431 // PDFLinkGraphicsItem
2432 // ===================
2433 
2434 // This class descends from `QGraphicsRectItem` and serves the following
2435 // functions:
2436 //
2437 //    * Provides easy access to the on-screen geometry of a hyperlink area.
2438 //
2439 //    * Handles tasks such as cursor changes on mouse hover and link activation
2440 //      on mouse clicks.
PDFLinkGraphicsItem(QSharedPointer<Annotation::Link> a_link,QGraphicsItem * parent)2441 PDFLinkGraphicsItem::PDFLinkGraphicsItem(QSharedPointer<Annotation::Link> a_link, QGraphicsItem *parent):
2442   Super(parent),
2443   _link(a_link),
2444   _activated(false)
2445 {
2446   // The link area is expressed in "normalized page coordinates", i.e.  values
2447   // in the range [0, 1]. The transformation matrix of this item will have to
2448   // be adjusted so that links will show up correctly in a graphics view.
2449   setRect(_link->rect());
2450 
2451   // Allows links to provide a context-specific cursor when the mouse is
2452   // hovering over them.
2453   //
2454   // **NOTE:** _Requires Qt 4.4 or newer._
2455   setAcceptHoverEvents(true);
2456 
2457   // Only left-clicks will trigger the link.
2458   setAcceptedMouseButtons(Qt::LeftButton);
2459 
2460 #ifdef DEBUG
2461   // **TODO:**
2462   // _Currently for debugging purposes only so that the link area can be
2463   // determined visually, but might make a nice option._
2464   setPen(QPen(Qt::red));
2465 #else
2466   // Perhaps there is a way to not draw the outline at all? Might be more
2467   // efficient...
2468   setPen(QPen(Qt::transparent));
2469 #endif
2470 
2471   retranslateUi();
2472 }
2473 
type() const2474 int PDFLinkGraphicsItem::type() const { return Type; }
2475 
retranslateUi()2476 void PDFLinkGraphicsItem::retranslateUi()
2477 {
2478   PDFAction * action = _link->actionOnActivation();
2479   if (action) {
2480     // Set some meaningful tooltip to inform the user what the link does
2481     // Using <p>...</p> ensures the tooltip text is interpreted as rich text
2482     // and thus is wrapping sensibly to avoid over-long lines.
2483     // Using PDFDocumentView::trUtf8 avoids having to explicitly derive
2484     // PDFLinkGraphicsItem explicily from QObject and puts all translatable
2485     // strings into the same context.
2486     switch(action->type()) {
2487       case PDFAction::ActionTypeGoTo:
2488         {
2489           PDFGotoAction * actionGoto = static_cast<PDFGotoAction*>(action);
2490           if (actionGoto->isRemote())
2491             setToolTip(QString::fromUtf8("<p>%1</p>").arg(actionGoto->filename()));
2492             // FIXME: Possibly include page as well after the filename
2493           else
2494             setToolTip(QString::fromUtf8("<p>") + PDFDocumentView::trUtf8("Goto page %1").arg(actionGoto->destination().page() + 1) + QString::fromUtf8("</p>"));
2495         }
2496         break;
2497       case PDFAction::ActionTypeURI:
2498         {
2499           PDFURIAction * actionURI = static_cast<PDFURIAction*>(action);
2500           setToolTip(QString::fromUtf8("<p>%1</p>").arg(actionURI->url().toString()));
2501         }
2502         break;
2503       case PDFAction::ActionTypeLaunch:
2504         {
2505           PDFLaunchAction * actionLaunch = static_cast<PDFLaunchAction*>(action);
2506           setToolTip(QString::fromUtf8("<p>") + PDFDocumentView::trUtf8("Execute `%1`").arg(actionLaunch->command()) + QString::fromUtf8("</p>"));
2507         }
2508         break;
2509       default:
2510         // All other link types are currently not supported
2511         break;
2512     }
2513   }
2514 }
2515 
2516 // Event Handlers
2517 // --------------
2518 
2519 // Swap cursor during hover events.
hoverEnterEvent(QGraphicsSceneHoverEvent * event)2520 void PDFLinkGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
2521 {
2522   setCursor(Qt::PointingHandCursor);
2523 }
2524 
hoverLeaveEvent(QGraphicsSceneHoverEvent * event)2525 void PDFLinkGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
2526 {
2527   unsetCursor();
2528 }
2529 
2530 // Respond to clicks. Limited to left-clicks by `setAcceptedMouseButtons` in
2531 // this object's constructor.
mousePressEvent(QGraphicsSceneMouseEvent * event)2532 void PDFLinkGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
2533 {
2534   // Actually opening the link is handled during a `mouseReleaseEvent` --- but
2535   // only if the `_activated` flag is `true`.
2536   // Only set _activated if no keyboard modifiers are currently pressed (which
2537   // most likely indicates some tool or other is active)
2538   if (event->modifiers() == Qt::NoModifier)
2539     _activated = true;
2540   else {
2541     _activated = false;
2542     Super::mousePressEvent(event);
2543   }
2544 }
2545 
2546 // The real nitty-gritty of link activation happens in here.
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)2547 void PDFLinkGraphicsItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2548 {
2549   // Check that this link was "activated" (mouse press occurred within the link
2550   // bounding box) and that the mouse release also occurred within the bounding
2551   // box.
2552   if ( (not _activated) || (not contains(event->pos())) )
2553   {
2554     _activated = false;
2555     Super::mouseReleaseEvent(event);
2556     return;
2557   }
2558 
2559   // Post an event to the parent scene. The scene then takes care of processing
2560   // it further, notifying objects, such as `PDFDocumentView`, that may want to
2561   // take action via a `SIGNAL`.
2562   // **TODO:** Wouldn't a direct call be more efficient?
2563   if (_link && _link->actionOnActivation())
2564     QCoreApplication::postEvent(scene(), new PDFActionEvent(_link->actionOnActivation()));
2565   _activated = false;
2566 }
2567 
2568 
2569 // PDFMarkupAnnotationGraphicsItem
2570 // ===============================
2571 
2572 // This class descends from `QGraphicsRectItem` and serves the following
2573 // functions:
2574 //
2575 //    * Provides easy access to the on-screen geometry of a markup annotation.
2576 //
2577 //    * Handles tasks such as cursor changes on mouse hover and link activation
2578 //      on mouse clicks.
2579 //
2580 //    * Displays note popups if necessary
PDFMarkupAnnotationGraphicsItem(QSharedPointer<Annotation::Markup> annot,QGraphicsItem * parent)2581 PDFMarkupAnnotationGraphicsItem::PDFMarkupAnnotationGraphicsItem(QSharedPointer<Annotation::Markup> annot, QGraphicsItem *parent):
2582   Super(parent),
2583   _annot(annot),
2584   _activated(false),
2585   _popup(NULL)
2586 {
2587   // The area is expressed in "normalized page coordinates", i.e.  values
2588   // in the range [0, 1]. The transformation matrix of this item will have to
2589   // be adjusted so that links will show up correctly in a graphics view.
2590   setRect(_annot->rect());
2591 
2592   // Allows annotations to provide a context-specific cursor when the mouse is
2593   // hovering over them.
2594   //
2595   // **NOTE:** _Requires Qt 4.4 or newer._
2596   setAcceptHoverEvents(true);
2597 
2598   // Only left-clicks will trigger the popup (if any).
2599   setAcceptedMouseButtons(annot->popup() ? Qt::LeftButton : Qt::NoButton);
2600 
2601 #ifdef DEBUG
2602   // **TODO:**
2603   // _Currently for debugging purposes only so that the annotation area can be
2604   // determined visually, but might make a nice option._
2605   setPen(QPen(Qt::blue));
2606 #else
2607   // Perhaps there is a way to not draw the outline at all? Might be more
2608   // efficient...
2609   setPen(QPen(Qt::transparent));
2610 #endif
2611 
2612   QString tooltip(annot->richContents());
2613   // If the text is not already split into paragraphs, we do that here to ensure
2614   // proper line folding in the tooltip and hence to avoid very wide tooltips.
2615   if (tooltip.indexOf(QString::fromLatin1("<p>")) < 0)
2616     tooltip = QString::fromLatin1("<p>%1</p>").arg(tooltip.replace(QChar::fromLatin1('\n'), QString::fromLatin1("</p>\n<p>")));
2617   setToolTip(tooltip);
2618 }
2619 
type() const2620 int PDFMarkupAnnotationGraphicsItem::type() const { return Type; }
2621 
2622 // Event Handlers
2623 // --------------
2624 
2625 // Swap cursor during hover events.
hoverEnterEvent(QGraphicsSceneHoverEvent * event)2626 void PDFMarkupAnnotationGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
2627 {
2628   if (_annot->popup())
2629     setCursor(Qt::PointingHandCursor);
2630 }
2631 
hoverLeaveEvent(QGraphicsSceneHoverEvent * event)2632 void PDFMarkupAnnotationGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
2633 {
2634   if (_annot->popup())
2635     unsetCursor();
2636 }
2637 
2638 // Respond to clicks. Limited to left-clicks by `setAcceptedMouseButtons` in
2639 // this object's constructor.
mousePressEvent(QGraphicsSceneMouseEvent * event)2640 void PDFMarkupAnnotationGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
2641 {
2642   // Actually opening the popup is handled during a `mouseReleaseEvent` --- but
2643   // only if the `_activated` flag is `true`.
2644   // Only set _activated if no keyboard modifiers are currently pressed (which
2645   // most likely indicates some tool or other is active)
2646   if (event->modifiers() == Qt::NoModifier)
2647     _activated = true;
2648   else {
2649     _activated = false;
2650     Super::mousePressEvent(event);
2651   }
2652 }
2653 
mouseReleaseEvent(QGraphicsSceneMouseEvent * event)2654 void PDFMarkupAnnotationGraphicsItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
2655 {
2656   Q_ASSERT(event != NULL);
2657 
2658   if (!_activated) {
2659     Super::mouseReleaseEvent(event);
2660     return;
2661   }
2662   _activated = false;
2663 
2664   if (!contains(event->pos()) || !_annot)
2665     return;
2666 
2667   // Find widget that received this mouse event in the first place
2668   // Note: according to the Qt docs, QApplication::widgetAt() can be slow. But
2669   // we don't care here, as this is called only once.
2670   QWidget * sender = QApplication::widgetAt(event->screenPos());
2671 
2672   if (!sender || !qobject_cast<PDFDocumentView*>(sender->parent()))
2673     return;
2674 
2675   if (_popup) {
2676     if (_popup->isVisible())
2677       _popup->hide();
2678     else {
2679       _popup->move(sender->mapFromGlobal(event->screenPos()));
2680       _popup->show();
2681       _popup->raise();
2682       _popup->setFocus();
2683     }
2684     return;
2685   }
2686 
2687   _popup = new QWidget(sender);
2688 
2689   QStringList styles;
2690   if (_annot->color().isValid()) {
2691     QColor c(_annot->color());
2692     styles << QString::fromUtf8(".QWidget { background-color: %1; }").arg(c.name());
2693     if (qGray(c.rgb()) >= 100)
2694       styles << QString::fromUtf8(".QWidget, .QLabel { color: black; }");
2695     else
2696       styles << QString::fromUtf8(".QWidget, .QLabel { color: white; }");
2697   }
2698   else {
2699     styles << QString::fromUtf8(".QWidget { background-color: %1; }").arg(QApplication::palette().color(QPalette::Window).name());
2700       styles << QString::fromUtf8(".QWidget, .QLabel { color: %1; }").arg(QApplication::palette().color(QPalette::Text).name());
2701   }
2702   _popup->setStyleSheet(styles.join(QString::fromLatin1("\n")));
2703   QGridLayout * layout = new QGridLayout(_popup);
2704   layout->setContentsMargins(2, 2, 2, 5);
2705 
2706   QLabel * subject = new QLabel(QString::fromUtf8("<b>%1</b>").arg(_annot->subject()), _popup);
2707   layout->addWidget(subject, 0, 0, 1, -1);
2708   QLabel * author = new QLabel(_annot->author(), _popup);
2709   layout->addWidget(author, 1, 0, 1, 1);
2710   QLabel * date = new QLabel(_annot->creationDate().toString(Qt::DefaultLocaleLongDate), _popup);
2711   layout->addWidget(date, 1, 1, 1, 1, Qt::AlignRight);
2712   QTextEdit * content = new QTextEdit(_annot->richContents(), _popup);
2713   content->setEnabled(false);
2714   layout->addWidget(content, 2, 0, 1, -1);
2715 
2716   _popup->setLayout(layout);
2717   _popup->move(sender->mapFromGlobal(event->screenPos()));
2718   _popup->show();
2719   // TODO: Make popup closable, movable; position it properly (also upon
2720   // zooming!), give some visible indication to which annotation it belongs.
2721   // (Probably turn it into a subclass of QWidget, too).
2722 }
2723 
2724 
2725 // PDFDocumentInfoWidget
2726 // =====================
2727 
setWindowTitle(const QString & windowTitle)2728 void PDFDocumentInfoWidget::setWindowTitle(const QString & windowTitle)
2729 {
2730   QWidget::setWindowTitle(windowTitle);
2731   emit windowTitleChanged(windowTitle);
2732 }
2733 
changeEvent(QEvent * event)2734 void PDFDocumentInfoWidget::changeEvent(QEvent * event)
2735 {
2736   if (event && event->type() == QEvent::LanguageChange)
2737     retranslateUi();
2738   QWidget::changeEvent(event);
2739 }
2740 
2741 
2742 // PDFToCInfoWidget
2743 // ============
2744 
PDFToCInfoWidget(QWidget * parent)2745 PDFToCInfoWidget::PDFToCInfoWidget(QWidget * parent) :
2746   PDFDocumentInfoWidget(parent, PDFDocumentView::trUtf8("Table of Contents"), QString::fromLatin1("QtPDF.ToCInfoWidget"))
2747 {
2748   QVBoxLayout * layout = new QVBoxLayout(this);
2749   layout->setContentsMargins(0, 0, 0, 0);
2750 
2751   _tree = new QTreeWidget(this);
2752   _tree->setAlternatingRowColors(true);
2753   _tree->setHeaderHidden(true);
2754   _tree->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
2755   connect(_tree, SIGNAL(itemSelectionChanged()), this, SLOT(itemSelectionChanged()));
2756 
2757   layout->addWidget(_tree);
2758   setLayout(layout);
2759 }
2760 
retranslateUi()2761 void PDFToCInfoWidget::retranslateUi()
2762 {
2763   setWindowTitle(PDFDocumentView::trUtf8("Table of Contents"));
2764 }
2765 
~PDFToCInfoWidget()2766 PDFToCInfoWidget::~PDFToCInfoWidget()
2767 {
2768   clear();
2769 }
2770 
initFromDocument(const QWeakPointer<Backend::Document> newDoc)2771 void PDFToCInfoWidget::initFromDocument(const QWeakPointer<Backend::Document> newDoc)
2772 {
2773   Q_ASSERT(_tree != NULL);
2774 
2775   PDFDocumentInfoWidget::initFromDocument(newDoc);
2776 
2777   clear();
2778   QSharedPointer<Backend::Document> doc(newDoc.toStrongRef());
2779   if (doc) {
2780     const Backend::PDFToC data = doc->toc();
2781     recursiveAddTreeItems(data, _tree->invisibleRootItem());
2782   }
2783 }
2784 
clear()2785 void PDFToCInfoWidget::clear()
2786 {
2787   Q_ASSERT(_tree != NULL);
2788   // make sure that no item is (and can be) selected while we clear the tree
2789   // (otherwise clearing it could trigger (numerous) itemSelectionChanged signals)
2790   _tree->setSelectionMode(QAbstractItemView::NoSelection);
2791   recursiveClearTreeItems(_tree->invisibleRootItem());
2792   _tree->setSelectionMode(QAbstractItemView::SingleSelection);
2793 }
2794 
itemSelectionChanged()2795 void PDFToCInfoWidget::itemSelectionChanged()
2796 {
2797   Q_ASSERT(_tree != NULL);
2798   // Since the ToC QTreeWidget is in single selection mode, we can only get zero
2799   // or one selected item(s)
2800 
2801   QList<QTreeWidgetItem *> selectedItems = _tree->selectedItems();
2802   if (selectedItems.count() == 0)
2803     return;
2804   QTreeWidgetItem * item = selectedItems.first();
2805   Q_ASSERT(item != NULL);
2806   // TODO: It might be better to register PDFAction with the QMetaType framework
2807   // instead of doing casts with (void*).
2808   PDFAction * action = (PDFAction*)item->data(0, Qt::UserRole).value<void*>();
2809   if (action)
2810     emit actionTriggered(action);
2811 }
2812 
2813 //static
recursiveAddTreeItems(const QList<Backend::PDFToCItem> & tocItems,QTreeWidgetItem * parentTreeItem)2814 void PDFToCInfoWidget::recursiveAddTreeItems(const QList<Backend::PDFToCItem> & tocItems, QTreeWidgetItem * parentTreeItem)
2815 {
2816   foreach (const Backend::PDFToCItem & tocItem, tocItems) {
2817     QTreeWidgetItem * treeItem = new QTreeWidgetItem(parentTreeItem, QStringList(tocItem.label()));
2818     treeItem->setForeground(0, tocItem.color());
2819     if (tocItem.flags()) {
2820       QFont font = treeItem->font(0);
2821       font.setBold(tocItem.flags().testFlag(Backend::PDFToCItem::Flag_Bold));
2822       font.setItalic(tocItem.flags().testFlag(Backend::PDFToCItem::Flag_Bold));
2823       treeItem->setFont(0, font);
2824     }
2825     treeItem->setExpanded(tocItem.isOpen());
2826     // TODO: It might be better to register PDFAction via QMetaType to avoid
2827     // having to use (void*).
2828     if (tocItem.action())
2829       treeItem->setData(0, Qt::UserRole, QVariant::fromValue((void*)tocItem.action()->clone()));
2830     // TODO: maybe display page numbers in col 2?
2831 
2832     if (!tocItem.children().isEmpty())
2833       recursiveAddTreeItems(tocItem.children(), treeItem);
2834   }
2835 }
2836 
2837 //static
recursiveClearTreeItems(QTreeWidgetItem * parent)2838 void PDFToCInfoWidget::recursiveClearTreeItems(QTreeWidgetItem * parent)
2839 {
2840   Q_ASSERT(parent != NULL);
2841   while (parent->childCount() > 0) {
2842     QTreeWidgetItem * item = parent->child(0);
2843     recursiveClearTreeItems(item);
2844     PDFAction * action = (PDFAction*)item->data(0, Qt::UserRole).value<void*>();
2845     if (action)
2846       delete action;
2847     parent->removeChild(item);
2848     delete item;
2849   }
2850 }
2851 
2852 
2853 // PDFMetaDataInfoWidget
2854 // ============
PDFMetaDataInfoWidget(QWidget * parent)2855 PDFMetaDataInfoWidget::PDFMetaDataInfoWidget(QWidget * parent) :
2856   PDFDocumentInfoWidget(parent, PDFDocumentView::trUtf8("Meta Data"), QString::fromLatin1("QtPDF.MetaDataInfoWidget"))
2857 {
2858   setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
2859   // scrollArea ... the central widget of the QDockWidget
2860   // w ... the central widget of scrollArea
2861   // groupBox ... one (of many) group box in w
2862   // vLayout ... lays out the group boxes in w
2863   // layout ... lays out the actual data widgets in groupBox
2864   QVBoxLayout * vLayout = new QVBoxLayout(this);
2865   QFormLayout * layout;
2866 
2867   // We want the vLayout to set the size of w (which should encompass all child
2868   // widgets completely, since we in turn put it into scrollArea to handle
2869   // oversized children
2870   vLayout->setSizeConstraint(QLayout::SetFixedSize);
2871   // Set margins to 0 as space is very limited in the sidebar
2872   vLayout->setContentsMargins(0, 0, 0, 0);
2873 
2874   // NOTE: The labels are initialized in retranslteUi() below
2875   // The "Document" group box
2876   _documentGroup = new QGroupBox(this);
2877   layout = new QFormLayout(_documentGroup);
2878 
2879   _titleLabel = new QLabel(_documentGroup);
2880   _title = new QLabel(_documentGroup);
2881   _title->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2882   layout->addRow(_titleLabel, _title);
2883 
2884   _authorLabel = new QLabel(_documentGroup);
2885   _author = new QLabel(_documentGroup);
2886   _author->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2887   layout->addRow(_authorLabel, _author);
2888 
2889   _subjectLabel = new QLabel(_documentGroup);
2890   _subject = new QLabel(_documentGroup);
2891   _subject->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2892   layout->addRow(_subjectLabel, _subject);
2893 
2894   _keywordsLabel = new QLabel(_documentGroup);
2895   _keywords = new QLabel(_documentGroup);
2896   _keywords->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2897   layout->addRow(_keywordsLabel, _keywords);
2898 
2899   _documentGroup->setLayout(layout);
2900   vLayout->addWidget(_documentGroup);
2901 
2902   // The "Processing" group box
2903   _processingGroup = new QGroupBox(PDFDocumentView::trUtf8("Processing"), this);
2904   layout = new QFormLayout(_processingGroup);
2905 
2906   _creatorLabel = new QLabel(_processingGroup);
2907   _creator = new QLabel(_processingGroup);
2908   _creator->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2909   layout->addRow(_creatorLabel, _creator);
2910 
2911   _producerLabel = new QLabel(_processingGroup);
2912   _producer = new QLabel(_processingGroup);
2913   _producer->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2914   layout->addRow(_producerLabel, _producer);
2915 
2916   _creationDateLabel = new QLabel(_processingGroup);
2917   _creationDate = new QLabel(_processingGroup);
2918   _creationDate->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2919   layout->addRow(_creationDateLabel, _creationDate);
2920 
2921   _modDateLabel = new QLabel(_processingGroup);
2922   _modDate = new QLabel(_processingGroup);
2923   _modDate->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2924   layout->addRow(_modDateLabel, _modDate);
2925 
2926   _trappedLabel = new QLabel(_processingGroup);
2927   _trapped = new QLabel(_processingGroup);
2928   _trapped->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2929   layout->addRow(_trappedLabel, _trapped);
2930 
2931   _processingGroup->setLayout(layout);
2932   vLayout->addWidget(_processingGroup);
2933 
2934   // The "Other" group box
2935   _otherGroup = new QGroupBox(PDFDocumentView::trUtf8("Other"), this);
2936   layout = new QFormLayout(_otherGroup);
2937   // Hide the "Other" group box unless it has something to display
2938   _otherGroup->setVisible(false);
2939 
2940   // Note: Items are added to the "Other" box dynamically in
2941   // initFromDocument()
2942 
2943   _otherGroup->setLayout(layout);
2944   vLayout->addWidget(_otherGroup);
2945 
2946   setLayout(vLayout);
2947   retranslateUi();
2948 }
2949 
initFromDocument(const QWeakPointer<Backend::Document> doc)2950 void PDFMetaDataInfoWidget::initFromDocument(const QWeakPointer<Backend::Document> doc)
2951 {
2952   PDFDocumentInfoWidget::initFromDocument(doc);
2953   reload();
2954 }
2955 
reload()2956 void PDFMetaDataInfoWidget::reload()
2957 {
2958   QSharedPointer<Backend::Document> doc(_doc.toStrongRef());
2959   if (!doc) {
2960     clear();
2961     return;
2962   }
2963   _title->setText(doc->title());
2964   _author->setText(doc->author());
2965   _subject->setText(doc->subject());
2966   _keywords->setText(doc->keywords());
2967   _creator->setText(doc->creator());
2968   _producer->setText(doc->producer());
2969   _creationDate->setText(doc->creationDate().toString(Qt::DefaultLocaleLongDate));
2970   _modDate->setText(doc->modDate().toString(Qt::DefaultLocaleLongDate));
2971   switch (doc->trapped()) {
2972     case Backend::Document::Trapped_True:
2973       _trapped->setText(PDFDocumentView::trUtf8("Yes"));
2974       break;
2975     case Backend::Document::Trapped_False:
2976       _trapped->setText(PDFDocumentView::trUtf8("No"));
2977       break;
2978     default:
2979       _trapped->setText(PDFDocumentView::trUtf8("Unknown"));
2980       break;
2981   }
2982   QFormLayout * layout = qobject_cast<QFormLayout*>(_otherGroup->layout());
2983   Q_ASSERT(layout != NULL);
2984 
2985   // Remove any items there may be
2986   while (layout->count() > 0) {
2987     QLayoutItem * child = layout->takeAt(0);
2988     if (child) {
2989       if (child->widget())
2990         child->widget()->deleteLater();
2991       delete child;
2992     }
2993   }
2994   QMap<QString, QString>::const_iterator it;
2995   for (it = doc->metaDataOther().constBegin(); it != doc->metaDataOther().constEnd(); ++it) {
2996     QLabel * l = new QLabel(it.value(), _otherGroup);
2997     l->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
2998     layout->addRow(it.key(), l);
2999   }
3000   // Hide the "Other" group box unless it has something to display
3001   _otherGroup->setVisible(layout->count() > 0);
3002 }
3003 
clear()3004 void PDFMetaDataInfoWidget::clear()
3005 {
3006   _title->setText(QString());
3007   _author->setText(QString());
3008   _subject->setText(QString());
3009   _keywords->setText(QString());
3010   _creator->setText(QString());
3011   _producer->setText(QString());
3012   _creationDate->setText(QString());
3013   _modDate->setText(QString());
3014   _trapped->setText(PDFDocumentView::trUtf8("Unknown"));
3015   QFormLayout * layout = qobject_cast<QFormLayout*>(_otherGroup->layout());
3016   Q_ASSERT(layout != NULL);
3017 
3018   // Remove any items there may be
3019   while (layout->count() > 0) {
3020     QLayoutItem * child = layout->takeAt(0);
3021     if (child)
3022       delete child;
3023   }
3024 }
3025 
retranslateUi()3026 void PDFMetaDataInfoWidget::retranslateUi()
3027 {
3028   setWindowTitle(PDFDocumentView::trUtf8("Meta Data"));
3029 
3030   _documentGroup->setTitle(PDFDocumentView::trUtf8("Document"));
3031   _titleLabel->setText(PDFDocumentView::trUtf8("Title:"));
3032   _authorLabel->setText(PDFDocumentView::trUtf8("Author:"));
3033   _subjectLabel->setText(PDFDocumentView::trUtf8("Subject:"));
3034   _keywordsLabel->setText(PDFDocumentView::trUtf8("Keywords:"));
3035 
3036   _processingGroup->setTitle(PDFDocumentView::trUtf8("Processing"));
3037   _creatorLabel->setText(PDFDocumentView::trUtf8("Creator:"));
3038   _producerLabel->setText(PDFDocumentView::trUtf8("Producer:"));
3039   _creationDateLabel->setText(PDFDocumentView::trUtf8("Creation date:"));
3040   _modDateLabel->setText(PDFDocumentView::trUtf8("Modification date:"));
3041   _trappedLabel->setText(PDFDocumentView::trUtf8("Trapped:"));
3042 
3043   _otherGroup->setTitle(PDFDocumentView::trUtf8("Other"));
3044 
3045   reload();
3046 }
3047 
3048 
3049 // PDFFontsInfoWidget
3050 // ============
PDFFontsInfoWidget(QWidget * parent)3051 PDFFontsInfoWidget::PDFFontsInfoWidget(QWidget * parent) :
3052   PDFDocumentInfoWidget(parent, PDFDocumentView::trUtf8("Fonts"), QString::fromLatin1("QtPDF.FontsInfoWidget"))
3053 {
3054   QVBoxLayout * layout = new QVBoxLayout(this);
3055   layout->setContentsMargins(0, 0, 0, 0);
3056   _table = new QTableWidget(this);
3057 
3058 #if defined(Q_WS_MAC) || defined(Q_OS_MAC) /* don't do this on windows, as the font ends up too small */
3059   QFont f(_table->font());
3060   f.setPointSize(f.pointSize() - 2);
3061   _table->setFont(f);
3062 #endif
3063   _table->setColumnCount(4);
3064   _table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
3065   _table->setEditTriggers(QAbstractItemView::NoEditTriggers);
3066   _table->setAlternatingRowColors(true);
3067   _table->setShowGrid(false);
3068   _table->setSelectionBehavior(QAbstractItemView::SelectRows);
3069   _table->verticalHeader()->hide();
3070   _table->horizontalHeader()->setStretchLastSection(true);
3071   _table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
3072 
3073   layout->addWidget(_table);
3074   setLayout(layout);
3075   retranslateUi();
3076 }
3077 
initFromDocument(const QWeakPointer<Backend::Document> doc)3078 void PDFFontsInfoWidget::initFromDocument(const QWeakPointer<Backend::Document> doc)
3079 {
3080   PDFDocumentInfoWidget::initFromDocument(doc);
3081   if (isVisible())
3082     reload();
3083 }
3084 
reload()3085 void PDFFontsInfoWidget::reload()
3086 {
3087   Q_ASSERT(_table != NULL);
3088 
3089   clear();
3090   QSharedPointer<Backend::Document> doc(_doc.toStrongRef());
3091   if (!doc)
3092     return;
3093 
3094   QList<Backend::PDFFontInfo> fonts = doc->fonts();
3095   _table->setRowCount(fonts.count());
3096 
3097   int i = 0;
3098   foreach (Backend::PDFFontInfo font, fonts) {
3099     _table->setItem(i, 0, new QTableWidgetItem(font.descriptor().pureName()));
3100     switch (font.fontType()) {
3101       case Backend::PDFFontInfo::FontType_Type0:
3102         _table->setItem(i, 1, new QTableWidgetItem(PDFDocumentView::trUtf8("Type 0")));
3103         break;
3104       case Backend::PDFFontInfo::FontType_Type1:
3105         _table->setItem(i, 1, new QTableWidgetItem(PDFDocumentView::trUtf8("Type 1")));
3106         break;
3107       case Backend::PDFFontInfo::FontType_MMType1:
3108         _table->setItem(i, 1, new QTableWidgetItem(PDFDocumentView::trUtf8("Type 1 (multiple master)")));
3109         break;
3110       case Backend::PDFFontInfo::FontType_Type3:
3111         _table->setItem(i, 1, new QTableWidgetItem(PDFDocumentView::trUtf8("Type 3")));
3112         break;
3113       case Backend::PDFFontInfo::FontType_TrueType:
3114         _table->setItem(i, 1, new QTableWidgetItem(PDFDocumentView::trUtf8("TrueType")));
3115         break;
3116     }
3117     _table->setItem(i, 2, new QTableWidgetItem(font.isSubset() ? PDFDocumentView::trUtf8("yes") : PDFDocumentView::trUtf8("no")));
3118     switch (font.source()) {
3119       case Backend::PDFFontInfo::Source_Embedded:
3120         _table->setItem(i, 3, new QTableWidgetItem(PDFDocumentView::trUtf8("[embedded]")));
3121         break;
3122       case Backend::PDFFontInfo::Source_Builtin:
3123         _table->setItem(i, 3, new QTableWidgetItem(PDFDocumentView::trUtf8("[builtin]")));
3124         break;
3125       case Backend::PDFFontInfo::Source_File:
3126         _table->setItem(i, 3, new QTableWidgetItem(font.fileName().canonicalFilePath()));
3127         break;
3128     }
3129     ++i;
3130   }
3131   _table->resizeColumnsToContents();
3132   _table->resizeRowsToContents();
3133   _table->sortItems(0);
3134 }
3135 
clear()3136 void PDFFontsInfoWidget::clear()
3137 {
3138   Q_ASSERT(_table != NULL);
3139   _table->clearContents();
3140   _table->setRowCount(0);
3141 }
3142 
retranslateUi()3143 void PDFFontsInfoWidget::retranslateUi()
3144 {
3145   Q_ASSERT(_table != NULL);
3146   setWindowTitle(PDFDocumentView::trUtf8("Fonts"));
3147   _table->setHorizontalHeaderLabels(QStringList() << PDFDocumentView::trUtf8("Name") << PDFDocumentView::trUtf8("Type") << PDFDocumentView::trUtf8("Subset") << PDFDocumentView::trUtf8("Source"));
3148   reload();
3149 }
3150 
3151 
3152 // PDFPermissionsInfoWidget)
3153 // ============
PDFPermissionsInfoWidget(QWidget * parent)3154 PDFPermissionsInfoWidget::PDFPermissionsInfoWidget(QWidget * parent) :
3155   PDFDocumentInfoWidget(parent, PDFDocumentView::trUtf8("Permissions"), QString::fromLatin1("QtPDF.PermissionsInfoWidget"))
3156 {
3157   setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
3158   // layout ... lays out the widgets in w
3159   QFormLayout * layout = new QFormLayout(this);
3160 
3161   // We want the layout to set the size of w (which should encompass all child
3162   // widgets completely, since we in turn put it into scrollArea to handle
3163   // oversized children
3164   layout->setSizeConstraint(QLayout::SetFixedSize);
3165 
3166   _printLabel = new QLabel(this);
3167   _print = new QLabel(this);
3168   layout->addRow(_printLabel, _print);
3169   _modifyLabel = new QLabel(this);
3170   _modify = new QLabel(this);
3171   layout->addRow(_modifyLabel, _modify);
3172   _extractLabel = new QLabel(this);
3173   _extract = new QLabel(this);
3174   layout->addRow(_extractLabel, _extract);
3175   _addNotesLabel = new QLabel(this);
3176   _addNotes = new QLabel(this);
3177   layout->addRow(_addNotesLabel, _addNotes);
3178   _formLabel = new QLabel(this);
3179   _form = new QLabel(this);
3180   layout->addRow(_formLabel, _form);
3181 
3182   setLayout(layout);
3183   retranslateUi();
3184 }
3185 
initFromDocument(const QWeakPointer<Backend::Document> doc)3186 void PDFPermissionsInfoWidget::initFromDocument(const QWeakPointer<Backend::Document> doc)
3187 {
3188   PDFDocumentInfoWidget::initFromDocument(doc);
3189   reload();
3190 }
3191 
reload()3192 void PDFPermissionsInfoWidget::reload()
3193 {
3194   QSharedPointer<Backend::Document> doc(_doc.toStrongRef());
3195   if (!doc) {
3196     clear();
3197     return;
3198   }
3199 
3200   Backend::Document::Permissions & perm = doc->permissions();
3201 
3202   if (perm.testFlag(Backend::Document::Permission_Print)) {
3203     if (perm.testFlag(Backend::Document::Permission_PrintHighRes))
3204       _print->setText(PDFDocumentView::trUtf8("Allowed"));
3205     else
3206       _print->setText(PDFDocumentView::trUtf8("Low resolution only"));
3207   }
3208   else
3209     _print->setText(PDFDocumentView::trUtf8("Denied"));
3210 
3211   _modify->setToolTip(QString());
3212   if (perm.testFlag(Backend::Document::Permission_Change))
3213     _modify->setText(PDFDocumentView::trUtf8("Allowed"));
3214   else if (perm.testFlag(Backend::Document::Permission_Assemble)) {
3215     _modify->setText(PDFDocumentView::trUtf8("Assembling only"));
3216     _modify->setToolTip(PDFDocumentView::trUtf8("Insert, rotate, or delete pages and create bookmarks or thumbnail images"));
3217   }
3218   else
3219     _modify->setText(PDFDocumentView::trUtf8("Denied"));
3220 
3221   if (perm.testFlag(Backend::Document::Permission_Extract))
3222     _extract->setText(PDFDocumentView::trUtf8("Allowed"));
3223   else if (perm.testFlag(Backend::Document::Permission_ExtractForAccessibility))
3224     _extract->setText(PDFDocumentView::trUtf8("Accessibility support only"));
3225   else
3226     _extract->setText(PDFDocumentView::trUtf8("Denied"));
3227 
3228   if (perm.testFlag(Backend::Document::Permission_Annotate))
3229     _addNotes->setText(PDFDocumentView::trUtf8("Allowed"));
3230   else
3231     _addNotes->setText(PDFDocumentView::trUtf8("Denied"));
3232 
3233   if (perm.testFlag(Backend::Document::Permission_FillForm))
3234     _form->setText(PDFDocumentView::trUtf8("Allowed"));
3235   else
3236     _form->setText(PDFDocumentView::trUtf8("Denied"));
3237 }
3238 
clear()3239 void PDFPermissionsInfoWidget::clear()
3240 {
3241   _print->setText(PDFDocumentView::trUtf8("Denied"));
3242   _modify->setText(PDFDocumentView::trUtf8("Denied"));
3243   _extract->setText(PDFDocumentView::trUtf8("Denied"));
3244   _addNotes->setText(PDFDocumentView::trUtf8("Denied"));
3245   _form->setText(PDFDocumentView::trUtf8("Denied"));
3246 }
3247 
retranslateUi()3248 void PDFPermissionsInfoWidget::retranslateUi()
3249 {
3250   setWindowTitle(PDFDocumentView::trUtf8("Permissions"));
3251 
3252   _printLabel->setText(PDFDocumentView::trUtf8("Printing:"));
3253   _modifyLabel->setText(PDFDocumentView::trUtf8("Modifications:"));
3254   _extractLabel->setText(PDFDocumentView::trUtf8("Extraction:"));
3255   _addNotesLabel->setText(PDFDocumentView::trUtf8("Annotation:"));
3256   _formLabel->setText(PDFDocumentView::trUtf8("Filling forms:"));
3257   reload();
3258 }
3259 
3260 
3261 // PDFAnnotationsInfoWidget
3262 // ============
PDFAnnotationsInfoWidget(QWidget * parent)3263 PDFAnnotationsInfoWidget::PDFAnnotationsInfoWidget(QWidget * parent) :
3264   PDFDocumentInfoWidget(parent, PDFDocumentView::trUtf8("Annotations"), QString::fromLatin1("QtPDF.AnnotationsInfoWidget"))
3265 {
3266   QVBoxLayout * layout = new QVBoxLayout(this);
3267   layout->setContentsMargins(0, 0, 0, 0);
3268   _table = new QTableWidget(this);
3269 
3270 #if defined(Q_WS_MAC) || defined(Q_OS_MAC) /* don't do this on windows, as the font ends up too small */
3271   QFont f(_table->font());
3272   f.setPointSize(f.pointSize() - 2);
3273   _table->setFont(f);
3274 #endif
3275   _table->setColumnCount(4);
3276   _table->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
3277   _table->setEditTriggers(QAbstractItemView::NoEditTriggers);
3278   _table->setAlternatingRowColors(true);
3279   _table->setShowGrid(false);
3280   _table->setSelectionBehavior(QAbstractItemView::SelectRows);
3281   _table->verticalHeader()->hide();
3282   _table->horizontalHeader()->setStretchLastSection(true);
3283   _table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
3284 
3285   layout->addWidget(_table);
3286   setLayout(layout);
3287 
3288   connect(&_annotWatcher, SIGNAL(resultReadyAt(int)), this, SLOT(annotationsReady(int)));
3289   retranslateUi();
3290 }
3291 
initFromDocument(const QWeakPointer<Backend::Document> newDoc)3292 void PDFAnnotationsInfoWidget::initFromDocument(const QWeakPointer<Backend::Document> newDoc)
3293 {
3294   QSharedPointer<Backend::Document> doc(newDoc.toStrongRef());
3295   if (!doc || !doc->isValid())
3296     return;
3297 
3298   QList< QWeakPointer<Backend::Page> > pages;
3299   int i;
3300   for (i = 0; i < doc->numPages(); ++i) {
3301     QWeakPointer<Backend::Page> page = doc->page(i);
3302     if (page)
3303       pages << page;
3304   }
3305 
3306   // If another search is still running, cancel it---after all, the user wants
3307   // to perform a new search
3308   if (!_annotWatcher.isFinished()) {
3309     _annotWatcher.cancel();
3310     _annotWatcher.waitForFinished();
3311   }
3312 
3313   clear();
3314   _annotWatcher.setFuture(QtConcurrent::mapped(pages, PDFAnnotationsInfoWidget::loadAnnotations));
3315 }
3316 
3317 //static
loadAnnotations(QWeakPointer<Backend::Page> thePage)3318 QList< QSharedPointer<Annotation::AbstractAnnotation> > PDFAnnotationsInfoWidget::loadAnnotations(QWeakPointer<Backend::Page> thePage)
3319 {
3320   QSharedPointer<Backend::Page> page(thePage.toStrongRef());
3321   if (!page)
3322     return QList< QSharedPointer<Annotation::AbstractAnnotation> >();
3323   return page->loadAnnotations();
3324 }
3325 
annotationsReady(int index)3326 void PDFAnnotationsInfoWidget::annotationsReady(int index)
3327 {
3328   Q_ASSERT(_table != NULL);
3329   int i;
3330 
3331   i = _table->rowCount();
3332   _table->setRowCount(i + _annotWatcher.resultAt(index).count());
3333 
3334 
3335   foreach(QSharedPointer<Annotation::AbstractAnnotation> pdfAnnot, _annotWatcher.resultAt(index)) {
3336     // we only use valid markup annotation here
3337     if (!pdfAnnot || !pdfAnnot->isMarkup())
3338       continue;
3339     Annotation::Markup * annot = static_cast<Annotation::Markup*>(pdfAnnot.data());
3340     QSharedPointer<Backend::Page> page(annot->page().toStrongRef());
3341     if (page)
3342       _table->setItem(i, 0, new QTableWidgetItem(QString::number(page->pageNum() + 1)));
3343     _table->setItem(i, 1, new QTableWidgetItem(annot->subject()));
3344     _table->setItem(i, 2, new QTableWidgetItem(annot->author()));
3345     _table->setItem(i, 3, new QTableWidgetItem(annot->contents()));
3346     ++i;
3347   }
3348   _table->setRowCount(i);
3349 }
3350 
clear()3351 void PDFAnnotationsInfoWidget::clear()
3352 {
3353   _table->clearContents();
3354   _table->setRowCount(0);
3355 }
3356 
retranslateUi()3357 void PDFAnnotationsInfoWidget::retranslateUi()
3358 {
3359   setWindowTitle(PDFDocumentView::trUtf8("Annotations"));
3360   _table->setHorizontalHeaderLabels(QStringList() << PDFDocumentView::trUtf8("Page") << PDFDocumentView::trUtf8("Subject") << PDFDocumentView::trUtf8("Author") << PDFDocumentView::trUtf8("Contents"));
3361 }
3362 
3363 
3364 // PDFActionEvent
3365 // ============
3366 
3367 // A PDF Link event is generated when a link is clicked and contains the page
3368 // number of the link target.
PDFActionEvent(const PDFAction * action)3369 PDFActionEvent::PDFActionEvent(const PDFAction * action) : Super(ActionEvent), action(action) {}
3370 
3371 // Obtain a unique ID for `PDFActionEvent` that can be used by event handlers to
3372 // filter out these events.
3373 QEvent::Type PDFActionEvent::ActionEvent = static_cast<QEvent::Type>( QEvent::registerEventType() );
3374 
3375 
PDFPageLayout()3376 PDFPageLayout::PDFPageLayout() :
3377 _numCols(1),
3378 _firstCol(0),
3379 _xSpacing(10),
3380 _ySpacing(10),
3381 _isContinuous(true)
3382 {
3383 }
3384 
setColumnCount(const int numCols)3385 void PDFPageLayout::setColumnCount(const int numCols) {
3386   // We need at least one column, and we only handle changes
3387   if (numCols <= 0 || numCols == _numCols)
3388     return;
3389 
3390   _numCols = numCols;
3391   // Make sure the first column is still valid
3392   if (_firstCol >= _numCols)
3393     _firstCol = _numCols - 1;
3394   rearrange();
3395 }
3396 
setColumnCount(const int numCols,const int firstCol)3397 void PDFPageLayout::setColumnCount(const int numCols, const int firstCol) {
3398   // We need at least one column, and we only handle changes
3399   if (numCols <= 0 || (numCols == _numCols && firstCol == _firstCol))
3400     return;
3401 
3402   _numCols = numCols;
3403 
3404   if (firstCol < 0)
3405     _firstCol = 0;
3406   else if (firstCol >= _numCols)
3407     _firstCol = _numCols - 1;
3408   else
3409     _firstCol = firstCol;
3410   rearrange();
3411 }
3412 
setFirstColumn(const int firstCol)3413 void PDFPageLayout::setFirstColumn(const int firstCol) {
3414   // We only handle changes
3415   if (firstCol == _firstCol)
3416     return;
3417 
3418   if (firstCol < 0)
3419     _firstCol = 0;
3420   else if (firstCol >= _numCols)
3421     _firstCol = _numCols - 1;
3422   else
3423     _firstCol = firstCol;
3424   rearrange();
3425 }
3426 
setXSpacing(const qreal xSpacing)3427 void PDFPageLayout::setXSpacing(const qreal xSpacing) {
3428   if (xSpacing > 0)
3429     _xSpacing = xSpacing;
3430   else
3431     _xSpacing = 0.;
3432 }
3433 
setYSpacing(const qreal ySpacing)3434 void PDFPageLayout::setYSpacing(const qreal ySpacing) {
3435   if (ySpacing > 0)
3436     _ySpacing = ySpacing;
3437   else
3438     _ySpacing = 0.;
3439 }
3440 
setContinuous(const bool continuous)3441 void PDFPageLayout::setContinuous(const bool continuous /* = true */)
3442 {
3443   if (continuous == _isContinuous)
3444     return;
3445   _isContinuous = continuous;
3446   if (!_isContinuous)
3447     setColumnCount(1, 0);
3448     // setColumnCount() calls relayout automatically
3449   else relayout();
3450 }
3451 
rowCount() const3452 int PDFPageLayout::rowCount() const {
3453   if (_layoutItems.isEmpty())
3454     return 0;
3455   return _layoutItems.last().row + 1;
3456 }
3457 
addPage(PDFPageGraphicsItem * page)3458 void PDFPageLayout::addPage(PDFPageGraphicsItem * page) {
3459   LayoutItem item;
3460 
3461   if (!page)
3462     return;
3463 
3464   item.page = page;
3465   if (_layoutItems.isEmpty()) {
3466     item.row = 0;
3467     item.col = _firstCol;
3468   }
3469   else if (_layoutItems.last().col < _numCols - 1){
3470     item.row = _layoutItems.last().row;
3471     item.col = _layoutItems.last().col + 1;
3472   }
3473   else {
3474     item.row = _layoutItems.last().row + 1;
3475     item.col = 0;
3476   }
3477   _layoutItems.append(item);
3478 }
3479 
removePage(PDFPageGraphicsItem * page)3480 void PDFPageLayout::removePage(PDFPageGraphicsItem * page) {
3481   QList<LayoutItem>::iterator it;
3482   int row, col;
3483 
3484   // **TODO:** Decide what to do with pages that are in the list multiple times
3485   // (see also insertPage())
3486 
3487   // First, find the page and remove it
3488   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3489     if (it->page == page) {
3490       row = it->row;
3491       col = it->col;
3492       it = _layoutItems.erase(it);
3493       break;
3494     }
3495   }
3496 
3497   // Then, rearrange the pages behind it (no call to rearrange() to save time
3498   // by not going over the unchanged pages in front of the removed one)
3499   for (; it != _layoutItems.end(); ++it) {
3500     it->row = row;
3501     it->col = col;
3502 
3503     ++col;
3504     if (col >= _numCols) {
3505       col = 0;
3506       ++row;
3507     }
3508   }
3509 }
3510 
insertPage(PDFPageGraphicsItem * page,PDFPageGraphicsItem * before)3511 void PDFPageLayout::insertPage(PDFPageGraphicsItem * page, PDFPageGraphicsItem * before /* = NULL */) {
3512   QList<LayoutItem>::iterator it;
3513   int row, col;
3514   LayoutItem item;
3515 
3516   item.page = page;
3517 
3518   // **TODO:** Decide what to do with pages that are in the list multiple times
3519   // (see also insertPage())
3520 
3521   // First, find the page to insert before and insert (row and col will be set
3522   // below)
3523   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3524     if (it->page == before) {
3525       row = it->row;
3526       col = it->col;
3527       it = _layoutItems.insert(it, item);
3528       break;
3529     }
3530   }
3531   if (it == _layoutItems.end()) {
3532     // We haven't found "before", so we just append the page
3533     addPage(page);
3534     return;
3535   }
3536 
3537   // Then, rearrange the pages starting from the inserted one (no call to
3538   // rearrange() to save time by not going over the unchanged pages)
3539   for (; it != _layoutItems.end(); ++it) {
3540     it->row = row;
3541     it->col = col;
3542 
3543     ++col;
3544     if (col >= _numCols) {
3545       col = 0;
3546       ++row;
3547     }
3548   }
3549 }
3550 
3551 // Relayout the pages on the canvas
relayout()3552 void PDFPageLayout::relayout() {
3553   if (_isContinuous)
3554     continuousModeRelayout();
3555   else
3556     singlePageModeRelayout();
3557 }
3558 
3559 // Relayout the pages on the canvas in continuous mode
continuousModeRelayout()3560 void PDFPageLayout::continuousModeRelayout() {
3561   // Create arrays to hold offsets and make sure that they have
3562   // sufficient space (to avoid moving the data around in memory)
3563   QVector<qreal> colOffsets(_numCols + 1, 0), rowOffsets(rowCount() + 1, 0);
3564   int i;
3565   qreal x, y;
3566   QList<LayoutItem>::iterator it;
3567   QSizeF pageSize;
3568   QRectF sceneRect;
3569 
3570   // First, fill the offsets with the respective widths and heights
3571   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3572     if (!it->page)
3573       continue;
3574     pageSize = it->page->pageSizeF();
3575 
3576     if (colOffsets[it->col + 1] < pageSize.width())
3577       colOffsets[it->col + 1] = pageSize.width();
3578     if (rowOffsets[it->row + 1] < pageSize.height())
3579       rowOffsets[it->row + 1] = pageSize.height();
3580   }
3581 
3582   // Next, calculate cumulative offsets (including spacing)
3583   for (i = 1; i <= _numCols; ++i)
3584     colOffsets[i] += colOffsets[i - 1] + _xSpacing;
3585   for (i = 1; i <= rowCount(); ++i)
3586     rowOffsets[i] += rowOffsets[i - 1] + _ySpacing;
3587 
3588   // Finally, position pages
3589   // **TODO:** Figure out why this loop causes some noticeable lag when switching
3590   // from SinglePage to continuous mode in a large document (but not when
3591   // switching between separate continuous modes)
3592   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3593     if (!it->page)
3594       continue;
3595     // If we have more than one column, right-align the left-most column and
3596     // left-align the right-most column to avoid large space between columns
3597     // In all other cases, center the page in allotted space (in case we
3598     // stumble over pages of different sizes, e.g., landscape pages, etc.)
3599     pageSize = it->page->pageSizeF();
3600     if (_numCols > 1 && it->col == 0)
3601       x = colOffsets[it->col + 1] - _xSpacing - pageSize.width();
3602     else if (_numCols > 1 && it->col == _numCols - 1)
3603       x = colOffsets[it->col];
3604     else
3605       x = 0.5 * (colOffsets[it->col + 1] + colOffsets[it->col] - _xSpacing - pageSize.width());
3606     // Always center the page vertically
3607     y = 0.5 * (rowOffsets[it->row + 1] + rowOffsets[it->row] - _ySpacing - pageSize.height());
3608     it->page->setPos(x, y);
3609   }
3610 
3611   // leave some space around the pages (note that the space on the right/bottom
3612   // is already included in the corresponding Offset values and that the method
3613   // signature is (x0, y0, w, h)!)
3614   sceneRect.setRect(-_xSpacing / 2, -_ySpacing / 2, colOffsets[_numCols], rowOffsets[rowCount()]);
3615   emit layoutChanged(sceneRect);
3616 }
3617 
3618 // Relayout the pages on the canvas in single page mode
singlePageModeRelayout()3619 void PDFPageLayout::singlePageModeRelayout()
3620 {
3621   qreal width, height, maxWidth = 0.0, maxHeight = 0.0;
3622   QList<LayoutItem>::iterator it;
3623   QSizeF pageSize;
3624   QRectF sceneRect;
3625 
3626   // We lay out all pages such that their center is in the origin (since only
3627   // one page is visible at any time, this is no problem)
3628   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3629     if (!it->page)
3630       continue;
3631     pageSize = it->page->pageSizeF();
3632     width = pageSize.width();
3633     height = pageSize.height();
3634     if (width > maxWidth)
3635       maxWidth = width;
3636     if (height > maxHeight)
3637       maxHeight = height;
3638     it->page->setPos(-width / 2., -height / 2.);
3639   }
3640 
3641   sceneRect.setRect(-maxWidth / 2., -maxHeight / 2., maxWidth, maxHeight);
3642   emit layoutChanged(sceneRect);
3643 }
3644 
rearrange()3645 void PDFPageLayout::rearrange() {
3646   QList<LayoutItem>::iterator it;
3647   int row, col;
3648 
3649   row = 0;
3650   col = _firstCol;
3651   for (it = _layoutItems.begin(); it != _layoutItems.end(); ++it) {
3652     it->row = row;
3653     it->col = col;
3654 
3655     ++col;
3656     if (col >= _numCols) {
3657       col = 0;
3658       ++row;
3659     }
3660   }
3661 }
3662 
3663 } // namespace QtPDF
3664 
3665 // vim: set sw=2 ts=2 et
3666 
3667