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