1 /*
2 	This is part of TeXworks, an environment for working with TeX documents
3 	Copyright (C) 2007-2016  Jonathan Kew, Stefan Löffler, Charlie Sharpsteen
4 
5 	This program is free software; you can redistribute it and/or modify
6 	it under the terms of the GNU General Public License as published by
7 	the Free Software Foundation; either version 2 of the License, or
8 	(at your option) any later version.
9 
10 	This program is distributed in the hope that it will be useful,
11 	but WITHOUT ANY WARRANTY; without even the implied warranty of
12 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 	GNU General Public License for more details.
14 
15 	You should have received a copy of the GNU General Public License
16 	along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 
18 	For links to further information, or to contact the authors,
19 	see <http://www.tug.org/texworks/>.
20 */
21 
22 #include "PDFDocument.h"
23 #include "TeXDocument.h"
24 #include "TWApp.h"
25 #include "TWUtils.h"
26 #include "FindDialog.h"
27 #include "ClickableLabel.h"
28 
29 #include <QDockWidget>
30 #include <QCloseEvent>
31 #include <QFileDialog>
32 #include <QMessageBox>
33 #include <QStatusBar>
34 #include <QPainter>
35 #include <QPaintEngine>
36 #include <QLabel>
37 #include <QScrollArea>
38 #include <QStyle>
39 #include <QDesktopWidget>
40 #include <QScrollBar>
41 #include <QRegion>
42 #include <QVector>
43 #include <QList>
44 #include <QStack>
45 #include <QInputDialog>
46 #include <QDesktopServices>
47 #include <QUrl>
48 #include <QShortcut>
49 #include <QToolTip>
50 #include <QSignalMapper>
51 
52 #include <math.h>
53 
54 #define SYNCTEX_GZ_EXT	".synctex.gz"
55 #define SYNCTEX_EXT		".synctex"
56 
57 #define ROUND(x) floor((x)+0.5)
58 
59 // Possible sizes of the magnifying glass (in pixel)
60 const int magSizes[] = { 200, 300, 400 };
61 
62 // duration of highlighting in PDF view (might make configurable?)
63 const int kPDFHighlightDuration = 2000;
64 
65 
66 
67 #pragma mark === PDFDocument ===
68 
69 // TODO: This is seemingly unused---verify && remove
70 QList<PDFDocument*> PDFDocument::docList;
71 
PDFDocument(const QString & fileName,TeXDocument * texDoc)72 PDFDocument::PDFDocument(const QString &fileName, TeXDocument *texDoc)
73 	: _syncHighlight(NULL), _synchronizer(NULL), openedManually(false)
74 {
75 	init();
76 
77 	if (texDoc == NULL)
78 		openedManually = true;
79 
80 	loadFile(fileName);
81 
82 	QMap<QString,QVariant> properties = TWApp::instance()->getFileProperties(curFile);
83 	if (properties.contains("geometry"))
84 		restoreGeometry(properties.value("geometry").toByteArray());
85 	else
86 		TWUtils::zoomToHalfScreen(this, true);
87 
88 	if (properties.contains("state"))
89 		restoreState(properties.value("state").toByteArray(), kPDFWindowStateVersion);
90 
91 	if (properties.contains("pdfPageMode"))
92 		setPageMode(properties.value("pdfPageMode", -1).toInt());
93 
94 	QTimer::singleShot(100, this, SLOT(setDefaultScale()));
95 
96 	if (texDoc != NULL) {
97 		stackUnder((QWidget*)texDoc);
98 		actionSide_by_Side->setEnabled(true);
99 		actionGo_to_Source->setEnabled(true);
100 		sourceDocList.append(texDoc);
101 	}
102 }
103 
~PDFDocument()104 PDFDocument::~PDFDocument()
105 {
106 	docList.removeAll(this);
107 }
108 
init()109 void PDFDocument::init()
110 {
111 	docList.append(this);
112 
113 	setupUi(this);
114 #if defined(Q_OS_WIN)
115 	TWApp::instance()->createMessageTarget(this);
116 #endif
117 
118 	setAttribute(Qt::WA_DeleteOnClose, true);
119 	setAttribute(Qt::WA_MacNoClickThrough, true);
120 
121 	QIcon winIcon;
122 #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
123 	// The Compiz window manager doesn't seem to support icons larger than
124 	// 128x128, so we add a suitable one first
125 	winIcon.addFile(":/images/images/TeXworks-doc-128.png");
126 #endif
127 	winIcon.addFile(":/images/images/TeXworks-doc.png");
128 	setWindowIcon(winIcon);
129 
130 	pdfWidget = new QtPDF::PDFDocumentWidget(this);
131 	pdfWidget->setSearchResultHighlightBrush(QBrush(Qt::transparent));
132 	pdfWidget->setCurrentSearchResultHighlightBrush(QBrush(Qt::transparent));
133 	pdfWidget->setAcceptDrops(false);
134 	_searchResultHighlightBrush = QColor(255, 255, 0, 63);
135 	setCentralWidget(pdfWidget);
136 
137 	connect(pdfWidget, SIGNAL(changedPage(int)), this, SLOT(updateStatusBar()));
138 	connect(pdfWidget, SIGNAL(changedZoom(qreal)), this, SLOT(updateStatusBar()));
139 	connect(pdfWidget, SIGNAL(changedDocument(const QWeakPointer<QtPDF::Backend::Document>)), this, SLOT(changedDocument(const QWeakPointer<QtPDF::Backend::Document>)));
140 	connect(pdfWidget, SIGNAL(searchResultHighlighted(const int, const QList<QPolygonF>)), this, SLOT(searchResultHighlighted(const int, const QList<QPolygonF>)));
141 	connect(pdfWidget, SIGNAL(changedPageMode(QtPDF::PDFDocumentView::PageMode)), this, SLOT(updatePageMode(QtPDF::PDFDocumentView::PageMode)));
142 	connect(pdfWidget, SIGNAL(requestOpenPdf(QString,QtPDF::PDFDestination,bool)), this, SLOT(maybeOpenPdf(QString,QtPDF::PDFDestination,bool)));
143 	connect(pdfWidget, SIGNAL(requestOpenUrl(QUrl)), this, SLOT(maybeOpenUrl(QUrl)));
144 
145 	toolButtonGroup = new QButtonGroup(toolBar);
146 	toolButtonGroup->addButton(qobject_cast<QAbstractButton*>(toolBar->widgetForAction(actionMagnify)), QtPDF::PDFDocumentView::MouseMode_MagnifyingGlass);
147 	toolButtonGroup->addButton(qobject_cast<QAbstractButton*>(toolBar->widgetForAction(actionScroll)), QtPDF::PDFDocumentView::MouseMode_Move);
148 	toolButtonGroup->addButton(qobject_cast<QAbstractButton*>(toolBar->widgetForAction(actionSelect_Text)), QtPDF::PDFDocumentView::MouseMode_Select);
149 //	toolButtonGroup->addButton(qobject_cast<QAbstractButton*>(toolBar->widgetForAction(actionSelect_Image)), kSelectImage);
150 	connect(toolButtonGroup, SIGNAL(buttonClicked(int)), this, SLOT(setMouseMode(int)));
151 	pdfWidget->setMouseModeMagnifyingGlass();
152 
153 	scaleLabel = new ClickableLabel();
154 	statusBar()->addPermanentWidget(scaleLabel);
155 	scaleLabel->setFrameStyle(QFrame::StyledPanel);
156 	scaleLabel->setFont(statusBar()->font());
157 	connect(scaleLabel, SIGNAL(mouseLeftClick(QMouseEvent*)), this, SLOT(scaleLabelClick(QMouseEvent*)));
158 
159 	pageLabel = new ClickableLabel();
160 	statusBar()->addPermanentWidget(pageLabel);
161 	pageLabel->setFrameStyle(QFrame::StyledPanel);
162 	pageLabel->setFont(statusBar()->font());
163 	connect(pageLabel, SIGNAL(mouseLeftClick(QMouseEvent*)), this, SLOT(doPageDialog()));
164 
165 	connect(actionAbout_TW, SIGNAL(triggered()), qApp, SLOT(about()));
166 	connect(actionSettings_and_Resources, SIGNAL(triggered()), qApp, SLOT(doResourcesDialog()));
167 	connect(actionGoToHomePage, SIGNAL(triggered()), qApp, SLOT(goToHomePage()));
168 	connect(actionWriteToMailingList, SIGNAL(triggered()), qApp, SLOT(writeToMailingList()));
169 
170 	connect(actionNew, SIGNAL(triggered()), qApp, SLOT(newFile()));
171 	connect(actionNew_from_Template, SIGNAL(triggered()), qApp, SLOT(newFromTemplate()));
172 	connect(actionOpen, SIGNAL(triggered()), qApp, SLOT(open()));
173 	connect(actionPrintPdf, SIGNAL(triggered()), this, SLOT(print()));
174 
175 	connect(actionQuit_TeXworks, SIGNAL(triggered()), TWApp::instance(), SLOT(maybeQuit()));
176 
177 	connect(actionFind, SIGNAL(triggered()), this, SLOT(doFindDialog()));
178 
179 	connect(actionFirst_Page, SIGNAL(triggered()), pdfWidget, SLOT(goFirst()));
180 	connect(actionPrevious_Page, SIGNAL(triggered()), pdfWidget, SLOT(goPrev()));
181 	connect(actionNext_Page, SIGNAL(triggered()), pdfWidget, SLOT(goNext()));
182 	connect(actionLast_Page, SIGNAL(triggered()), pdfWidget, SLOT(goLast()));
183 	connect(actionGo_to_Page, SIGNAL(triggered()), this, SLOT(doPageDialog()));
184 	addAction(actionPrevious_ViewRect);
185 	connect(actionPrevious_ViewRect, SIGNAL(triggered()), pdfWidget, SLOT(goPrevViewRect()));
186 	connect(pdfWidget, SIGNAL(changedPage(int)), this, SLOT(enablePageActions(int)));
187 
188 	connect(actionActual_Size, SIGNAL(triggered()), pdfWidget, SLOT(zoom100()));
189 	connect(actionFit_to_Width, SIGNAL(triggered()), pdfWidget, SLOT(zoomFitWidth()));
190 	connect(actionFit_to_Window, SIGNAL(triggered()), pdfWidget, SLOT(zoomFitWindow()));
191 	connect(actionZoom_In, SIGNAL(triggered()), pdfWidget, SLOT(zoomIn()));
192 	connect(actionZoom_Out, SIGNAL(triggered()), pdfWidget, SLOT(zoomOut()));
193 	connect(actionFull_Screen, SIGNAL(triggered()), this, SLOT(toggleFullScreen()));
194 	connect(pdfWidget, SIGNAL(contextClick(int, const QPointF&)), this, SLOT(syncClick(int, const QPointF&)));
195 	pageModeSignalMapper.setMapping(actionPageMode_Single, QtPDF::PDFDocumentView::PageMode_SinglePage);
196 	pageModeSignalMapper.setMapping(actionPageMode_Continuous, QtPDF::PDFDocumentView::PageMode_OneColumnContinuous);
197 	pageModeSignalMapper.setMapping(actionPageMode_TwoPagesContinuous, QtPDF::PDFDocumentView::PageMode_TwoColumnContinuous);
198 	connect(actionPageMode_Single, SIGNAL(triggered()), &pageModeSignalMapper, SLOT(map()));
199 	connect(actionPageMode_Continuous, SIGNAL(triggered()), &pageModeSignalMapper, SLOT(map()));
200 	connect(actionPageMode_TwoPagesContinuous, SIGNAL(triggered()), &pageModeSignalMapper, SLOT(map()));
201 	connect(&pageModeSignalMapper, SIGNAL(mapped(int)), this, SLOT(setPageMode(int)));
202 
203 	if (actionZoom_In->shortcut() == QKeySequence("Ctrl++"))
204 		new QShortcut(QKeySequence("Ctrl+="), pdfWidget, SLOT(zoomIn()));
205 
206 	connect(actionTypeset, SIGNAL(triggered()), this, SLOT(retypeset()));
207 
208 	connect(actionStack, SIGNAL(triggered()), qApp, SLOT(stackWindows()));
209 	connect(actionTile, SIGNAL(triggered()), qApp, SLOT(tileWindows()));
210 	connect(actionSide_by_Side, SIGNAL(triggered()), this, SLOT(sideBySide()));
211 	connect(actionPlace_on_Left, SIGNAL(triggered()), this, SLOT(placeOnLeft()));
212 	connect(actionPlace_on_Right, SIGNAL(triggered()), this, SLOT(placeOnRight()));
213 	connect(actionGo_to_Source, SIGNAL(triggered()), this, SLOT(goToSource()));
214 
215 	connect(actionFind_Again, SIGNAL(triggered()), this, SLOT(doFindAgain()));
216 
217 	updateRecentFileActions();
218 	connect(qApp, SIGNAL(recentFileActionsChanged()), this, SLOT(updateRecentFileActions()));
219 	connect(qApp, SIGNAL(windowListChanged()), this, SLOT(updateWindowMenu()));
220 	connect(actionClear_Recent_Files, SIGNAL(triggered()), TWApp::instance(), SLOT(clearRecentFiles()));
221 
222 	connect(qApp, SIGNAL(hideFloatersExcept(QWidget*)), this, SLOT(hideFloatersUnlessThis(QWidget*)));
223 	connect(this, SIGNAL(activatedWindow(QWidget*)), qApp, SLOT(activatedWindow(QWidget*)));
224 
225 	connect(actionPreferences, SIGNAL(triggered()), qApp, SLOT(preferences()));
226 
227 	connect(this, SIGNAL(destroyed()), qApp, SLOT(updateWindowMenus()));
228 
229 	connect(qApp, SIGNAL(syncPdf(const QString&, int, int, bool)), this, SLOT(syncFromSource(const QString&, int, int, bool)));
230 
231 	_syncHighlightRemover.setSingleShot(true);
232 	connect(&_syncHighlightRemover, SIGNAL(timeout()), this, SLOT(clearSyncHighlight()));
233 
234 	_searchResultHighlightRemover.setSingleShot(true);
235 	connect(&_searchResultHighlightRemover, SIGNAL(timeout()), this, SLOT(clearSearchResultHighlight()));
236 
237 	menuShow->addAction(toolBar->toggleViewAction());
238 	menuShow->addSeparator();
239 
240 	QDockWidget * dw = pdfWidget->dockWidget(QtPDF::PDFDocumentView::Dock_TableOfContents, this);
241 	dw->hide();
242 	addDockWidget(Qt::LeftDockWidgetArea, dw);
243 	menuShow->addAction(dw->toggleViewAction());
244 
245 	dw = pdfWidget->dockWidget(QtPDF::PDFDocumentView::Dock_MetaData, this);
246 	dw->hide();
247 	addDockWidget(Qt::LeftDockWidgetArea, dw);
248 	menuShow->addAction(dw->toggleViewAction());
249 
250 	dw = pdfWidget->dockWidget(QtPDF::PDFDocumentView::Dock_Fonts, this);
251 	dw->hide();
252 	addDockWidget(Qt::BottomDockWidgetArea, dw);
253 	menuShow->addAction(dw->toggleViewAction());
254 
255 	dw = pdfWidget->dockWidget(QtPDF::PDFDocumentView::Dock_Permissions, this);
256 	dw->hide();
257 	addDockWidget(Qt::LeftDockWidgetArea, dw);
258 	menuShow->addAction(dw->toggleViewAction());
259 
260 	dw = pdfWidget->dockWidget(QtPDF::PDFDocumentView::Dock_Annotations, this);
261 	dw->hide();
262 	addDockWidget(Qt::LeftDockWidgetArea, dw);
263 	menuShow->addAction(dw->toggleViewAction());
264 
265 	exitFullscreen = NULL;
266 
267 	QSETTINGS_OBJECT(settings);
268 	switch(settings.value("pdfPageMode", kDefault_PDFPageMode).toInt()) {
269 		case 0:
270 			setPageMode(QtPDF::PDFDocumentView::PageMode_SinglePage);
271 			break;
272 		case 1:
273 			setPageMode(QtPDF::PDFDocumentView::PageMode_OneColumnContinuous);
274 			break;
275 		case 2:
276 			setPageMode(QtPDF::PDFDocumentView::PageMode_TwoColumnContinuous);
277 			break;
278 		default:
279 			setPageMode(kDefault_PDFPageMode);
280 			break;
281 	}
282 	resetMagnifier();
283 
284 	if (settings.contains("previewResolution"))
285 		pdfWidget->setResolution(settings.value("previewResolution", QApplication::desktop()->logicalDpiX()).toInt());
286 
287 	TWUtils::applyToolbarOptions(this, settings.value("toolBarIconSize", 2).toInt(), settings.value("toolBarShowText", false).toBool());
288 
289 	TWApp::instance()->updateWindowMenus();
290 
291 	initScriptable(menuScripts, actionAbout_Scripts, actionManage_Scripts,
292 				   actionUpdate_Scripts, actionShow_Scripts_Folder);
293 
294 	TWUtils::insertHelpMenuItems(menuHelp);
295 	TWUtils::installCustomShortcuts(this);
296 }
297 
changeEvent(QEvent * event)298 void PDFDocument::changeEvent(QEvent *event)
299 {
300 	if (event->type() == QEvent::LanguageChange) {
301 		QString title = windowTitle();
302 		retranslateUi(this);
303 		TWUtils::insertHelpMenuItems(menuHelp);
304 		setWindowTitle(title);
305 		updateStatusBar();
306 	}
307 	QMainWindow::changeEvent(event);
308 }
309 
linkToSource(TeXDocument * texDoc)310 void PDFDocument::linkToSource(TeXDocument *texDoc)
311 {
312 	if (texDoc != NULL) {
313 		if (!sourceDocList.contains(texDoc))
314 			sourceDocList.append(texDoc);
315 		actionGo_to_Source->setEnabled(true);
316 	}
317 }
318 
texClosed(QObject * obj)319 void PDFDocument::texClosed(QObject *obj)
320 {
321 	TeXDocument *texDoc = reinterpret_cast<TeXDocument*>(obj);
322 		// can't use qobject_cast here as the object's metadata is already gone!
323 	if (texDoc != 0) {
324 		sourceDocList.removeAll(texDoc);
325 		if (sourceDocList.count() == 0)
326 			close();
327 	}
328 }
329 
texActivated(TeXDocument * texDoc)330 void PDFDocument::texActivated(TeXDocument * texDoc)
331 {
332 	// A source file was activated. Make sure it is the first in the list of
333 	// source docs so that future "Goto Source" actions point there.
334 	if (sourceDocList.first() != texDoc) {
335 		sourceDocList.removeAll(texDoc);
336 		sourceDocList.prepend(texDoc);
337 	}
338 }
339 
updateRecentFileActions()340 void PDFDocument::updateRecentFileActions()
341 {
342 	TWUtils::updateRecentFileActions(this, recentFileActions, menuOpen_Recent, actionClear_Recent_Files);
343 }
344 
updateWindowMenu()345 void PDFDocument::updateWindowMenu()
346 {
347 	TWUtils::updateWindowMenu(this, menuWindow);
348 }
349 
sideBySide()350 void PDFDocument::sideBySide()
351 {
352 	if (sourceDocList.count() > 0) {
353 		TWUtils::sideBySide(sourceDocList.first(), this);
354 		sourceDocList.first()->selectWindow(false);
355 		selectWindow();
356 	}
357 	else
358 		placeOnRight();
359 }
360 
event(QEvent * event)361 bool PDFDocument::event(QEvent *event)
362 {
363 	switch (event->type()) {
364 		case QEvent::WindowActivate:
365 			showFloaters();
366 			emit activatedWindow(this);
367 			break;
368 		default:
369 			break;
370 	}
371 	return QMainWindow::event(event);
372 }
373 
closeEvent(QCloseEvent * event)374 void PDFDocument::closeEvent(QCloseEvent *event)
375 {
376 	event->accept();
377 	if (openedManually) {
378 		saveRecentFileInfo();
379 	}
380 	deleteLater();
381 }
382 
saveRecentFileInfo()383 void PDFDocument::saveRecentFileInfo()
384 {
385 	QMap<QString,QVariant> fileProperties;
386 	fileProperties.insert("path", curFile);
387 	fileProperties.insert("geometry", saveGeometry());
388 	fileProperties.insert("state", saveState(kPDFWindowStateVersion));
389 	fileProperties.insert("pdfPageMode", pdfWidget->pageMode());
390 	TWApp::instance()->addToRecentFiles(fileProperties);
391 }
392 
loadFile(const QString & fileName)393 void PDFDocument::loadFile(const QString &fileName)
394 {
395 	setCurrentFile(fileName);
396 	QSETTINGS_OBJECT(settings);
397 	QFileInfo info(fileName);
398 	settings.setValue("openDialogDir", info.canonicalPath());
399 
400 	reload();
401 }
402 
reload()403 void PDFDocument::reload()
404 {
405 	QApplication::setOverrideCursor(Qt::WaitCursor);
406 
407 	clearSyncHighlight();
408 	if (pdfWidget->load(curFile)) {
409 		loadSyncData();
410 		emit reloaded();
411 	}
412 	else {
413 		statusBar()->showMessage(tr("Failed to load file \"%1\"; perhaps it is not a valid PDF document.").arg(TWUtils::strippedName(curFile)));
414 	}
415 	QApplication::restoreOverrideCursor();
416 }
417 
loadSyncData()418 void PDFDocument::loadSyncData()
419 {
420 	if (_synchronizer) {
421 		delete _synchronizer;
422 		_synchronizer = NULL;
423 	}
424 	_synchronizer = new TWSyncTeXSynchronizer(curFile);
425 	if (!_synchronizer)
426 		statusBar()->showMessage(tr("Error initializing SyncTeX"), kStatusMessageDuration);
427 	else if (!_synchronizer->isValid())
428 		statusBar()->showMessage(tr("No SyncTeX data available"), kStatusMessageDuration);
429 	else
430 		statusBar()->showMessage(tr("SyncTeX: \"%1\"").arg(_synchronizer->syncTeXFilename()), kStatusMessageDuration);
431 }
432 
syncClick(int pageIndex,const QPointF & pos)433 void PDFDocument::syncClick(int pageIndex, const QPointF& pos)
434 {
435 	if (!_synchronizer)
436 		return;
437 
438 	clearSyncHighlight();
439 
440 	/* NOTE: PDF coordinates are upside down (i.e., (0,0) is in the lower left),
441 	 *       whereas SyncTeX expects TeX coordinates (i.e., (0,0) in the upper
442 	 *       left). Hence we need to convert the coordinates
443 	 */
444 	QSharedPointer<QtPDF::Backend::Document> doc = pdfWidget->document().toStrongRef();
445 	if (!doc)
446 		return;
447 	QSharedPointer<QtPDF::Backend::Page> page = doc->page(pageIndex).toStrongRef();
448 	if (!page)
449 		return;
450 
451 	TWSynchronizer::PDFSyncPoint src;
452 	src.filename = curFile;
453 	src.page = pageIndex + 1;
454 	src.rects.append(QRectF(pos.x(), page->pageSizeF().height() - pos.y(), 0, 0));
455 
456 	// Get target point
457 	TWSynchronizer::TeXSyncPoint dest = _synchronizer->syncFromPDF(src);
458 
459 	// Check target point
460 	if (dest.filename.isEmpty() || dest.line < 0)
461 		return;
462 
463 	// Display the result
464 	QDir curDir(QFileInfo(curFile).canonicalPath());
465 	if (dest.col >= 0)
466 		TeXDocument::openDocument(QFileInfo(curDir, dest.filename).canonicalFilePath(), true, true, dest.line, dest.col, dest.col + 1);
467 	else
468 		TeXDocument::openDocument(QFileInfo(curDir, dest.filename).canonicalFilePath(), true, true, dest.line, -1, -1);
469 }
470 
syncFromSource(const QString & sourceFile,int lineNo,int col,bool activatePreview)471 void PDFDocument::syncFromSource(const QString& sourceFile, int lineNo, int col, bool activatePreview)
472 {
473 	if (!_synchronizer)
474 		return;
475 
476 	TWSynchronizer::TeXSyncPoint src;
477 	src.filename = sourceFile;
478 	src.line = lineNo;
479 	src.col = col;
480 
481 	// Get target point
482 	TWSynchronizer::PDFSyncPoint dest = _synchronizer->syncFromTeX(src);
483 
484 	// Check target point
485 	if (dest.page < 1 || QFileInfo(curFile) != QFileInfo(dest.filename))
486 		return;
487 
488 	// Display the result
489 	pdfWidget->goToPage(dest.page - 1);
490 	QPainterPath path;
491 	path.setFillRule(Qt::WindingFill);
492 	foreach(QRectF r, dest.rects)
493 		path.addRect(r);
494 
495 	clearSyncHighlight();
496 	_syncHighlight = pdfWidget->addHighlightPath(dest.page - 1, path, QColor(255, 255, 0, 63));
497 
498 	// Ensure that the synhronization point is displayed (in the center)
499 	pdfWidget->centerOn(_syncHighlight->mapToScene(_syncHighlight->boundingRect().center()));
500 
501 	// Start the highlight removal timer (if applicable)
502 	if (kPDFHighlightDuration > 0)
503 		_syncHighlightRemover.start(kPDFHighlightDuration);
504 
505 	// Update the view (and possibly bring it to the front)
506 	pdfWidget->update();
507 	if (activatePreview)
508 		selectWindow();
509 }
510 
invalidateSyncHighlight()511 void PDFDocument::invalidateSyncHighlight()
512 {
513 	// This slot should be called when the graphics item pointed to by
514 	// _syncHighlight goes out of scope (e.g., because the PDF changed, all pages
515 	// were deleted, and, in the process, all subordinate graphics items as well).
516 	_syncHighlight = NULL;
517 	_syncHighlightRemover.stop();
518 }
519 
clearSyncHighlight()520 void PDFDocument::clearSyncHighlight()
521 {
522 	if (_syncHighlight) {
523 		delete _syncHighlight;
524 		_syncHighlight = NULL;
525 	}
526 	_syncHighlightRemover.stop();
527 }
528 
clearSearchResultHighlight()529 void PDFDocument::clearSearchResultHighlight()
530 {
531 	if (!widget())
532 		return;
533 	widget()->setCurrentSearchResultHighlightBrush(QBrush(Qt::transparent));
534 }
535 
setCurrentFile(const QString & fileName)536 void PDFDocument::setCurrentFile(const QString &fileName)
537 {
538 	curFile = QFileInfo(fileName).canonicalFilePath();
539 	setWindowTitle(tr("%1[*] - %2").arg(TWUtils::strippedName(curFile)).arg(tr(TEXWORKS_NAME)));
540 	TWApp::instance()->updateWindowMenus();
541 }
542 
findDocument(const QString & fileName)543 PDFDocument *PDFDocument::findDocument(const QString &fileName)
544 {
545 	QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
546 
547 	foreach (QWidget *widget, qApp->topLevelWidgets()) {
548 		PDFDocument *theDoc = qobject_cast<PDFDocument*>(widget);
549 		if (theDoc && theDoc->curFile == canonicalFilePath)
550 			return theDoc;
551 	}
552 	return NULL;
553 }
554 
zoomToRight(QWidget * otherWindow)555 void PDFDocument::zoomToRight(QWidget *otherWindow)
556 {
557 	QDesktopWidget *desktop = QApplication::desktop();
558 	QRect screenRect = desktop->availableGeometry(otherWindow == NULL ? this : otherWindow);
559 	screenRect.setTop(screenRect.top() + 22);
560 	screenRect.setLeft((screenRect.left() + screenRect.right()) / 2 + 1);
561 	screenRect.setBottom(screenRect.bottom() - 1);
562 	screenRect.setRight(screenRect.right() - 1);
563 	setGeometry(screenRect);
564 }
565 
showPage(int page)566 void PDFDocument::showPage(int page)
567 {
568 	pageLabel->setText(tr("page %1 of %2").arg(page).arg(pdfWidget->lastPage()));
569 }
570 
showScale(qreal scale)571 void PDFDocument::showScale(qreal scale)
572 {
573 	scaleLabel->setText(tr("%1%").arg(ROUND(scale * 10000.0) / 100.0));
574 }
575 
retypeset()576 void PDFDocument::retypeset()
577 {
578 	if (sourceDocList.count() > 0)
579 		sourceDocList.first()->typeset();
580 }
581 
interrupt()582 void PDFDocument::interrupt()
583 {
584 	if (sourceDocList.count() > 0)
585 		sourceDocList.first()->interrupt();
586 }
587 
goToSource()588 void PDFDocument::goToSource()
589 {
590 	if (sourceDocList.count() > 0)
591 		sourceDocList.first()->selectWindow();
592 	else
593 		// should not occur, the action is supposed to be disabled
594 		actionGo_to_Source->setEnabled(false);
595 }
596 
changedDocument(const QWeakPointer<QtPDF::Backend::Document> newDoc)597 void PDFDocument::changedDocument(const QWeakPointer<QtPDF::Backend::Document> newDoc) {
598 	updateStatusBar();
599 	invalidateSyncHighlight();
600 	enablePageActions(pdfWidget->currentPage());
601 }
602 
enablePageActions(int pageIndex)603 void PDFDocument::enablePageActions(int pageIndex)
604 {
605 //#if !defined(Q_OS_DARWIN)
606 // On Mac OS X, disabling these leads to a crash if we hit the end of document while auto-repeating a key
607 // (seems like a Qt bug, but needs further investigation)
608 // 2008-09-07: seems to no longer be a problem, probably thanks to Qt 4.4 update
609 	actionFirst_Page->setEnabled(pageIndex > 0);
610 	actionPrevious_Page->setEnabled(pageIndex > 0);
611 	actionNext_Page->setEnabled(pageIndex < pdfWidget->lastPage() - 1);
612 	actionLast_Page->setEnabled(pageIndex < pdfWidget->lastPage() - 1);
613 //#endif
614 }
615 
toggleFullScreen()616 void PDFDocument::toggleFullScreen()
617 {
618 	if (windowState() & Qt::WindowFullScreen) {
619 		// exiting full-screen mode
620 		statusBar()->show();
621 		toolBar->show();
622 		showNormal();
623 		actionFull_Screen->setChecked(false);
624 		delete exitFullscreen;
625 	}
626 	else {
627 		// entering full-screen mode
628 		statusBar()->hide();
629 		toolBar->hide();
630 		showFullScreen();
631 		pdfWidget->zoomFitWindow();
632 		actionFull_Screen->setChecked(true);
633 		exitFullscreen = new QShortcut(Qt::Key_Escape, this, SLOT(toggleFullScreen()));
634 	}
635 }
636 
setPageMode(const int newMode)637 void PDFDocument::setPageMode(const int newMode)
638 {
639 	if (!pdfWidget)
640 		return;
641 
642 	switch (newMode) {
643 		case QtPDF::PDFDocumentView::PageMode_SinglePage:
644 		case QtPDF::PDFDocumentView::PageMode_OneColumnContinuous:
645 		case QtPDF::PDFDocumentView::PageMode_TwoColumnContinuous:
646 			pdfWidget->setPageMode((QtPDF::PDFDocumentView::PageMode)newMode);
647 			break;
648 		default:
649 			return;
650 	}
651 }
652 
updatePageMode(const QtPDF::PDFDocumentView::PageMode newMode)653 void PDFDocument::updatePageMode(const QtPDF::PDFDocumentView::PageMode newMode)
654 {
655 	// Mark proper menu item
656 	actionPageMode_Single->setChecked(newMode == QtPDF::PDFDocumentView::PageMode_SinglePage);
657 	actionPageMode_Continuous->setChecked(newMode== QtPDF::PDFDocumentView::PageMode_OneColumnContinuous);
658 	actionPageMode_TwoPagesContinuous->setChecked(newMode == QtPDF::PDFDocumentView::PageMode_TwoColumnContinuous);
659 }
660 
resetMagnifier()661 void PDFDocument::resetMagnifier()
662 {
663 	Q_ASSERT(pdfWidget != NULL);
664 	QSETTINGS_OBJECT(settings);
665 
666 	if (settings.value("circularMagnifier", kDefault_CircularMagnifier).toBool())
667 		pdfWidget->setMagnifierShape(QtPDF::DocumentTool::MagnifyingGlass::Magnifier_Circle);
668 	else
669 		pdfWidget->setMagnifierShape(QtPDF::DocumentTool::MagnifyingGlass::Magnifier_Rectangle);
670 
671 	pdfWidget->setMagnifierSize(magSizes[qBound(0, settings.value("magnifierSize", kDefault_MagnifierSize).toInt() - 1, 2)]);
672 }
673 
setResolution(const double res)674 void PDFDocument::setResolution(const double res)
675 {
676 	Q_ASSERT(pdfWidget != NULL);
677 	if (res > 0)
678 		pdfWidget->setResolution(res);
679 }
680 
enableTypesetAction(bool enabled)681 void PDFDocument::enableTypesetAction(bool enabled)
682 {
683 	actionTypeset->setEnabled(enabled);
684 }
685 
updateTypesettingAction(bool processRunning)686 void PDFDocument::updateTypesettingAction(bool processRunning)
687 {
688 	if (processRunning) {
689 		disconnect(actionTypeset, SIGNAL(triggered()), this, SLOT(retypeset()));
690 		actionTypeset->setIcon(QIcon(":/images/tango/process-stop.png"));
691 		actionTypeset->setText(tr("Abort typesetting"));
692 		connect(actionTypeset, SIGNAL(triggered()), this, SLOT(interrupt()));
693 		enableTypesetAction(true);
694 	}
695 	else {
696 		disconnect(actionTypeset, SIGNAL(triggered()), this, SLOT(interrupt()));
697 		actionTypeset->setIcon(QIcon(":/images/images/runtool.png"));
698 		actionTypeset->setText(tr("Typeset"));
699 		connect(actionTypeset, SIGNAL(triggered()), this, SLOT(retypeset()));
700 	}
701 }
702 
dragEnterEvent(QDragEnterEvent * event)703 void PDFDocument::dragEnterEvent(QDragEnterEvent *event)
704 {
705 	// Only accept files for now
706 	event->ignore();
707 	if (event->mimeData()->hasUrls()) {
708 		const QList<QUrl> urls = event->mimeData()->urls();
709 		foreach (const QUrl& url, urls) {
710 			if (url.scheme() == "file") {
711 				event->acceptProposedAction();
712 				break;
713 			}
714 		}
715 	}
716 }
717 
dropEvent(QDropEvent * event)718 void PDFDocument::dropEvent(QDropEvent *event)
719 {
720 	event->ignore();
721 	if (event->mimeData()->hasUrls()) {
722 		const QList<QUrl> urls = event->mimeData()->urls();
723 		foreach (const QUrl& url, urls)
724 			if (url.scheme() == "file")
725 				TWApp::instance()->openFile(url.toLocalFile());
726 		event->acceptProposedAction();
727 	}
728 }
729 
contextMenuEvent(QContextMenuEvent * event)730 void PDFDocument::contextMenuEvent(QContextMenuEvent *event)
731 {
732 	Q_ASSERT(pdfWidget != NULL);
733 	QMenu menu(this);
734 
735 	if (_synchronizer && _synchronizer->isValid()) {
736 		QAction *act = new QAction(tr("Jump to Source"), &menu);
737 		act->setData(QVariant(event->pos()));
738 		connect(act, SIGNAL(triggered()), this, SLOT(jumpToSource()));
739 		menu.addAction(act);
740 		menu.addSeparator();
741 	}
742 
743 	menu.addAction(tr("Zoom In"), pdfWidget, SLOT(zoomIn()));
744 	menu.addAction(tr("Zoom Out"), pdfWidget, SLOT(zoomOut()));
745 	menu.addAction(tr("Actual Size"), pdfWidget, SLOT(zoom100()));
746 	menu.addAction(tr("Fit to Width"), pdfWidget, SLOT(zoomFitWidth()));
747 	menu.addAction(tr("Fit to Window"), pdfWidget, SLOT(zoomFitWindow()));
748 
749 	menu.exec(event->globalPos());
750 }
751 
jumpToSource()752 void PDFDocument::jumpToSource()
753 {
754 	QAction *act = qobject_cast<QAction*>(sender());
755 	if (!act || !pdfWidget)
756 		return;
757 
758 	QPoint eventPos = act->data().toPoint();
759 	QPointF scenePos = pdfWidget->mapToScene(pdfWidget->mapFrom(this, eventPos));
760 
761 	// Map to scene, then map to page
762 	QtPDF::PDFDocumentScene * scene = qobject_cast<QtPDF::PDFDocumentScene*>(pdfWidget->scene());
763 	if (!scene)
764 		return;
765 	QtPDF::PDFPageGraphicsItem * page = (QtPDF::PDFPageGraphicsItem*)(scene->pageAt(scenePos));
766 	if (!page)
767 		return;
768 
769 	syncClick(scene->pageNumFor(page), page->mapToPage(page->mapFromScene(scenePos)));
770 }
771 
doFindDialog()772 void PDFDocument::doFindDialog()
773 {
774 	if (PDFFindDialog::doFindDialog(this) == QDialog::Accepted)
775 		doFindAgain(true);
776 }
777 
doFindAgain(bool newSearch)778 void PDFDocument::doFindAgain(bool newSearch /* = false */)
779 {
780 	QSETTINGS_OBJECT(settings);
781 
782 	QString	searchText = settings.value("searchText").toString();
783 	if (searchText.isEmpty())
784 		return;
785 
786 	QtPDF::Backend::SearchFlags searchFlags;
787 	QTextDocument::FindFlags flags = (QTextDocument::FindFlags)settings.value("searchFlags").toInt();
788 
789 	if ((flags & QTextDocument::FindCaseSensitively) == 0)
790 		searchFlags |= QtPDF::Backend::Search_CaseInsensitive;
791 	if ((flags & QTextDocument::FindBackward) != 0)
792 		searchFlags |= QtPDF::Backend::Search_Backwards;
793 
794 	widget()->search(searchText, searchFlags);
795 }
796 
searchResultHighlighted(const int pageNum,const QList<QPolygonF> region)797 void PDFDocument::searchResultHighlighted(const int pageNum, const QList<QPolygonF> region)
798 {
799 	QSETTINGS_OBJECT(settings);
800 
801 	widget()->setCurrentSearchResultHighlightBrush(_searchResultHighlightBrush);
802 	if (kPDFHighlightDuration > 0)
803 		_searchResultHighlightRemover.start(kPDFHighlightDuration);
804 
805 	if (hasSyncData() && settings.value("searchPdfSync").toBool() && !region.isEmpty()) {
806 		// emit a syncClick message at the center of the left edge of the (bounding)
807 		// rect. To ensure hit-testing succeeds later on, we add an offset of 1e-5
808 		// (for rectangles of finite width)
809 		QRectF r = region[0].boundingRect();
810 		QPointF pt(r.left() + 1e-5 * qMin(r.width(), 1.), r.center().y());
811 		emit syncClick(pageNum, pt);
812 	}
813 }
814 
setDefaultScale()815 void PDFDocument::setDefaultScale() {
816 	QSETTINGS_OBJECT(settings);
817 	switch (settings.value("scaleOption", kDefault_PreviewScaleOption).toInt()) {
818 		case 2:
819 			pdfWidget->zoomFitWidth();
820 			break;
821 		case 3:
822 			pdfWidget->zoomFitWindow();
823 			break;
824 		case 4:
825 			pdfWidget->setZoomLevel(settings.value("previewScale", kDefault_PreviewScale).toFloat() / 100.);
826 			break;
827 		default:
828 			pdfWidget->zoom100();
829 			break;
830 	}
831 }
832 
maybeOpenUrl(const QUrl url)833 void PDFDocument::maybeOpenUrl(const QUrl url)
834 {
835 	// Opening URLs could be a security risk, so ask the user (but make "yes,
836 	// proceed the default option - after all the user typically clicked on the
837 	// link deliberately)
838 	if (QMessageBox::question(this, tr("Open URL"), tr("You are in the process of opening the URL %1. Opening unknown or untrusted web adresses can be a security risk.\nDo you want to continue?").arg(url.toString()),
839 	                          QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes)
840 		QDesktopServices::openUrl(url);
841 }
842 
maybeOpenPdf(QString filename,QtPDF::PDFDestination destination,bool newWindow)843 void PDFDocument::maybeOpenPdf(QString filename, QtPDF::PDFDestination destination, bool newWindow)
844 {
845 	// Unlike in maybeOpenUrl, this function only works on local PDF files which
846 	// we assume are safe.
847 	// TODO: We currently ignore the value of newWindow and always open a new
848 	// window. This avoids the need to update/invalidate all pointers to this
849 	// PDFDocument (e.g., in the TeXDocument associated with it) to notify the
850 	// other parts of the code that a completely new and unrelated document is
851 	// loaded here now.
852 	PDFDocument * pdf = qobject_cast<PDFDocument*>(TWApp::instance()->openFile(filename));
853 	if (!pdf || !pdf->widget())
854 		return;
855 	pdf->widget()->goToPDFDestination(destination, false);
856 }
857 
858 
print()859 void PDFDocument::print()
860 {
861 	// Currently, printing is not supported in a reliable, cross-platform way
862 	// Instead, offer to open the document in the system's default viewer
863 
864 	QString msg = tr("Unfortunately, this version of %1 is unable to print Pdf documents due to various technical reasons.\n").arg(TEXWORKS_NAME);
865 	msg += tr("Do you want to open the file in the default viewer for printing instead?");
866 	msg += tr(" (remember to close it again to avoid access problems)");
867 
868 	if(QMessageBox::information(this,
869 		tr("Print Pdf..."), msg,
870 		QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes
871 	) {
872 		QDesktopServices::openUrl(QUrl::fromLocalFile(curFile));
873 	}
874 }
875 
showScaleContextMenu(const QPoint pos)876 void PDFDocument::showScaleContextMenu(const QPoint pos)
877 {
878 	static QMenu * contextMenu = NULL;
879 	static QSignalMapper * contextMenuMapper = NULL;
880 	QAction * a;
881 
882 	if (contextMenu == NULL) {
883 		contextMenu = new QMenu(this);
884 		contextMenuMapper = new QSignalMapper(this);
885 
886 		contextMenu->addAction(actionFit_to_Width);
887 		contextMenu->addAction(actionFit_to_Window);
888 		contextMenu->addSeparator();
889 
890 		a = contextMenu->addAction(tr("Custom..."));
891 		connect(a, SIGNAL(triggered()), this, SLOT(doScaleDialog()));
892 
893 		a = contextMenu->addAction("200%");
894 		connect(a, SIGNAL(triggered()), contextMenuMapper, SLOT(map()));
895 		contextMenuMapper->setMapping(a, "2");
896 		a = contextMenu->addAction("150%");
897 		connect(a, SIGNAL(triggered()), contextMenuMapper, SLOT(map()));
898 		contextMenuMapper->setMapping(a, "1.5");
899 		// "100%" corresponds to "Actual Size", but we keep the numeric value
900 		// here for consistency
901 		a = contextMenu->addAction("100%");
902 		a->setShortcut(actionActual_Size->shortcut());
903 		connect(a, SIGNAL(triggered()), contextMenuMapper, SLOT(map()));
904 		contextMenuMapper->setMapping(a, "1");
905 		a = contextMenu->addAction("75%");
906 		connect(a, SIGNAL(triggered()), contextMenuMapper, SLOT(map()));
907 		contextMenuMapper->setMapping(a, ".75");
908 		a = contextMenu->addAction("50%");
909 		connect(a, SIGNAL(triggered()), contextMenuMapper, SLOT(map()));
910 		contextMenuMapper->setMapping(a, ".5");
911 
912 		connect(contextMenuMapper, SIGNAL(mapped(const QString&)), this, SLOT(setScaleFromContextMenu(const QString&)));
913 	}
914 
915 	contextMenu->popup(scaleLabel->mapToGlobal(pos));
916 }
917 
setScaleFromContextMenu(const QString & strZoom)918 void PDFDocument::setScaleFromContextMenu(const QString & strZoom)
919 {
920 	bool conversionOK = false;
921 	float zoom = strZoom.toFloat(&conversionOK);
922 	// FIXME: This should actually use the point the context menu was opened at as
923 	// anchor for zooming. Currently, arbitrary coordinates are not supported yet
924 	// (and using QGraphicsView::AnchorUnderMouse would use the position of the
925 	// mouse cursor when the user clicks on the respective menu item - which will
926 	// be somewhere else
927 	if (pdfWidget && conversionOK)
928 		pdfWidget->setZoomLevel(zoom);
929 }
930 
updateStatusBar()931 void PDFDocument::updateStatusBar()
932 {
933 	Q_ASSERT(pdfWidget != NULL);
934 	showPage(pdfWidget->currentPage() + 1);
935 	showScale(pdfWidget->zoomLevel());
936 }
937 
setMouseMode(const int newMode)938 void PDFDocument::setMouseMode(const int newMode)
939 {
940 	Q_ASSERT(pdfWidget != NULL);
941 	pdfWidget->setMouseMode((QtPDF::PDFDocumentView::MouseMode)newMode);
942 }
943 
doPageDialog()944 void PDFDocument::doPageDialog()
945 {
946 	bool ok;
947 	Q_ASSERT(pdfWidget != NULL);
948 
949 #if QT_VERSION < 0x050000
950 	int pageNo = QInputDialog::getInteger(this, tr("Go to Page"),
951 									tr("Page number:"), pdfWidget->currentPage() + 1,
952                   1, pdfWidget->lastPage(), 1, &ok);
953 #else
954 	int pageNo = QInputDialog::getInt(this, tr("Go to Page"),
955 									tr("Page number:"), pdfWidget->currentPage() + 1,
956                   1, pdfWidget->lastPage(), 1, &ok);
957 #endif
958 	if (ok)
959 		pdfWidget->goToPage(pageNo - 1);
960 }
961 
doScaleDialog()962 void PDFDocument::doScaleDialog()
963 {
964 	bool ok;
965 	Q_ASSERT(pdfWidget != NULL);
966 
967 	double newScale = QInputDialog::getDouble(this, tr("Set Zoom"), tr("Zoom level:"), 100 * pdfWidget->zoomLevel(), 0, 2147483647, 0, &ok);
968 	if (ok)
969 		pdfWidget->setZoomLevel(newScale / 100);
970 }
971