--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/PdfViewer/PdfViewerWindow.py Thu Jan 12 18:08:12 2023 +0100 @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the PDF viewer main window. +""" + +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 +) + +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 + + +class PdfViewerWindow(EricMainWindow): + """ + Class implementing the PDF viewer main window. + + @signal editorClosed() emitted after the window was requested to close down + """ + + editorClosed = pyqtSignal() + + 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) + + # TODO: insert central widget here + 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.__cw.addWidget(self.__view) + self.setCentralWidget(self.__cw) + + 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() + + state = Preferences.getPdfViewer("PdfViewerState") + self.restoreState(state) + + self.__project = project + self.__lastOpenPath = "" + + self.__recent = [] + self.__loadRecent() + + self.__setCurrentFile("") + self.__setViewerTitle("") + if fileName: + self.__loadPdfFile(fileName) + + self.__checkActions() + + def __initActions(self): + """ + Private method to define the user interface actions. + """ + # list of all actions + self.__actions = [] + + self.__initFileActions() + self.__initHelpActions() + + def __initFileActions(self): + """ + Private method to define the file related user interface actions. + """ + # TODO: not yet implemented + 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")) + self.exitAct.setWhatsThis(self.tr("""<b>Quit</b><p>Quit the PDF Viewer.</p>""")) + self.__actions.append(self.exitAct) + + 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. + """ + # TODO: not yet implemented + + 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) + + # 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. + """ + # create a few widgets needed in the toolbars + self.__pageSelector = QSpinBox(self) + + filetb = self.addToolBar(self.tr("File")) + filetb.setObjectName("FileToolBar") + # TODO: not yet implemented + if not self.__fromEric: + filetb.addAction(self.exitAct) + + # 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) + + Preferences.setGeometry("PdfViewerGeometry", self.saveGeometry()) + + if not self.__fromEric: + Preferences.syncPreferences() + + self.__saveRecent() + + evt.accept() + self.editorClosed.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 + """ + # TODO: not yet implemented + 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) + + 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) + + self.__checkActions() + + def __pageSelected(self, page): + """ + Private method 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()) + + 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) + + def __strippedName(self, fullFileName): + """ + Private method to return the filename part of the given path. + + @param fullFileName full pathname of the given file + @type str + @return filename part + @rtype str + """ + return pathlib.Path(fullFileName).name + + 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." + ), + ) + + def __aboutQt(self): + """ + Private slot to handle the About Qt dialog. + """ + EricMessageBox.aboutQt(self, "eric PDF Viewer") + + def __whatsThis(self): + """ + Private slot called in to enter Whats This mode. + """ + QWhatsThis.enterWhatsThisMode() + + 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() + if fileName and self.__maybeSave(): + self.__loadPdfFile(fileName) + self.__checkActions() + + @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()