Fri, 13 Jan 2023 18:20:54 +0100
Continued implementing a PDF viewer tool (page navigation).
# -*- coding: utf-8 -*- # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the PDF viewer main window. """ import contextlib import os import pathlib from PyQt6.QtCore import Qt, pyqtSignal, QSize, pyqtSlot, QPointF from PyQt6.QtGui import QAction, QKeySequence from PyQt6.QtPdf import QPdfDocument from PyQt6.QtPdfWidgets import QPdfView from PyQt6.QtWidgets import ( QWhatsThis, QMenu, QTabWidget, QSplitter, QSpinBox, QAbstractSpinBox ) from eric7 import Preferences from eric7.EricGui import EricPixmapCache from eric7.EricGui.EricAction import EricAction from eric7.EricWidgets import EricFileDialog, EricMessageBox from eric7.EricWidgets.EricMainWindow import EricMainWindow from eric7.Globals import recentNamePdfFiles from eric7.SystemUtilities import FileSystemUtilities from .PdfPageSelector import PdfPageSelector class PdfViewerWindow(EricMainWindow): """ Class implementing the PDF viewer main window. @signal viewerClosed() emitted after the window was requested to close """ viewerClosed = pyqtSignal() windows = [] maxMenuFilePathLen = 75 def __init__(self, fileName="", parent=None, fromEric=False, project=None): """ Constructor @param fileName name of a file to load on startup @type str @param parent parent widget of this window @type QWidget @param fromEric flag indicating whether it was called from within eric @type bool @param project reference to the project object @type Project """ super().__init__(parent) self.setObjectName("eric7_pdf_viewer") self.__fromEric = fromEric self.setWindowIcon(EricPixmapCache.getIcon("ericPdf")) if not self.__fromEric: self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet")) self.__pdfDocument = QPdfDocument(self) self.__cw = QSplitter(Qt.Orientation.Horizontal, self) self.__info = QTabWidget(self) self.__cw.addWidget(self.__info) self.__view = QPdfView(self) self.__view.setDocument(self.__pdfDocument) self.__view.setPageMode(QPdfView.PageMode.MultiPage) self.__cw.addWidget(self.__view) self.setCentralWidget(self.__cw) # create a few widgets needed in the toolbars self.__pageSelector = PdfPageSelector() self.__pageSelector.setDocument(self.__pdfDocument) self.__view.pageNavigator().currentPageChanged.connect( self.__pageSelector.setValue ) self.__pageSelector.valueChanged.connect(self.__pageSelected) self.__pageSelector.gotoPage.connect(self.__gotoPage) g = Preferences.getGeometry("PdfViewerGeometry") if g.isEmpty(): s = QSize(1000, 1000) self.resize(s) self.__cw.setSizes([300, 700]) else: self.restoreGeometry(g) self.__initActions() self.__initMenus() self.__initToolbars() self.__createStatusBar() self.__view.pageNavigator().backAvailableChanged.connect( self.backwardAct.setEnabled ) self.__view.pageNavigator().forwardAvailableChanged.connect( self.forwardAct.setEnabled ) PdfViewerWindow.windows.append(self) state = Preferences.getPdfViewer("PdfViewerState") self.restoreState(state) splitterState = Preferences.getPdfViewer("PdfViewerSplitterState") self.__cw.restoreState(splitterState) self.__checkActions() self.__project = project self.__lastOpenPath = "" self.__recent = [] self.__loadRecent() self.__setCurrentFile("") self.__setViewerTitle("") if fileName: self.__loadPdfFile(fileName) def __initActions(self): """ Private method to define the user interface actions. """ # list of all actions self.__actions = [] self.__initFileActions() self.__initGotoActions() self.__initViewActions() self.__initHelpActions() def __initFileActions(self): """ Private method to define the file related user interface actions. """ self.newWindowAct = EricAction( self.tr("New Window"), EricPixmapCache.getIcon("newWindow"), self.tr("New &Window"), QKeySequence(self.tr("Ctrl+Shift+N", "File|New Window")), 0, self, "pdfviewer_file_new_window", ) self.newWindowAct.setStatusTip( self.tr("Open a PDF file in a new PDF Viewer window") ) self.newWindowAct.setWhatsThis( self.tr( """<b>New Window</b>""" """<p>This opens a PDF file in a new PDF Viewer window. It pops up""" """ a file selection dialog.</p>""" ) ) self.newWindowAct.triggered.connect(self.__openPdfFileNewWindow) self.__actions.append(self.newWindowAct) self.openAct = EricAction( self.tr("Open"), EricPixmapCache.getIcon("open"), self.tr("&Open..."), QKeySequence(self.tr("Ctrl+O", "File|Open")), 0, self, "pdfviewer_file_open", ) self.openAct.setStatusTip(self.tr("Open a PDF file for viewing")) self.openAct.setWhatsThis( self.tr( """<b>Open</b>""" """<p>This opens a PDF file for viewing. It pops up a file""" """ selection dialog.</p>""" ) ) self.openAct.triggered.connect(self.__openPdfFile) self.__actions.append(self.openAct) self.reloadAct = EricAction( self.tr("Reload"), EricPixmapCache.getIcon("reload"), self.tr("&Reload"), QKeySequence("F5"), 0, self, "pdfviewer_file_reload", ) self.reloadAct.setStatusTip(self.tr("Reload the current PDF document")) self.reloadAct.triggered.connect(self.__reload) self.__actions.append(self.reloadAct) # TODO: maybe this will be a tab of the side widget self.propertiesAct = EricAction( self.tr("Properties"), EricPixmapCache.getIcon("documentProperties"), self.tr("&Properties..."), QKeySequence(self.tr("Alt+Return")), 0, self, "pdfviewer_file_properties", ) self.propertiesAct.setStatusTip(self.tr("Show the document properties")) self.propertiesAct.setWhatsThis( self.tr( """<b>Properties</b><p>Opens a dialog showing the document""" """ properties.</p>""" ) ) self.propertiesAct.triggered.connect(self.__showDocumentProperties) self.__actions.append(self.propertiesAct) self.closeAct = EricAction( self.tr("Close"), EricPixmapCache.getIcon("close"), self.tr("&Close"), QKeySequence(self.tr("Ctrl+W", "File|Close")), 0, self, "pdfviewer_file_close", ) self.closeAct.setStatusTip(self.tr("Close the current PDF Viewer window")) self.closeAct.triggered.connect(self.close) self.__actions.append(self.closeAct) self.closeAllAct = EricAction( self.tr("Close All"), self.tr("Close &All"), 0, 0, self, "pdfviewer_file_close_all", ) self.closeAllAct.setStatusTip(self.tr("Close all PDF Viewer windows")) self.closeAllAct.triggered.connect(self.__closeAll) self.__actions.append(self.closeAllAct) self.closeOthersAct = EricAction( self.tr("Close Others"), self.tr("Close Others"), 0, 0, self, "pdfviewer_file_close_others", ) self.closeOthersAct.setStatusTip(self.tr("Close all other PDF Viewer windows")) self.closeOthersAct.triggered.connect(self.__closeOthers) self.__actions.append(self.closeOthersAct) self.exitAct = EricAction( self.tr("Quit"), EricPixmapCache.getIcon("exit"), self.tr("&Quit"), QKeySequence(self.tr("Ctrl+Q", "File|Quit")), 0, self, "pdfviewer_file_quit", ) self.exitAct.setStatusTip(self.tr("Quit the PDF Viewer")) if not self.__fromEric: self.exitAct.triggered.connect(self.__closeAll) self.__actions.append(self.exitAct) def __initGotoActions(self): """ Private method to define the navigation related user interface actions. """ # TODO: Goto page (goto dialog) (Ctrl+G) self.previousPageAct = EricAction( self.tr("Previous Page"), EricPixmapCache.getIcon("1leftarrow"), self.tr("&Previous Page"), 0, 0, self, "pdfviewer_goto_previous", ) self.previousPageAct.setStatusTip(self.tr("Go to the previous page")) self.previousPageAct.triggered.connect(self.__previousPage) self.__actions.append(self.previousPageAct) self.nextPageAct = EricAction( self.tr("Next Page"), EricPixmapCache.getIcon("1rightarrow"), self.tr("&Next Page"), 0, 0, self, "pdfviewer_goto_next", ) self.nextPageAct.setStatusTip(self.tr("Go to the next page")) self.nextPageAct.triggered.connect(self.__nextPage) self.__actions.append(self.nextPageAct) self.startDocumentAct = EricAction( self.tr("Start of Document"), EricPixmapCache.getIcon("gotoFirst"), self.tr("&Start of Document"), QKeySequence(self.tr("Ctrl+Home", "Goto|Start")), 0, self, "pdfviewer_goto_start", ) self.startDocumentAct.setStatusTip( self.tr("Go to the first page of the document") ) self.startDocumentAct.triggered.connect(self.__startDocument) self.__actions.append(self.startDocumentAct) self.endDocumentAct = EricAction( self.tr("End of Document"), EricPixmapCache.getIcon("gotoLast"), self.tr("&End of Document"), QKeySequence(self.tr("Ctrl+End", "Goto|End")), 0, self, "pdfviewer_goto_end", ) self.endDocumentAct.setStatusTip( self.tr("Go to the last page of the document") ) self.endDocumentAct.triggered.connect(self.__endDocument) self.__actions.append(self.endDocumentAct) self.backwardAct = EricAction( self.tr("Back"), EricPixmapCache.getIcon("back"), self.tr("&Back"), QKeySequence(self.tr("Alt+Shift+Left", "Goto|Back")), 0, self, "pdfviewer_goto_back", ) self.backwardAct.setStatusTip( self.tr("Go back in the view history") ) self.backwardAct.triggered.connect(self.__backInHistory) self.__actions.append(self.backwardAct) self.forwardAct = EricAction( self.tr("Forward"), EricPixmapCache.getIcon("forward"), self.tr("&Forward"), QKeySequence(self.tr("Alt+Shift+Right", "Goto|Forward")), 0, self, "pdfviewer_goto_forward", ) self.forwardAct.setStatusTip( self.tr("Go forward in the view history") ) self.forwardAct.triggered.connect(self.__forwardInHistory) self.__actions.append(self.forwardAct) def __initViewActions(self): """ Private method to define the view related user interface actions. """ # Zoom in (Ctrl++) # Zoom out (Ctrl+-) # Zoom reset (Ctrl+0) # Page Width (checkable, exclusive) # Whole Page (checkable, exclusive) def __initHelpActions(self): """ Private method to create the Help actions. """ self.aboutAct = EricAction( self.tr("About"), self.tr("&About"), 0, 0, self, "pdfviewer_help_about" ) self.aboutAct.setStatusTip(self.tr("Display information about this software")) self.aboutAct.setWhatsThis( self.tr( """<b>About</b>""" """<p>Display some information about this software.</p>""" ) ) self.aboutAct.triggered.connect(self.__about) self.__actions.append(self.aboutAct) self.aboutQtAct = EricAction( self.tr("About Qt"), self.tr("About &Qt"), 0, 0, self, "pdfviewer_help_about_qt", ) self.aboutQtAct.setStatusTip( self.tr("Display information about the Qt toolkit") ) self.aboutQtAct.setWhatsThis( self.tr( """<b>About Qt</b>""" """<p>Display some information about the Qt toolkit.</p>""" ) ) self.aboutQtAct.triggered.connect(self.__aboutQt) self.__actions.append(self.aboutQtAct) self.whatsThisAct = EricAction( self.tr("What's This?"), EricPixmapCache.getIcon("whatsThis"), self.tr("&What's This?"), QKeySequence(self.tr("Shift+F1", "Help|What's This?'")), 0, self, "pdfviewer_help_whats_this", ) self.whatsThisAct.setStatusTip(self.tr("Context sensitive help")) self.whatsThisAct.setWhatsThis( self.tr( """<b>Display context sensitive help</b>""" """<p>In What's This? mode, the mouse cursor shows an arrow""" """ with a question mark, and you can click on the interface""" """ elements to get a short description of what they do and""" """ how to use them. In dialogs, this feature can be accessed""" """ using the context help button in the titlebar.</p>""" ) ) self.whatsThisAct.triggered.connect(self.__whatsThis) self.__actions.append(self.whatsThisAct) @pyqtSlot() def __checkActions(self): """ Private slot to check some actions for their enable/disable status. """ self.reloadAct.setEnabled( self.__pdfDocument.status() == QPdfDocument.Status.Ready ) curPage = self.__view.pageNavigator().currentPage() self.previousPageAct.setEnabled(curPage > 0) self.nextPageAct.setEnabled(curPage < self.__pdfDocument.pageCount() - 1) self.startDocumentAct.setEnabled(curPage != 0) self.endDocumentAct.setEnabled(curPage != self.__pdfDocument.pageCount() - 1) self.backwardAct.setEnabled(self.__view.pageNavigator().backAvailable()) self.forwardAct.setEnabled(self.__view.pageNavigator().forwardAvailable()) # TODO: not yet implemented ##def setRecentPath(self, openPath): ##""" ##Public method to set the last open path. ## ##@param openPath least recently used open path ##@type str ##""" ##if openPath: ##self.__lastOpenPath = openPath def __initMenus(self): """ Private method to create the menus. """ mb = self.menuBar() menu = mb.addMenu(self.tr("&File")) menu.setTearOffEnabled(True) self.__recentMenu = QMenu(self.tr("Open &Recent Files"), menu) menu.addAction(self.newWindowAct) menu.addAction(self.openAct) self.__menuRecentAct = menu.addMenu(self.__recentMenu) menu.addSeparator() menu.addAction(self.reloadAct) menu.addSeparator() menu.addAction(self.propertiesAct) menu.addSeparator() menu.addAction(self.closeAct) menu.addAction(self.closeOthersAct) if self.__fromEric: menu.addAction(self.closeAllAct) else: menu.addSeparator() menu.addAction(self.exitAct) menu.aboutToShow.connect(self.__showFileMenu) self.__recentMenu.aboutToShow.connect(self.__showRecentMenu) self.__recentMenu.triggered.connect(self.__openRecentPdfFile) menu = mb.addMenu(self.tr("&Go To")) menu.setTearOffEnabled(True) menu.addAction(self.previousPageAct) menu.addAction(self.nextPageAct) menu.addSeparator() menu.addAction(self.startDocumentAct) menu.addAction(self.endDocumentAct) menu.addSeparator() menu.addAction(self.backwardAct) menu.addAction(self.forwardAct) menu.addSeparator() # TODO: not yet implemented mb.addSeparator() menu = mb.addMenu(self.tr("&Help")) menu.addAction(self.aboutAct) menu.addAction(self.aboutQtAct) menu.addSeparator() menu.addAction(self.whatsThisAct) def __initToolbars(self): """ Private method to create the toolbars. """ filetb = self.addToolBar(self.tr("File")) filetb.setObjectName("FileToolBar") filetb.addAction(self.newWindowAct) filetb.addAction(self.openAct) filetb.addSeparator() filetb.addAction(self.closeAct) if not self.__fromEric: filetb.addAction(self.exitAct) gototb = self.addToolBar(self.tr("Goto")) gototb.setObjectName("GotoToolBar") gototb.addAction(self.startDocumentAct) gototb.addWidget(self.__pageSelector) gototb.addAction(self.endDocumentAct) viewtb = self.addToolBar(self.tr("View")) viewtb.setObjectName("ViewToolBar") # TODO: not yet implemented helptb = self.addToolBar(self.tr("Help")) helptb.setObjectName("HelpToolBar") helptb.addAction(self.whatsThisAct) def __createStatusBar(self): """ Private method to initialize the status bar. """ self.__statusBar = self.statusBar() self.__statusBar.setSizeGripEnabled(True) # not yet implemented def closeEvent(self, evt): """ Protected method handling the close event. @param evt reference to the close event @type QCloseEvent """ state = self.saveState() Preferences.setPdfViewer("PdfViewerState", state) splitterState = self.__cw.saveState() Preferences.setPdfViewer("PdfViewerSplitterState", splitterState) Preferences.setGeometry("PdfViewerGeometry", self.saveGeometry()) with contextlib.suppress(ValueError): if self.__fromEric or len(PdfViewerWindow.windows) > 1: PdfViewerWindow.windows.remove(self) if not self.__fromEric: Preferences.syncPreferences() self.__saveRecent() evt.accept() self.viewerClosed.emit() def __setViewerTitle(self, title): """ Private method to set the viewer title. @param title title to be set @type str """ if title: self.setWindowTitle(self.tr("{0} - PDF Viewer").format(title)) else: self.setWindowTitle(self.tr("PDF Viewer")) def __getErrorString(self, err): """ Private method to get an error string for the given error. @param err error type @type QPdfDocument.Error @return string for the given error type @rtype str """ if err == QPdfDocument.Error.None_: reason = "" elif err == QPdfDocument.Error.DataNotYetAvailable: reason = self.tr("The document is still loading.") elif err == QPdfDocument.Error.FileNotFound: reason = self.tr("The file does not exist.") elif err == QPdfDocument.Error.InvalidFileFormat: reason = self.tr("The file is not a valid PDF file.") elif err == QPdfDocument.Error.IncorrectPassword: reason = self.tr("The password is not correct for this file.") elif err == QPdfDocument.Error.UnsupportedSecurityScheme: reason = self.tr("This kind of PDF file cannot be unlocked.") else: reason = self.tr("Unknown type of error.") return reason def __loadPdfFile(self, fileName): """ Private method to load a PDF file. @param fileName path of the PDF file to load @type str """ err = self.__pdfDocument.load(fileName) if err != QPdfDocument.Error.None_: EricMessageBox.critical( self, self.tr("Load PDF File"), self.tr( """<p>The PDF file <b>{0}</b> could not be loaded.</p>""" """<p>Reason: {1}</p>""" ).format(fileName, self.__getErrorString(err)), ) return self.__lastOpenPath = os.path.dirname(fileName) self.__setCurrentFile(fileName) documentTitle = self.__pdfDocument.metaData(QPdfDocument.MetaDataField.Title) self.__setViewerTitle(documentTitle) self.__pageSelected(0) self.__pageSelector.setMaximum(self.__pdfDocument.pageCount() - 1) self.__pageSelector.setValue(0) @pyqtSlot() def __reload(self): """ Private slot to reload the current PDF document. """ self.__loadPdfFile(self.__fileName) @pyqtSlot() def __openPdfFile(self): """ Private slot to open a PDF file. """ if ( not self.__lastOpenPath and self.__project is not None and self.__project.isOpen() ): self.__lastOpenPath = self.__project.getProjectPath() fileName = EricFileDialog.getOpenFileName( self, self.tr("Open PDF File"), self.__lastOpenPath, self.tr("PDF Files (*.pdf);;All Files (*)"), ) if fileName: self.__loadPdfFile(fileName) @pyqtSlot() def __openPdfFileNewWindow(self): """ Private slot called to open a PDF file in new viewer window. """ if ( not self.__lastOpenPath and self.__project is not None and self.__project.isOpen() ): self.__lastOpenPath = self.__project.getProjectPath() fileName = EricFileDialog.getOpenFileName( self, self.tr("Open PDF File"), self.__lastOpenPath, self.tr("PDF Files (*.pdf);;All Files (*)"), ) if fileName: viewer = PdfViewerWindow( fileName=fileName, parent=self.parent(), fromEric=self.__fromEric, project=self.__project, ) viewer.show() @pyqtSlot() def __closeAll(self): """ Private slot to close all windows. """ self.__closeOthers() self.close() @pyqtSlot() def __closeOthers(self): """ Private slot to close all other windows. """ for win in PdfViewerWindow.windows[:]: if win != self: win.close() @pyqtSlot(int) def __pageSelected(self, page): """ Private slot to navigate to the given page. @param page index of the page to be shown @type int """ nav = self.__view.pageNavigator() nav.jump(page, QPointF(), nav.currentZoom()) self.__checkActions() def __setCurrentFile(self, fileName): """ Private method to register the file name of the current file. @param fileName name of the file to register @type str """ self.__fileName = fileName # insert filename into list of recently opened files self.__addToRecentList(fileName) @pyqtSlot() def __about(self): """ Private slot to show a little About message. """ EricMessageBox.about( self, self.tr("About eric PDF Viewer"), self.tr( "The eric PDF Viewer is a simple component for viewing PDF files." ), ) @pyqtSlot() def __aboutQt(self): """ Private slot to handle the About Qt dialog. """ EricMessageBox.aboutQt(self, "eric PDF Viewer") @pyqtSlot() def __whatsThis(self): """ Private slot called in to enter Whats This mode. """ QWhatsThis.enterWhatsThisMode() @pyqtSlot() def __showPreferences(self): """ Private slot to set the preferences. """ from eric7.Preferences.ConfigurationDialog import ( ConfigurationDialog, ConfigurationMode, ) # TODO: not yet implemented @pyqtSlot() def __showFileMenu(self): """ Private slot to modify the file menu before being shown. """ self.__menuRecentAct.setEnabled(len(self.__recent) > 0) @pyqtSlot() def __showRecentMenu(self): """ Private slot to set up the recent files menu. """ self.__loadRecent() self.__recentMenu.clear() for idx, rs in enumerate(self.__recent, start=1): formatStr = "&{0:d}. {1}" if idx < 10 else "{0:d}. {1}" act = self.__recentMenu.addAction( formatStr.format( idx, FileSystemUtilities.compactPath( rs, PdfViewerWindow.maxMenuFilePathLen ), ) ) act.setData(rs) act.setEnabled(pathlib.Path(rs).exists()) self.__recentMenu.addSeparator() self.__recentMenu.addAction(self.tr("&Clear"), self.__clearRecent) @pyqtSlot(QAction) def __openRecentPdfFile(self, act): """ Private method to open a file from the list of recently opened files. @param act reference to the action that triggered @type QAction """ fileName = act.data() self.__loadPdfFile(fileName) @pyqtSlot() def __clearRecent(self): """ Private method to clear the list of recently opened files. """ self.__recent = [] def __loadRecent(self): """ Private method to load the list of recently opened files. """ self.__recent = [] Preferences.Prefs.rsettings.sync() rs = Preferences.Prefs.rsettings.value(recentNamePdfFiles) if rs is not None: for f in Preferences.toList(rs): if pathlib.Path(f).exists(): self.__recent.append(f) def __saveRecent(self): """ Private method to save the list of recently opened files. """ Preferences.Prefs.rsettings.setValue(recentNamePdfFiles, self.__recent) Preferences.Prefs.rsettings.sync() def __addToRecentList(self, fileName): """ Private method to add a file name to the list of recently opened files. @param fileName name of the file to be added """ if fileName: for recent in self.__recent[:]: if FileSystemUtilities.samepath(fileName, recent): self.__recent.remove(recent) self.__recent.insert(0, fileName) maxRecent = Preferences.getPdfViewer("RecentNumber") if len(self.__recent) > maxRecent: self.__recent = self.__recent[:maxRecent] self.__saveRecent() @pyqtSlot() def __showDocumentProperties(self): """ Private slot to open a dialog showing the document properties. """ # TODO: not yet implemented @pyqtSlot() def __gotoPage(self): """ Private slot to show a dialog to select a page to jump to. """ # TODO: not yet implemented @pyqtSlot() def __previousPage(self): """ Private slot to go to the previous page. """ curPage = self.__view.pageNavigator().currentPage() if curPage > 0: self.__pageSelected(curPage - 1) @pyqtSlot() def __nextPage(self): """ Private slot to go to the next page. """ curPage = self.__view.pageNavigator().currentPage() if curPage < self.__pdfDocument.pageCount() - 1: self.__pageSelected(curPage + 1) @pyqtSlot() def __startDocument(self): """ Private slot to go to the first page of the document. """ self.__pageSelected(0) @pyqtSlot() def __endDocument(self): """ Private slot to go to the last page of the document. """ self.__pageSelected(self.__pdfDocument.pageCount() - 1) @pyqtSlot() def __backInHistory(self): """ Private slot to go back in the view history. """ self.__view.pageNavigator().back() @pyqtSlot() def __forwardInHistory(self): """ Private slot to go forward in the view history. """ self.__view.pageNavigator().forward()