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