|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the PDF viewer main window. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import pathlib |
|
12 |
|
13 from PyQt6.QtCore import Qt, pyqtSignal, QSize, pyqtSlot, QPointF |
|
14 from PyQt6.QtGui import QAction, QKeySequence |
|
15 from PyQt6.QtPdf import QPdfDocument |
|
16 from PyQt6.QtPdfWidgets import QPdfView |
|
17 from PyQt6.QtWidgets import ( |
|
18 QWhatsThis, QMenu, QTabWidget, QSplitter, QSpinBox |
|
19 ) |
|
20 |
|
21 from eric7 import Preferences |
|
22 from eric7.EricGui import EricPixmapCache |
|
23 from eric7.EricGui.EricAction import EricAction |
|
24 from eric7.EricWidgets import EricFileDialog, EricMessageBox |
|
25 from eric7.EricWidgets.EricMainWindow import EricMainWindow |
|
26 from eric7.Globals import recentNamePdfFiles |
|
27 from eric7.SystemUtilities import FileSystemUtilities |
|
28 |
|
29 |
|
30 class PdfViewerWindow(EricMainWindow): |
|
31 """ |
|
32 Class implementing the PDF viewer main window. |
|
33 |
|
34 @signal editorClosed() emitted after the window was requested to close down |
|
35 """ |
|
36 |
|
37 editorClosed = pyqtSignal() |
|
38 |
|
39 maxMenuFilePathLen = 75 |
|
40 |
|
41 def __init__(self, fileName="", parent=None, fromEric=False, project=None): |
|
42 """ |
|
43 Constructor |
|
44 |
|
45 @param fileName name of a file to load on startup |
|
46 @type str |
|
47 @param parent parent widget of this window |
|
48 @type QWidget |
|
49 @param fromEric flag indicating whether it was called from within |
|
50 eric |
|
51 @type bool |
|
52 @param project reference to the project object |
|
53 @type Project |
|
54 """ |
|
55 super().__init__(parent) |
|
56 self.setObjectName("eric7_pdf_viewer") |
|
57 |
|
58 self.__fromEric = fromEric |
|
59 self.setWindowIcon(EricPixmapCache.getIcon("ericPdf")) |
|
60 |
|
61 if not self.__fromEric: |
|
62 self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet")) |
|
63 |
|
64 self.__pdfDocument = QPdfDocument(self) |
|
65 |
|
66 # TODO: insert central widget here |
|
67 self.__cw = QSplitter(Qt.Orientation.Horizontal, self) |
|
68 self.__info = QTabWidget(self) |
|
69 self.__cw.addWidget(self.__info) |
|
70 self.__view = QPdfView(self) |
|
71 self.__view.setDocument(self.__pdfDocument) |
|
72 self.__cw.addWidget(self.__view) |
|
73 self.setCentralWidget(self.__cw) |
|
74 |
|
75 g = Preferences.getGeometry("PdfViewerGeometry") |
|
76 if g.isEmpty(): |
|
77 s = QSize(1000, 1000) |
|
78 self.resize(s) |
|
79 self.__cw.setSizes([300, 700]) |
|
80 else: |
|
81 self.restoreGeometry(g) |
|
82 |
|
83 self.__initActions() |
|
84 self.__initMenus() |
|
85 self.__initToolbars() |
|
86 self.__createStatusBar() |
|
87 |
|
88 state = Preferences.getPdfViewer("PdfViewerState") |
|
89 self.restoreState(state) |
|
90 |
|
91 self.__project = project |
|
92 self.__lastOpenPath = "" |
|
93 |
|
94 self.__recent = [] |
|
95 self.__loadRecent() |
|
96 |
|
97 self.__setCurrentFile("") |
|
98 self.__setViewerTitle("") |
|
99 if fileName: |
|
100 self.__loadPdfFile(fileName) |
|
101 |
|
102 self.__checkActions() |
|
103 |
|
104 def __initActions(self): |
|
105 """ |
|
106 Private method to define the user interface actions. |
|
107 """ |
|
108 # list of all actions |
|
109 self.__actions = [] |
|
110 |
|
111 self.__initFileActions() |
|
112 self.__initHelpActions() |
|
113 |
|
114 def __initFileActions(self): |
|
115 """ |
|
116 Private method to define the file related user interface actions. |
|
117 """ |
|
118 # TODO: not yet implemented |
|
119 self.exitAct = EricAction( |
|
120 self.tr("Quit"), |
|
121 EricPixmapCache.getIcon("exit"), |
|
122 self.tr("&Quit"), |
|
123 QKeySequence(self.tr("Ctrl+Q", "File|Quit")), |
|
124 0, |
|
125 self, |
|
126 "pdfviewer_file_quit", |
|
127 ) |
|
128 self.exitAct.setStatusTip(self.tr("Quit the PDF Viewer")) |
|
129 self.exitAct.setWhatsThis(self.tr("""<b>Quit</b><p>Quit the PDF Viewer.</p>""")) |
|
130 self.__actions.append(self.exitAct) |
|
131 |
|
132 def __initHelpActions(self): |
|
133 """ |
|
134 Private method to create the Help actions. |
|
135 """ |
|
136 self.aboutAct = EricAction( |
|
137 self.tr("About"), self.tr("&About"), 0, 0, self, "pdfviewer_help_about" |
|
138 ) |
|
139 self.aboutAct.setStatusTip(self.tr("Display information about this software")) |
|
140 self.aboutAct.setWhatsThis( |
|
141 self.tr( |
|
142 """<b>About</b>""" |
|
143 """<p>Display some information about this software.</p>""" |
|
144 ) |
|
145 ) |
|
146 self.aboutAct.triggered.connect(self.__about) |
|
147 self.__actions.append(self.aboutAct) |
|
148 |
|
149 self.aboutQtAct = EricAction( |
|
150 self.tr("About Qt"), |
|
151 self.tr("About &Qt"), |
|
152 0, |
|
153 0, |
|
154 self, |
|
155 "pdfviewer_help_about_qt", |
|
156 ) |
|
157 self.aboutQtAct.setStatusTip( |
|
158 self.tr("Display information about the Qt toolkit") |
|
159 ) |
|
160 self.aboutQtAct.setWhatsThis( |
|
161 self.tr( |
|
162 """<b>About Qt</b>""" |
|
163 """<p>Display some information about the Qt toolkit.</p>""" |
|
164 ) |
|
165 ) |
|
166 self.aboutQtAct.triggered.connect(self.__aboutQt) |
|
167 self.__actions.append(self.aboutQtAct) |
|
168 |
|
169 self.whatsThisAct = EricAction( |
|
170 self.tr("What's This?"), |
|
171 EricPixmapCache.getIcon("whatsThis"), |
|
172 self.tr("&What's This?"), |
|
173 QKeySequence(self.tr("Shift+F1", "Help|What's This?'")), |
|
174 0, |
|
175 self, |
|
176 "pdfviewer_help_whats_this", |
|
177 ) |
|
178 self.whatsThisAct.setStatusTip(self.tr("Context sensitive help")) |
|
179 self.whatsThisAct.setWhatsThis( |
|
180 self.tr( |
|
181 """<b>Display context sensitive help</b>""" |
|
182 """<p>In What's This? mode, the mouse cursor shows an arrow""" |
|
183 """ with a question mark, and you can click on the interface""" |
|
184 """ elements to get a short description of what they do and""" |
|
185 """ how to use them. In dialogs, this feature can be accessed""" |
|
186 """ using the context help button in the titlebar.</p>""" |
|
187 ) |
|
188 ) |
|
189 self.whatsThisAct.triggered.connect(self.__whatsThis) |
|
190 self.__actions.append(self.whatsThisAct) |
|
191 |
|
192 @pyqtSlot() |
|
193 def __checkActions(self): |
|
194 """ |
|
195 Private slot to check some actions for their enable/disable status. |
|
196 """ |
|
197 # TODO: not yet implemented |
|
198 |
|
199 def __initMenus(self): |
|
200 """ |
|
201 Private method to create the menus. |
|
202 """ |
|
203 mb = self.menuBar() |
|
204 |
|
205 menu = mb.addMenu(self.tr("&File")) |
|
206 menu.setTearOffEnabled(True) |
|
207 self.__recentMenu = QMenu(self.tr("Open &Recent Files"), menu) |
|
208 |
|
209 # TODO: not yet implemented |
|
210 |
|
211 mb.addSeparator() |
|
212 |
|
213 menu = mb.addMenu(self.tr("&Help")) |
|
214 menu.addAction(self.aboutAct) |
|
215 menu.addAction(self.aboutQtAct) |
|
216 menu.addSeparator() |
|
217 menu.addAction(self.whatsThisAct) |
|
218 |
|
219 def __initToolbars(self): |
|
220 """ |
|
221 Private method to create the toolbars. |
|
222 """ |
|
223 # create a few widgets needed in the toolbars |
|
224 self.__pageSelector = QSpinBox(self) |
|
225 |
|
226 filetb = self.addToolBar(self.tr("File")) |
|
227 filetb.setObjectName("FileToolBar") |
|
228 # TODO: not yet implemented |
|
229 if not self.__fromEric: |
|
230 filetb.addAction(self.exitAct) |
|
231 |
|
232 # TODO: not yet implemented |
|
233 |
|
234 helptb = self.addToolBar(self.tr("Help")) |
|
235 helptb.setObjectName("HelpToolBar") |
|
236 helptb.addAction(self.whatsThisAct) |
|
237 |
|
238 def __createStatusBar(self): |
|
239 """ |
|
240 Private method to initialize the status bar. |
|
241 """ |
|
242 self.__statusBar = self.statusBar() |
|
243 self.__statusBar.setSizeGripEnabled(True) |
|
244 |
|
245 # not yet implemented |
|
246 |
|
247 def closeEvent(self, evt): |
|
248 """ |
|
249 Protected method handling the close event. |
|
250 |
|
251 @param evt reference to the close event |
|
252 @type QCloseEvent |
|
253 """ |
|
254 state = self.saveState() |
|
255 Preferences.setPdfViewer("PdfViewerState", state) |
|
256 |
|
257 Preferences.setGeometry("PdfViewerGeometry", self.saveGeometry()) |
|
258 |
|
259 if not self.__fromEric: |
|
260 Preferences.syncPreferences() |
|
261 |
|
262 self.__saveRecent() |
|
263 |
|
264 evt.accept() |
|
265 self.editorClosed.emit() |
|
266 |
|
267 def __setViewerTitle(self, title): |
|
268 """ |
|
269 Private method to set the viewer title. |
|
270 |
|
271 @param title title to be set |
|
272 @type str |
|
273 """ |
|
274 if title: |
|
275 self.setWindowTitle(self.tr("{0} - PDF Viewer").format(title)) |
|
276 else: |
|
277 self.setWindowTitle(self.tr("PDF Viewer")) |
|
278 |
|
279 def __getErrorString(self, err): |
|
280 """ |
|
281 Private method to get an error string for the given error. |
|
282 |
|
283 @param err error type |
|
284 @type QPdfDocument.Error |
|
285 @return string for the given error type |
|
286 @rtype str |
|
287 """ |
|
288 if err == QPdfDocument.Error.None_: |
|
289 reason = "" |
|
290 elif err == QPdfDocument.Error.DataNotYetAvailable: |
|
291 reason = self.tr("The document is still loading.") |
|
292 elif err == QPdfDocument.Error.FileNotFound: |
|
293 reason = self.tr("The file does not exist.") |
|
294 elif err == QPdfDocument.Error.InvalidFileFormat: |
|
295 reason = self.tr("The file is not a valid PDF file.") |
|
296 elif err == QPdfDocument.Error.IncorrectPassword: |
|
297 reason = self.tr("The password is not correct for this file.") |
|
298 elif err == QPdfDocument.Error.UnsupportedSecurityScheme: |
|
299 reason = self.tr("This kind of PDF file cannot be unlocked.") |
|
300 else: |
|
301 reason = self.tr("Unknown type of error.") |
|
302 |
|
303 return reason |
|
304 |
|
305 def __loadPdfFile(self, fileName): |
|
306 """ |
|
307 Private method to load a PDF file. |
|
308 |
|
309 @param fileName path of the PDF file to load |
|
310 @type str |
|
311 """ |
|
312 # TODO: not yet implemented |
|
313 err = self.__pdfDocument.load(fileName) |
|
314 if err != QPdfDocument.Error.None_: |
|
315 EricMessageBox.critical( |
|
316 self, |
|
317 self.tr("Load PDF File"), |
|
318 self.tr( |
|
319 """<p>The PDF file <b>{0}</b> could not be loaded.</p>""" |
|
320 """<p>Reason: {1}</p>""" |
|
321 ).format(fileName, self.__getErrorString(err)), |
|
322 ) |
|
323 return |
|
324 |
|
325 self.__lastOpenPath = os.path.dirname(fileName) |
|
326 self.__setCurrentFile(fileName) |
|
327 |
|
328 documentTitle = self.__pdfDocument.metaData(QPdfDocument.MetaDataField.Title) |
|
329 self.__setViewerTitle(documentTitle) |
|
330 |
|
331 self.__pageSelected(0) |
|
332 self.__pageSelector.setMaximum(self.__pdfDocument.pageCount() - 1) |
|
333 |
|
334 def __openPdfFile(self): |
|
335 """ |
|
336 Private slot to open a PDF file. |
|
337 """ |
|
338 if ( |
|
339 not self.__lastOpenPath |
|
340 and self.__project is not None |
|
341 and self.__project.isOpen() |
|
342 ): |
|
343 self.__lastOpenPath = self.__project.getProjectPath() |
|
344 |
|
345 fileName = EricFileDialog.getOpenFileName( |
|
346 self, |
|
347 self.tr("Open PDF File"), |
|
348 self.__lastOpenPath, |
|
349 self.tr("PDF Files (*.pdf);;All Files (*)"), |
|
350 ) |
|
351 if fileName: |
|
352 self.__loadPdfFile(fileName) |
|
353 |
|
354 self.__checkActions() |
|
355 |
|
356 def __pageSelected(self, page): |
|
357 """ |
|
358 Private method to navigate to the given page. |
|
359 |
|
360 @param page index of the page to be shown |
|
361 @type int |
|
362 """ |
|
363 nav = self.__view.pageNavigator() |
|
364 nav.jump(page, QPointF(), nav.currentZoom()) |
|
365 |
|
366 def __setCurrentFile(self, fileName): |
|
367 """ |
|
368 Private method to register the file name of the current file. |
|
369 |
|
370 @param fileName name of the file to register |
|
371 @type str |
|
372 """ |
|
373 self.__fileName = fileName |
|
374 # insert filename into list of recently opened files |
|
375 self.__addToRecentList(fileName) |
|
376 |
|
377 def __strippedName(self, fullFileName): |
|
378 """ |
|
379 Private method to return the filename part of the given path. |
|
380 |
|
381 @param fullFileName full pathname of the given file |
|
382 @type str |
|
383 @return filename part |
|
384 @rtype str |
|
385 """ |
|
386 return pathlib.Path(fullFileName).name |
|
387 |
|
388 def __about(self): |
|
389 """ |
|
390 Private slot to show a little About message. |
|
391 """ |
|
392 EricMessageBox.about( |
|
393 self, |
|
394 self.tr("About eric PDF Viewer"), |
|
395 self.tr( |
|
396 "The eric PDF Viewer is a simple component for viewing PDF files." |
|
397 ), |
|
398 ) |
|
399 |
|
400 def __aboutQt(self): |
|
401 """ |
|
402 Private slot to handle the About Qt dialog. |
|
403 """ |
|
404 EricMessageBox.aboutQt(self, "eric PDF Viewer") |
|
405 |
|
406 def __whatsThis(self): |
|
407 """ |
|
408 Private slot called in to enter Whats This mode. |
|
409 """ |
|
410 QWhatsThis.enterWhatsThisMode() |
|
411 |
|
412 def __showPreferences(self): |
|
413 """ |
|
414 Private slot to set the preferences. |
|
415 """ |
|
416 from eric7.Preferences.ConfigurationDialog import ( |
|
417 ConfigurationDialog, |
|
418 ConfigurationMode, |
|
419 ) |
|
420 |
|
421 # TODO: not yet implemented |
|
422 |
|
423 @pyqtSlot() |
|
424 def __showFileMenu(self): |
|
425 """ |
|
426 Private slot to modify the file menu before being shown. |
|
427 """ |
|
428 self.__menuRecentAct.setEnabled(len(self.__recent) > 0) |
|
429 |
|
430 @pyqtSlot() |
|
431 def __showRecentMenu(self): |
|
432 """ |
|
433 Private slot to set up the recent files menu. |
|
434 """ |
|
435 self.__loadRecent() |
|
436 |
|
437 self.__recentMenu.clear() |
|
438 |
|
439 for idx, rs in enumerate(self.__recent, start=1): |
|
440 formatStr = "&{0:d}. {1}" if idx < 10 else "{0:d}. {1}" |
|
441 act = self.__recentMenu.addAction( |
|
442 formatStr.format( |
|
443 idx, |
|
444 FileSystemUtilities.compactPath( |
|
445 rs, PdfViewerWindow.maxMenuFilePathLen |
|
446 ), |
|
447 ) |
|
448 ) |
|
449 act.setData(rs) |
|
450 act.setEnabled(pathlib.Path(rs).exists()) |
|
451 |
|
452 self.__recentMenu.addSeparator() |
|
453 self.__recentMenu.addAction(self.tr("&Clear"), self.__clearRecent) |
|
454 |
|
455 @pyqtSlot(QAction) |
|
456 def __openRecentPdfFile(self, act): |
|
457 """ |
|
458 Private method to open a file from the list of recently opened files. |
|
459 |
|
460 @param act reference to the action that triggered |
|
461 @type QAction |
|
462 """ |
|
463 fileName = act.data() |
|
464 if fileName and self.__maybeSave(): |
|
465 self.__loadPdfFile(fileName) |
|
466 self.__checkActions() |
|
467 |
|
468 @pyqtSlot() |
|
469 def __clearRecent(self): |
|
470 """ |
|
471 Private method to clear the list of recently opened files. |
|
472 """ |
|
473 self.__recent = [] |
|
474 |
|
475 def __loadRecent(self): |
|
476 """ |
|
477 Private method to load the list of recently opened files. |
|
478 """ |
|
479 self.__recent = [] |
|
480 Preferences.Prefs.rsettings.sync() |
|
481 rs = Preferences.Prefs.rsettings.value(recentNamePdfFiles) |
|
482 if rs is not None: |
|
483 for f in Preferences.toList(rs): |
|
484 if pathlib.Path(f).exists(): |
|
485 self.__recent.append(f) |
|
486 |
|
487 def __saveRecent(self): |
|
488 """ |
|
489 Private method to save the list of recently opened files. |
|
490 """ |
|
491 Preferences.Prefs.rsettings.setValue(recentNamePdfFiles, self.__recent) |
|
492 Preferences.Prefs.rsettings.sync() |
|
493 |
|
494 def __addToRecentList(self, fileName): |
|
495 """ |
|
496 Private method to add a file name to the list of recently opened files. |
|
497 |
|
498 @param fileName name of the file to be added |
|
499 """ |
|
500 if fileName: |
|
501 for recent in self.__recent[:]: |
|
502 if FileSystemUtilities.samepath(fileName, recent): |
|
503 self.__recent.remove(recent) |
|
504 self.__recent.insert(0, fileName) |
|
505 maxRecent = Preferences.getPdfViewer("RecentNumber") |
|
506 if len(self.__recent) > maxRecent: |
|
507 self.__recent = self.__recent[:maxRecent] |
|
508 self.__saveRecent() |