src/eric7/PdfViewer/PdfViewerWindow.py

Thu, 12 Jan 2023 18:08:12 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 12 Jan 2023 18:08:12 +0100
branch
pdf_viewer
changeset 9697
cdaa3cc805f7
child 9698
69e183e4db6f
permissions
-rw-r--r--

Started implementing a PDF viewer tool.

# -*- 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()

eric ide

mercurial