1 /*
2     dspdfviewer - Dual Screen PDF Viewer for LaTeX-Beamer
3     Copyright (C) 2012  Danny Edel <mail@danny-edel.de>
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 along
16     with this program; if not, write to the Free Software Foundation, Inc.,
17     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19 
20 
21 #include "pdfviewerwindow.h"
22 #include <QApplication>
23 #include <QDesktopWidget>
24 #include <QHBoxLayout>
25 #include <QLabel>
26 #include <QMouseEvent>
27 #include <QScreen>
28 #if defined(POPPLER_QT5) && defined(_WIN32)
29 #include <QWindow>
30 #endif
31 #include "debug.h"
32 #include <QInputDialog>
33 #include <QMessageBox>
34 #include "sconnect.h"
35 #include <cstdlib>
36 #include <boost/numeric/conversion/cast.hpp>
37 #include "ui_keybindings.h"
38 
39 using boost::numeric_cast;
40 
setMonitor(const unsigned int monitor)41 void PDFViewerWindow::setMonitor(const unsigned int monitor)
42 {
43   if ( m_monitor != monitor )
44   {
45     m_monitor = monitor;
46     reposition();
47   }
48 }
49 
getMonitor() const50 unsigned int PDFViewerWindow::getMonitor() const
51 {
52   return m_monitor;
53 }
54 
PDFViewerWindow(unsigned int monitor,PagePart pagePart,bool showInformationLine,const RuntimeConfiguration & r,const WindowRole & wr,bool enabled)55 PDFViewerWindow::PDFViewerWindow(unsigned int monitor, PagePart pagePart, bool showInformationLine, const RuntimeConfiguration& r, const WindowRole& wr, bool enabled):
56   QWidget(),
57   ui(),
58   m_enabled(enabled),
59   m_monitor(monitor),
60   currentImage(),
61   blank(false),
62   informationLineVisible(false),
63   currentPageNumber(0),
64   minimumPageNumber(0),
65   maximumPageNumber(65535),
66   correctImageRendered(false),
67   myPart(pagePart),
68   windowRole(wr),
69   runtimeConfiguration(r),
70   linkAreas()
71 {
72   if ( ! enabled )
73     return;
74   ui.setupUi(this);
75   unsigned mainImageHeight=100-r.bottomPaneHeight();
76   ui.verticalLayout->setStretch(0, numeric_cast<int>(mainImageHeight) );
77   ui.verticalLayout->setStretch(1, numeric_cast<int>(r.bottomPaneHeight()) );
78   setWindowRole(to_QString(wr));
79   /*: User visible Window Title Line */
80 	if ( windowRole == WindowRole::AudienceWindow ) {
81 		setWindowTitle(tr("DS PDF Viewer - Audience Window"));
82 	} else {
83 		setWindowTitle(tr("DS PDF Viewer - Secondary Window"));
84 	}
85   if ( !showInformationLine || ! r.showPresenterArea()) {
86     /* If the information line is disabled because we're the primary screen,
87      * or the user explicitly said so, disable it completely.
88      */
89     hideInformationLine();
90   }
91   else {
92     /* Enable the information line, but control visibility of the components as requested by the user.
93      */
94     this->showInformationLine();
95     ui.wallClock->setVisible(r.showWallClock());
96     ui.thumbnailArea->setVisible(r.showThumbnails());
97     ui.slideClock->setVisible(r.showSlideClock());
98     ui.presentationClock->setVisible(r.showPresentationClock());
99   }
100 
101   reposition(); // This will fullscreen on its own
102 }
103 
104 
105 
reposition()106 void PDFViewerWindow::reposition()
107 {
108   if ( ! m_enabled )
109     return;
110   this->setWindowFlags(windowFlags() & ~Qt::FramelessWindowHint);
111   this->showNormal();
112 #if defined(POPPLER_QT5) && defined(_WIN32)
113   static QList<QScreen *> screens = QApplication::screens();
114   if ( m_monitor < numeric_cast<unsigned>(screens.count()) )
115     this->windowHandle()->setScreen(screens[m_monitor]);
116   else
117     this->windowHandle()->setScreen(0);
118   this->showFullScreen();
119 #else
120   auto screens = QGuiApplication::screens();
121   int screen_number = numeric_cast<int>(getMonitor());
122   if ((screen_number < 0) || (screen_number >= screens.length())) {
123     screen_number = 0;
124   }
125   QRect rect = screens.at(screen_number)->geometry();
126   move(rect.topLeft());
127   resize( rect.size() );
128   this->showFullScreen();
129 #endif
130   /* Note: The focus should be on the primary window, because at least
131    * Gnome draws the primary window's border onto the secondary.
132    *
133    * I dont mind the border on my helper screen, but the
134    * audience shouldnt see it.
135    */
136   if ( !informationLineVisible )
137     this->activateWindow();
138 //  this->resize( 100, 100 );
139  // this->move(rect.topLeft());
140   //this->showFullScreen();
141 
142 }
143 
displayImage(QImage image)144 void PDFViewerWindow::displayImage(QImage image)
145 {
146   ui.imageLabel->setText( QString() );
147   ui.imageLabel->resize( image.size() );
148   if ( blank ) {
149     // If we're supposed to display a blank image, leave it at this state.
150     return;
151   }
152   currentImage= image;
153   ui.imageLabel->setPixmap(QPixmap::fromImage(image));
154   //imageArea->setWidgetResizable(true);
155 
156   /*
157   if ( geometry().size() != getTargetImageSize() )
158     reposition();
159   */
160 }
161 
162 
wheelEvent(QWheelEvent * e)163 void PDFViewerWindow::wheelEvent(QWheelEvent* e)
164 {
165     // QWidget::wheelEvent(e);
166 
167     if ( e->delta() > 0 )
168     {
169       DEBUGOUT << "Back";
170       emit previousPageRequested();
171     }
172     else{
173       DEBUGOUT << "Next";
174       emit nextPageRequested();
175     }
176 	e->accept();
177 }
178 
keyPressEvent(QKeyEvent * e)179 void PDFViewerWindow::keyPressEvent(QKeyEvent* e)
180 {
181     QWidget::keyPressEvent(e);
182 
183     switch( e->key() )
184     {
185       case Qt::Key_F1:
186       case Qt::Key_Question: // Help
187 	keybindingsPopup();
188 	break;
189       case Qt::Key_G:
190 	changePageNumberDialog();
191 	break;
192       case Qt::Key_F12:
193       case Qt::Key_S: //Swap
194 	emit screenSwapRequested();
195 	break;
196       case Qt::Key_Escape:
197       case Qt::Key_Q: //quit
198 	emit quitRequested();
199 	break;
200       case Qt::Key_T:
201 	emit secondScreenFunctionToggleRequested();
202 	break;
203       case Qt::Key_D:
204 	emit secondScreenDuplicateRequested();
205 	break;
206       case Qt::Key_Space:
207       case Qt::Key_Enter:
208       case Qt::Key_Return:
209       case Qt::Key_PageDown:
210       case Qt::Key_Down:
211       case Qt::Key_Right:
212       case Qt::Key_F: // Forward
213       case Qt::Key_N: // Next
214 	emit nextPageRequested();
215 	break;
216       case Qt::Key_PageUp:
217       case Qt::Key_Up:
218       case Qt::Key_Left:
219       case Qt::Key_Backspace:
220       case Qt::Key_P: //Previous
221 	emit previousPageRequested();
222 	break;
223       case Qt::Key_B:
224       case Qt::Key_Period:
225 	emit blankToggleRequested();
226 	break;
227       case Qt::Key_Home:
228       case Qt::Key_H: //Home
229 	emit restartRequested();
230 	break;
231     }
232 }
233 
234 
getTargetImageSize() const235 QSize PDFViewerWindow::getTargetImageSize() const
236 {
237   return ui.imageArea->geometry().size();
238 }
239 
getPreviewImageSize()240 QSize PDFViewerWindow::getPreviewImageSize()
241 {
242   QSize completeThumbnailArea = ui.thumbnailArea->frameRect().size();
243   DEBUGOUT << "Space for all thumbnails:" << completeThumbnailArea;
244   /** FIXME Work needed:
245    * since this space must fit three images, we divide horizontal size by three
246    */
247   QSize thirdThumbnailArea ( completeThumbnailArea.width()/3, completeThumbnailArea.height());
248   static QSize lastThumbnailSize = thirdThumbnailArea;
249   if ( lastThumbnailSize != thirdThumbnailArea ) {
250     lastThumbnailSize=thirdThumbnailArea;
251     emit rerenderRequested();
252   }
253   DEBUGOUT << "Space for one thumbnail:" << thirdThumbnailArea;
254 
255   return thirdThumbnailArea;
256 }
257 
258 
mousePressEvent(QMouseEvent * e)259 void PDFViewerWindow::mousePressEvent(QMouseEvent* e)
260 {
261     // QWidget::mousePressEvent(e);
262 	if ( e->button() == Qt::LeftButton ) {
263 		emit nextPageRequested();
264 	} else if ( e->button() == Qt::RightButton ) {
265 		emit previousPageRequested();
266 	}
267 	// Ignore other buttons.
268 }
269 
hideInformationLine()270 void PDFViewerWindow::hideInformationLine()
271 {
272   if ( ! m_enabled )
273     return;
274   informationLineVisible=false;
275   this->ui.bottomArea->hide();
276 }
277 
isInformationLineVisible() const278 bool PDFViewerWindow::isInformationLineVisible() const
279 {
280   return informationLineVisible;
281 }
282 
showInformationLine()283 void PDFViewerWindow::showInformationLine()
284 {
285   if ( ! m_enabled )
286     return;
287   informationLineVisible=true;
288   this->ui.bottomArea->show();
289 }
290 
addThumbnail(uint pageNumber,QImage thumbnail)291 void PDFViewerWindow::addThumbnail(uint pageNumber, QImage thumbnail)
292 {
293   if ( pageNumber == currentPageNumber-1)
294     ui.previousThumbnail->setPixmap(QPixmap::fromImage(thumbnail));
295   else if ( pageNumber == currentPageNumber )
296     ui.currentThumbnail -> setPixmap(QPixmap::fromImage(thumbnail));
297   else if ( pageNumber == currentPageNumber+1 )
298     ui.nextThumbnail->setPixmap(QPixmap::fromImage(thumbnail));
299 }
300 
301 
302 
renderedPageIncoming(QSharedPointer<RenderedPage> renderedPage)303 void PDFViewerWindow::renderedPageIncoming(QSharedPointer< RenderedPage > renderedPage)
304 {
305   if ( ! m_enabled )
306     return;
307 
308   // If we're blank, don't do anything with incoming renders.
309   // Un-blanking will request a rerender.
310   if ( blank )
311     return;
312 
313 
314   // It might be a thumbnail. If we're waiting for one, check if it would fit.
315   if ( isInformationLineVisible()
316     && renderedPage->getPart() == runtimeConfiguration.thumbnailPagePart()
317     && renderedPage->getIdentifier().requestedPageSize() == this->getPreviewImageSize() ) {
318     this->addThumbnail(renderedPage->getPageNumber(), renderedPage->getImage());
319   }
320 
321 
322   // If we are not waiting for an image, ignore incoming answers.
323   if ( correctImageRendered )
324     return;
325 
326   if ( renderedPage->getPageNumber() != this->currentPageNumber )
327     return; // This page is not for us. Ignore it.
328 
329   if ( renderedPage->getPart() != this->myPart )
330     return; // This is not our part
331 
332   // There is an image incoming that might fit.
333   displayImage(renderedPage->getImage());
334 
335   // It was even the right size! Yeah!
336   if ( renderedPage->getIdentifier().requestedPageSize() == getTargetImageSize() ) {
337     if ( this->runtimeConfiguration.hyperlinkSupport() ) {
338       this->parseLinks(renderedPage->getLinks());
339     }
340     this->correctImageRendered= true;
341   }
342 }
343 
showLoadingScreen(uint pageNumberToWaitFor)344 void PDFViewerWindow::showLoadingScreen(uint pageNumberToWaitFor)
345 {
346   if ( !m_enabled )
347     return;
348   // If we're blanked, don't render anything.
349   if ( blank )
350     return;
351 
352 
353   /// FIXME Loading image
354 
355   this->currentPageNumber = pageNumberToWaitFor;
356   this->correctImageRendered = false;
357   this->currentImage = QImage();
358   ui.imageLabel->setPixmap(QPixmap());
359   ui.imageLabel->setText(tr("Loading page number %1").arg(pageNumberToWaitFor) );
360 
361   /** Clear Thumbnails, they will come back in soon */
362   ui.previousThumbnail->setPixmap( QPixmap() );
363   ui.currentThumbnail->setPixmap( QPixmap() );
364   ui.nextThumbnail->setPixmap( QPixmap() );
365 
366 }
367 
getMyPagePart() const368 PagePart PDFViewerWindow::getMyPagePart() const
369 {
370   return myPart;
371 }
372 
resizeEvent(QResizeEvent * resizeEvent)373 void PDFViewerWindow::resizeEvent(QResizeEvent* resizeEvent)
374 {
375   if ( !m_enabled )
376     return;
377 
378   QWidget::resizeEvent(resizeEvent);
379   DEBUGOUT << "Resize event" << resizeEvent;
380   DEBUGOUT << "Resized from" << resizeEvent->oldSize() << "to" << resizeEvent->size() << ", requesting re-render.";
381 	static bool i3shellcode_executed = false;
382 	if (
383 		windowRole == WindowRole::AudienceWindow &&
384 		runtimeConfiguration.i3workaround() &&
385 		resizeEvent->spontaneous() &&
386 		// i3 generates a spontaneous resize.
387 		! i3shellcode_executed
388 		// Make sure to do this only once
389 		) {
390 		// QApplication::flush(); // Make sure the window has been painted
391 		// This is the second screen. It has now been created.
392 		// so we should call the i3 shellcode now
393 		const std::string shellcode = runtimeConfiguration.i3workaround_shellcode();
394 		DEBUGOUT << "Running i3 workaround shellcode" << shellcode.c_str();
395 		int rc = std::system( shellcode.c_str() );
396 		DEBUGOUT << "Return code of i3-workaround was" << rc ;
397 		i3shellcode_executed=true;
398 	}
399   emit rerenderRequested();
400 }
401 
timeToString(const QTime & time) const402 QString PDFViewerWindow::timeToString(const QTime & time) const
403 {
404   return time.toString( tr("HH:mm:ss", "This is used by QTime::toString. See its documentation before changing this.") );
405 }
406 
timeToString(int milliseconds) const407 QString PDFViewerWindow::timeToString(int milliseconds) const
408 {
409   return timeToString(QTime(0,0).addMSecs(milliseconds));
410 }
411 
412 
updatePresentationClock(const QTime & presentationClock)413 void PDFViewerWindow::updatePresentationClock(const QTime& presentationClock)
414 {
415   ui.presentationClock->setText( QCoreApplication::translate("Form", "Total\n%1").arg(timeToString(presentationClock)));
416 }
417 
updateSlideClock(const QTime & slideClock)418 void PDFViewerWindow::updateSlideClock(const QTime& slideClock)
419 {
420   ui.slideClock->setText(timeToString(slideClock) );
421 }
422 
updateWallClock(const QTime & wallClock)423 void PDFViewerWindow::updateWallClock(const QTime& wallClock)
424 {
425   ui.wallClock->setText(timeToString(wallClock));
426 }
427 
keybindingsPopup()428 void PDFViewerWindow::keybindingsPopup()
429 {
430 	Ui::KeybindingsDialog keybindUi;
431 	QDialog popup;
432 	keybindUi.setupUi(&popup);
433 	keybindUi.label_versionstring->setText(
434 		keybindUi.label_versionstring->text().arg(
435 			QString::fromUtf8(DSPDFVIEWER_VERSION )
436 		)
437 	);
438 	popup.exec();
439 }
440 
changePageNumberDialog()441 void PDFViewerWindow::changePageNumberDialog()
442 {
443   bool ok;
444   /* While PDF counts zero-based, users probably think that the first
445    * page is called "1".
446    */
447   uint displayMinNumber = minimumPageNumber+1;
448   uint displayMaxNumber = maximumPageNumber+1;
449   uint displayCurNumber = currentPageNumber+1;
450   int targetPageNumber = QInputDialog::getInt(this,
451 	/* Window Caption */ tr("Select page"),
452 	/* Input field caption */
453 	tr("Jump to page number (%1-%2):").arg(displayMinNumber).arg(displayMaxNumber),
454 	/* Starting number. */
455 	numeric_cast<int>(displayCurNumber),
456 	/* minimum value */
457 	numeric_cast<int>(displayMinNumber),
458 	/* maximum value */
459 	numeric_cast<int>(displayMaxNumber),
460 	/* Step */
461 	1,
462 	/* Did the user accept? */
463 	&ok);
464   targetPageNumber-=1; // Convert back to zero-based numbering scheme
465   if ( ok )
466   {
467     emit pageRequested(numeric_cast<uint>(targetPageNumber));
468   }
469 }
470 
setPageNumberLimits(uint minPageNumber,uint maxPageNumber)471 void PDFViewerWindow::setPageNumberLimits(uint minPageNumber, uint maxPageNumber)
472 {
473   this->minimumPageNumber = minPageNumber;
474   this->maximumPageNumber = maxPageNumber;
475 }
476 
setBlank(const bool newBlank)477 void PDFViewerWindow::setBlank(const bool newBlank)
478 {
479   if ( this->blank == newBlank)
480     return;
481   /* State changes. request re-render */
482   this->blank = newBlank;
483   DEBUGOUT << "Changing blank state to" << blank;
484   if ( blank ) {
485     ui.imageLabel->clear();
486   } else {
487     emit rerenderRequested();
488   }
489 }
490 
isBlank() const491 bool PDFViewerWindow::isBlank() const
492 {
493   return blank;
494 }
495 
setMyPagePart(const PagePart & newPagePart)496 void PDFViewerWindow::setMyPagePart(const PagePart& newPagePart)
497 {
498   this->myPart = newPagePart;
499 }
500 
parseLinks(QList<AdjustedLink> links)501 void PDFViewerWindow::parseLinks(QList< AdjustedLink > links)
502 {
503   QList< HyperlinkArea* > newLinkAreas;
504   for( AdjustedLink const & link: links ) {
505     const QRectF& rect = link.linkArea();
506     if ( rect.isNull() ) {
507       WARNINGOUT << "Null Link Area not supported yet.";
508       continue;
509     }
510     const Poppler::Link::LinkType& type = link.link()->linkType();
511     if ( type == Poppler::Link::LinkType::Goto ) {
512       // type is Goto. Bind it to imageLabel
513       const Poppler::LinkGoto& linkGoto = dynamic_cast<const Poppler::LinkGoto&>( * link.link() );
514       if( linkGoto.isExternal() ) {
515 	WARNINGOUT << "External links are not supported yet.";
516 	continue;
517       }
518       HyperlinkArea* linkArea = new HyperlinkArea(ui.imageLabel, link);
519       sconnect( linkArea, SIGNAL(gotoPageRequested(uint)), this, SLOT(linkClicked(uint)) );
520       newLinkAreas.append(linkArea);
521     }
522     else {
523       WARNINGOUT << "Types other than Goto are not supported yet.";
524       continue;
525     }
526   }
527   // Schedule all old links for deletion
528   for( HyperlinkArea* hla: this->linkAreas)
529     hla->deleteLater();
530   // Add the new list
531   this->linkAreas = newLinkAreas;
532 }
533 
linkClicked(uint targetNumber)534 void PDFViewerWindow::linkClicked(uint targetNumber)
535 {
536   DEBUGOUT << "Hyperlink detected";
537   emit pageRequested(targetNumber);
538 }
539