Wed, 18 Jan 2023 14:31:55 +0100
PDF Viewer
- created a specialized PDF view class to intercept and handle certain events
- added a 'Search' widget
--- a/eric7.epj Mon Jan 16 11:56:23 2023 +0100 +++ b/eric7.epj Wed Jan 18 14:31:55 2023 +0100 @@ -1318,7 +1318,9 @@ "src/eric7/PdfViewer/PdfGoToDialog.py", "src/eric7/PdfViewer/PdfInfoWidget.py", "src/eric7/PdfViewer/PdfPageSelector.py", + "src/eric7/PdfViewer/PdfSearchWidget.py", "src/eric7/PdfViewer/PdfToCWidget.py", + "src/eric7/PdfViewer/PdfView.py", "src/eric7/PdfViewer/PdfViewerWindow.py", "src/eric7/PdfViewer/PdfZoomSelector.py", "src/eric7/PdfViewer/__init__.py",
--- a/src/eric7/PdfViewer/PdfInfoWidget.py Mon Jan 16 11:56:23 2023 +0100 +++ b/src/eric7/PdfViewer/PdfInfoWidget.py Wed Jan 18 14:31:55 2023 +0100 @@ -72,8 +72,6 @@ ) self.__layout.addRow(self.tr("Keywords:"), self.__infoLabels["keywords"]) self.__layout.addRow(self.tr("Security:"), self.__infoLabels["security"]) - #self.__layout.addRow(self.tr(""), self.__infoLabels[""]) - # TODO: add more info labels self.setLayout(self.__layout)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/PdfViewer/PdfSearchWidget.py Wed Jan 18 14:31:55 2023 +0100 @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a Search widget. +""" + +from PyQt6.QtCore import Qt, pyqtSlot, QModelIndex, pyqtSignal +from PyQt6.QtPdf import QPdfSearchModel, QPdfDocument, QPdfLink +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QLabel, QLineEdit, QHBoxLayout, QToolButton, + QAbstractItemView, QTreeWidget, QTreeWidgetItem +) + +from eric7 import Preferences +from eric7.EricGui import EricPixmapCache + + +class PdfSearchResultsWidget(QTreeWidget): + """ + Class implementing a widget to show the search results. + + @signal rowCountChanged() emitted to indicate a change of the number + of items + """ + + rowCountChanged = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.setColumnCount(2) + self.setHeaderHidden(True) + self.setAlternatingRowColors(True) + self.setSortingEnabled(False) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + + self.__searchModel = QPdfSearchModel(self) + self.__searchModel.modelReset.connect(self.__clear) + self.__searchModel.rowsInserted.connect(self.__rowsInserted) + + def setSearchString(self, searchString): + """ + Public method to set the search string. + + @param searchString search string + @type str + """ + self.__searchModel.setSearchString(searchString) + + def searchString(self): + """ + Public method to get the current search string. + + @return search string + @rtype str + """ + return self.__searchModel.searchString() + + def setDocument(self, document): + """ + Public method to set the PDF document object to be searched. + + @param document reference to the PDF document object + @type QPdfDocument + """ + self.__searchModel.setDocument(document) + + def document(self): + """ + Public method to get the reference to the PDF document object. + + @return reference to the PDF document object + @rtype QPdfDocument + """ + return self.__searchModel.document() + + @pyqtSlot() + def __clear(self): + """ + Private slot to clear the list of search results. + """ + self.clear() + self.rowCountChanged.emit() + + @pyqtSlot(QModelIndex, int, int) + def __rowsInserted(self, parent, first, last): + """ + Private slot to handle the insertion of rows of the search model. + + @param parent reference to the parent index + @type QModelIndex + @param first first row inserted + @type int + @param last last row inserted + @type int + """ + contextLength = Preferences.getPdfViewer("PdfSearchContextLength") + + for row in range(first, last + 1): + index = self.__searchModel.index(row, 0) + itm = QTreeWidgetItem( + self, + [ + self.tr("Page {0}").format( + self.__searchModel.document().pageLabel( + self.__searchModel.data( + index, QPdfSearchModel.Role.Page.value + ) + ) + ), + "", + ] + ) + contextBefore = self.__searchModel.data( + index, QPdfSearchModel.Role.ContextBefore.value + ) + if len(contextBefore) > contextLength: + contextBefore = "... {0}".format(contextBefore[-contextLength:]) + contextAfter = self.__searchModel.data( + index, QPdfSearchModel.Role.ContextAfter.value + ) + if len(contextAfter) > contextLength: + contextAfter = "{0} ...".format(contextAfter[:contextLength]) + resultLabel = QLabel( + self.tr( + "{0}<b>{1}</b>{2}", + "context before, search string, context after" + ).format(contextBefore, self.searchString(), contextAfter) + ) + self.setItemWidget(itm, 1, resultLabel) + + for column in range(self.columnCount()): + self.resizeColumnToContents(column) + + self.rowCountChanged.emit() + + def rowCount(self): + """ + Public method to get the number of rows. + + @return number of rows + @rtype int + """ + return self.topLevelItemCount() + + def currentRow(self): + """ + Public method to get the current row. + + @return current row + @rtype int + """ + curItem = self.currentItem() + if curItem is None: + return -1 + else: + return self.indexOfTopLevelItem(curItem) + + def setCurrentRow(self, row): + """ + Public method to set the current row. + + @param row row number to make the current row + @type int + """ + if 0 <= row < self.topLevelItemCount(): + self.setCurrentItem(self.topLevelItem(row)) + + def searchResultData(self, item, role): + """ + Public method to get data of a search result item. + + @param item reference to the search result item + @type QTreeWidgetItem + @param role item data role + @type QPdfSearchModel.Role or Qt.ItemDataRole + @return requested data + @rtype Any + """ + row = self.indexOfTopLevelItem(item) + index = self.__searchModel.index(row, 0) + return self.__searchModel.data(index, role) + + def getPdfLink(self, item): + """ + Public method to get the PDF link associated with a search result item. + + @param item reference to the search result item + @type QTreeWidgetItem + @return associated PDF link + @rtype QPdfLink + """ + row = self.indexOfTopLevelItem(item) + return self.__searchModel.resultAtIndex(row) + + +class PdfSearchWidget(QWidget): + """ + Class implementing a Search widget. + """ + + searchResultActivated = pyqtSignal(QPdfLink) + + def __init__(self, document, parent=None): + """ + Constructor + + @param document reference to the PDF document object + @type QPdfDocument + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.__layout = QVBoxLayout(self) + + # Line 1: a header label + self.__header = QLabel("<h2>{0}</h2>".format(self.tr("Search"))) + self.__header.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.__layout.addWidget(self.__header) + + # Line 2: search entry and navigation buttons + self.__searchLineLayout = QHBoxLayout() + + self.__searchEdit = QLineEdit(self) + self.__searchEdit.setPlaceholderText(self.tr("Search ...")) + self.__searchEdit.setClearButtonEnabled(True) + self.__searchLineLayout.addWidget(self.__searchEdit) + + # layout for the navigation buttons + self.__buttonsLayout = QHBoxLayout() + self.__buttonsLayout.setSpacing(0) + + self.__findPrevButton = QToolButton(self) + self.__findPrevButton.setToolTip( + self.tr("Press to move to the previous occurrence") + ) + self.__findPrevButton.setIcon(EricPixmapCache.getIcon("1leftarrow")) + self.__buttonsLayout.addWidget(self.__findPrevButton) + + self.__findNextButton = QToolButton(self) + self.__findNextButton.setToolTip(self.tr("Press to move to the next occurrence")) + self.__findNextButton.setIcon(EricPixmapCache.getIcon("1rightarrow")) + self.__buttonsLayout.addWidget(self.__findNextButton) + + self.__searchLineLayout.addLayout(self.__buttonsLayout) + self.__layout.addLayout(self.__searchLineLayout) + + self.__resultsWidget = PdfSearchResultsWidget(self) + self.__resultsWidget.setDocument(document) + self.__layout.addWidget(self.__resultsWidget) + + self.setLayout(self.__layout) + + self.__searchEdit.setEnabled(False) + self.__resultsWidget.setEnabled(False) + self.__findPrevButton.setEnabled(False) + self.__findNextButton.setEnabled(False) + + self.__resultsWidget.itemActivated.connect(self.__entrySelected) + document.statusChanged.connect(self.__handleDocumentStatus) + self.__searchEdit.returnPressed.connect(self.__search) + self.__searchEdit.textChanged.connect(self.__searchTextChanged) + self.__resultsWidget.rowCountChanged.connect(self.__updateButtons) + self.__resultsWidget.currentItemChanged.connect( + self.__updateButtons + ) + self.__findNextButton.clicked.connect(self.__nextResult) + self.__findPrevButton.clicked.connect(self.__previousResult) + + @pyqtSlot(QPdfDocument.Status) + def __handleDocumentStatus(self, status): + """ + Private slot to handle a change of the document status. + + @param status document status + @type QPdfDocument.Status + """ + ready = status == QPdfDocument.Status.Ready + + self.__searchEdit.setEnabled(ready) + self.__resultsWidget.setEnabled(ready) + + if not ready: + self.__searchEdit.clear() + + @pyqtSlot(str) + def __searchTextChanged(self, text): + """ + Private slot to handle a change of the search string. + + @param text search string + @type str + """ + if not text: + self.__resultsWidget.setSearchString("") + + @pyqtSlot() + def __search(self): + """ + Private slot to initiate a new search. + """ + searchString = self.__searchEdit.text() + self.__resultsWidget.setSearchString(searchString) + + @pyqtSlot() + def __updateButtons(self): + """ + Private slot to update the state of the navigation buttons. + """ + hasSearchResults = bool(self.__resultsWidget.rowCount()) + currentRow = self.__resultsWidget.currentRow() + self.__findPrevButton.setEnabled(hasSearchResults and currentRow > 0) + self.__findNextButton.setEnabled( + hasSearchResults and currentRow < self.__resultsWidget.rowCount() - 2 + ) + + @pyqtSlot(QTreeWidgetItem) + def __entrySelected(self, item): + """ + Private slot to handle the selection of a search result entry. + + @param index index of the activated entry + @type QModelIndex + """ + link = self.__resultsWidget.getPdfLink(item) + self.searchResultActivated.emit(link) + + @pyqtSlot() + def __nextResult(self): + """ + Private slot to activate the next result. + """ + row = self.__resultsWidget.currentRow() + if row < self.__resultsWidget.rowCount() - 2: + nextItem = self.__resultsWidget.topLevelItem(row + 1) + self.__resultsWidget.setCurrentItem(nextItem) + self.__entrySelected(nextItem) + + @pyqtSlot() + def __previousResult(self): + """ + Private slot to activate the previous result. + """ + row = self.__resultsWidget.currentRow() + if row > 0: + prevItem = self.__resultsWidget.topLevelItem(row - 1) + self.__resultsWidget.setCurrentItem(prevItem) + self.__entrySelected(prevItem)
--- a/src/eric7/PdfViewer/PdfToCWidget.py Mon Jan 16 11:56:23 2023 +0100 +++ b/src/eric7/PdfViewer/PdfToCWidget.py Wed Jan 18 14:31:55 2023 +0100 @@ -99,12 +99,18 @@ self.__tocModel.setDocument(document) self.__tocFilterModel = QSortFilterProxyModel(self) self.__tocFilterModel.setRecursiveFilteringEnabled(True) + self.__tocFilterModel.setFilterCaseSensitivity( + Qt.CaseSensitivity.CaseInsensitive + ) self.__tocFilterModel.setSourceModel(self.__tocModel) self.__tocWidget.setModel(self.__tocFilterModel) self.__layout.addWidget(self.__tocWidget) self.setLayout(self.__layout) + self.__searchEdit.setEnabled(False) + self.__tocWidget.setEnabled(False) + self.__tocWidget.activated.connect(self.__topicSelected) document.statusChanged.connect(self.__handleDocumentStatus) self.__searchEdit.textEdited.connect(self.__searchTextChanged) @@ -133,11 +139,15 @@ @param status document status @type QPdfDocument.Status """ - if status == QPdfDocument.Status.Ready: + ready = status == QPdfDocument.Status.Ready + if ready: self.__tocWidget.expandAll() for column in range(self.__tocModel.columnCount(QModelIndex())): self.__tocWidget.resizeColumnToContents(column) + self.__searchEdit.setEnabled(ready) + self.__tocWidget.setEnabled(ready) + @pyqtSlot(str) def __searchTextChanged(self, text): """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/PdfViewer/PdfView.py Wed Jan 18 14:31:55 2023 +0100 @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a specialized PDF view class. +""" + +from PyQt6.QtCore import QSize, Qt, pyqtSlot, QEvent +from PyQt6.QtGui import QGuiApplication +from PyQt6.QtPdfWidgets import QPdfView + +from .PdfZoomSelector import PdfZoomSelector + + +class PdfView(QPdfView): + """ + Class implementing a specialized PDF view. + """ + + def __init__(self, parent): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super().__init__(parent) + + self.__screenResolution = ( + QGuiApplication.primaryScreen().logicalDotsPerInch() / 72.0 + ) + + self.grabGesture(Qt.GestureType.PinchGesture) + + def __zoomInOut(self, zoomIn): + """ + Private method to zoom into or out of the view. + + @param zoomIn flag indicating to zoom into the view + @type bool + """ + zoomFactor = self.__zoomFactorForMode(self.zoomMode()) + + factors = list(PdfZoomSelector.ZoomValues) + factors.append(self.__zoomFactorForMode(QPdfView.ZoomMode.FitInView)) + factors.append(self.__zoomFactorForMode(QPdfView.ZoomMode.FitToWidth)) + if zoomIn: + factors.sort() + if zoomFactor >= factors[-1]: + return + newIndex = next(x for x, val in enumerate(factors) if val > zoomFactor) + else: + factors.sort(reverse=True) + if zoomFactor <= factors[-1]: + return + newIndex = next(x for x, val in enumerate(factors) if val < zoomFactor) + newFactor = factors[newIndex] + if newFactor == self.__zoomFactorForMode(QPdfView.ZoomMode.FitInView): + self.setZoomMode(QPdfView.ZoomMode.FitInView) + self.zoomModeChanged.emit(QPdfView.ZoomMode.FitInView) + elif newFactor == self.__zoomFactorForMode(QPdfView.ZoomMode.FitToWidth): + self.setZoomMode(QPdfView.ZoomMode.FitToWidth) + self.zoomModeChanged.emit(QPdfView.ZoomMode.FitToWidth) + else: + self.setZoomFactor(newFactor) + self.zoomFactorChanged.emit(newFactor) + self.setZoomMode(QPdfView.ZoomMode.Custom) + self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom) + + def __zoomFactorForMode(self, zoomMode): + """ + Private method to calculate the zoom factor iaw. the current zoom mode. + + @param zoomMode zoom mode to get the zoom factor for + @type QPdfView.ZoomMode + @return zoom factor + @rtype float + """ + if zoomMode == QPdfView.ZoomMode.Custom: + return self.zoomFactor() + else: + viewport = self.viewport() + curPage = self.pageNavigator().currentPage() + margins = self.documentMargins() + if zoomMode == QPdfView.ZoomMode.FitToWidth: + pageSize = ( + self.document().pagePointSize(curPage) * self.__screenResolution + ).toSize() + factor = ( + viewport.width() - margins.left() - margins.right() + ) / pageSize.width() + pageSize *= factor + else: + # QPdfView.ZoomMode.FitInView + viewportSize = viewport.size() + QSize( + -margins.left() - margins.right(), -self.pageSpacing() + ) + pageSize = ( + self.document().pagePointSize(curPage) * self.__screenResolution + ).toSize() + pageSize = pageSize.scaled( + viewportSize, Qt.AspectRatioMode.KeepAspectRatio + ) + zoomFactor = pageSize.width() / ( + self.document().pagePointSize(curPage) * self.__screenResolution + ).width() + return zoomFactor + + @pyqtSlot() + def zoomIn(self): + """ + Public slot to zoom into the view. + """ + self.__zoomInOut(True) + + @pyqtSlot() + def zoomOut(self): + """ + Public slot to zoom out of the view. + """ + self.__zoomInOut(False) + + @pyqtSlot() + def zoomReset(self): + """ + Public slot to reset the zoom factor of the view. + """ + if self.zoomMode() != QPdfView.ZoomMode.Custom or self.zoomFactor() != 1.0: + self.setZoomFactor(1.0) + self.zoomFactorChanged.emit(1.0) + self.setZoomMode(QPdfView.ZoomMode.Custom) + self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom) + + def wheelEvent(self, evt): + """ + Protected method to handle wheel events. + + @param evt reference to the wheel event + @type QWheelEvent + """ + delta = evt.angleDelta().y() + if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: + if delta < 0: + self.zoomOut() + elif delta > 0: + self.zoomIn() + evt.accept() + return + + elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: + if delta < 0: + self.pageNavigator().back() + elif delta > 0: + self.pageNavigator().forward() + evt.accept() + return + + super().wheelEvent(evt) + + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event + @type QEvent + @return flag indicating, if the event was handled + @rtype bool + """ + if evt.type() == QEvent.Type.Gesture: + self.gestureEvent(evt) + return True + + return super().event(evt) + + def gestureEvent(self, evt): + """ + Protected method handling gesture events. + + @param evt reference to the gesture event + @type QGestureEvent + """ + pinch = evt.gesture(Qt.GestureType.PinchGesture) + if pinch: + if pinch.state() == Qt.GestureState.GestureStarted: + pinch.setTotalScaleFactor(self.__zoomFactorForMode(self.zoomMode())) + elif pinch.state() == Qt.GestureState.GestureUpdated: + if self.zoomMode() != QPdfView.ZoomMode.Custom: + self.setZoomMode(QPdfView.ZoomMode.Custom) + self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom) + zoomFactor = pinch.totalScaleFactor() + self.setZoomFactor(zoomFactor) + self.zoomFactorChanged.emit(zoomFactor) + evt.accept()
--- a/src/eric7/PdfViewer/PdfViewerWindow.py Mon Jan 16 11:56:23 2023 +0100 +++ b/src/eric7/PdfViewer/PdfViewerWindow.py Wed Jan 18 14:31:55 2023 +0100 @@ -12,8 +12,10 @@ import pathlib from PyQt6.QtCore import Qt, pyqtSignal, QSize, pyqtSlot, QPointF -from PyQt6.QtGui import QAction, QKeySequence, QGuiApplication, QActionGroup -from PyQt6.QtPdf import QPdfDocument +from PyQt6.QtGui import ( + QAction, QKeySequence, QActionGroup, QGuiApplication, QClipboard +) +from PyQt6.QtPdf import QPdfDocument, QPdfLink from PyQt6.QtPdfWidgets import QPdfView from PyQt6.QtWidgets import ( QWhatsThis, QMenu, QTabWidget, QSplitter, QToolBar, QDialog @@ -32,6 +34,8 @@ from .PdfZoomSelector import PdfZoomSelector from .PdfToCWidget import PdfToCWidget from .PdfInfoWidget import PdfInfoWidget +from .PdfSearchWidget import PdfSearchWidget +from .PdfView import PdfView class PdfViewerWindow(EricMainWindow): @@ -67,19 +71,16 @@ self.__fromEric = fromEric self.setWindowIcon(EricPixmapCache.getIcon("ericPdf")) - self.__screenResolution = ( - QGuiApplication.primaryScreen().logicalDotsPerInch() / 72.0 - ) - if not self.__fromEric: self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet")) self.__pdfDocument = QPdfDocument(self) self.__cw = QSplitter(Qt.Orientation.Horizontal, self) + self.__cw.setChildrenCollapsible(False) self.__info = QTabWidget(self) self.__cw.addWidget(self.__info) - self.__view = QPdfView(self) + self.__view = PdfView(self) self.__view.setDocument(self.__pdfDocument) self.__cw.addWidget(self.__view) self.setCentralWidget(self.__cw) @@ -90,7 +91,13 @@ self.__documentInfoWidget, EricPixmapCache.getIcon("documentProperties"), "" ) self.__info.setTabToolTip(index, self.tr("Document Properties")) - + + self.__searchWidget = PdfSearchWidget(self.__pdfDocument, self) + index = self.__info.addTab( + self.__searchWidget, EricPixmapCache.getIcon("find"), "" + ) + self.__info.setTabToolTip(index, self.tr("Search")) + self.__tocWidget = PdfToCWidget(self.__pdfDocument, self) index = self.__info.addTab( self.__tocWidget, EricPixmapCache.getIcon("listSelection"), "" @@ -140,6 +147,7 @@ self.__view.zoomModeChanged.connect(self.__zoomSelector.setZoomMode) self.__tocWidget.topicActivated.connect(self.__tocActivated) + self.__searchWidget.searchResultActivated.connect(self.__handleSearchResult) PdfViewerWindow.windows.append(self) @@ -168,6 +176,8 @@ self.__initFileActions() self.__initGotoActions() self.__initViewActions() + self.__initEditActions() + self.__initSettingsActions() self.__initHelpActions() def __initFileActions(self): @@ -229,7 +239,6 @@ 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"), @@ -409,6 +418,7 @@ """ Private method to define the view related user interface actions. """ + # TODO: add Fullscreen action self.zoomInAct = EricAction( self.tr("Zoom in"), EricPixmapCache.getIcon("zoomIn"), @@ -471,20 +481,61 @@ self.zoomWholePageAct.setCheckable(True) self.__actions.append(self.zoomWholePageAct) - ##self.__displayModeActGrp = QActionGroup(self) -## - ##self.displayModeSingleAct = EricAction( - ##self.tr("Whole Page"), - ##EricPixmapCache.getIcon("zoomFitPage"), - ##self.tr("Whole &Page"), - ##0, - ##0, - ##self, - ##"pdfviewer_view_zoomwholePage", - ##) - ##self.displayModeSingleAct.triggered.connect(self.__zoomWholePage) - ##self.displayModeSingleAct.setCheckable(True) - ##self.__actions.append(self.displayModeSingleAct) + def __initEditActions(self): + """ + Private method to create the Edit actions. + """ + self.copyAllAct = EricAction( + self.tr("Copy All Text"), + EricPixmapCache.getIcon("editCopy"), + self.tr("&Copy All Text"), + QKeySequence(self.tr("Ctrl+C", "Edit|Copy All Text")), + 0, + self, + "pdfviewer_edit_copyall", + ) + self.copyAllAct.triggered.connect(self.__copyAllText) + self.__actions.append(self.copyAllAct) + + self.copyAllPageAct = EricAction( + self.tr("Copy All Page Text"), + self.tr("Copy &All Page Text"), + QKeySequence(self.tr("Shift+Ctrl+C", "Edit|Copy All Page Text")), + 0, + self, + "pdfviewer_edit_copyallpage", + ) + self.copyAllPageAct.triggered.connect(self.__copyAllTextOfPage) + self.__actions.append(self.copyAllPageAct) + + def __initSettingsActions(self): + """ + Private method to create the Settings actions. + """ + self.sidebarAct = EricAction( + self.tr("Show Sidebar"), + EricPixmapCache.getIcon("sidebarExpandLeft"), + self.tr("Show &Sidebar"), + 0, + 0, + self, + "pdfviewer_settings_sidebar", + ) + self.sidebarAct.triggered.connect(self.__toggleSideBar) + self.sidebarAct.setCheckable(True) + self.__actions.append(self.sidebarAct) + + self.openRecentNewAct = EricAction( + self.tr("Open Recent File in New Window"), + self.tr("Open Recent File in New Window"), + 0, + 0, + self, + "pdfviewer_settings_openrecent new", + ) + self.openRecentNewAct.triggered.connect(self.__toggleOpenRecentNew) + self.openRecentNewAct.setCheckable(True) + self.__actions.append(self.sidebarAct) def __initHelpActions(self): """ @@ -577,18 +628,6 @@ self.zoomWholePageAct.setEnabled(ready) self.__zoomSelector.setEnabled(ready) - # 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. @@ -635,6 +674,12 @@ self.__displayModeActGrp.addAction(self.__continuousPageAct) modeMenu.triggered.connect(self.__displayModeSelected) + menu = mb.addMenu(self.tr("&Edit")) + menu.setTearOffEnabled(True) + menu.addAction(self.copyAllAct) + menu.addSeparator() + menu.addAction(self.copyAllPageAct) + menu = mb.addMenu(self.tr("&Go To")) menu.setTearOffEnabled(True) menu.addAction(self.previousPageAct) @@ -648,6 +693,12 @@ menu.addSeparator() menu.addAction(self.gotoAct) + menu = mb.addMenu(self.tr("Se&ttings")) + menu.setTearOffEnabled(True) + menu.addAction(self.sidebarAct) + menu.addSeparator() + menu.addAction(self.openRecentNewAct) + mb.addSeparator() menu = mb.addMenu(self.tr("&Help")) @@ -665,6 +716,10 @@ mainToolBar.setMovable(False) mainToolBar.setFloatable(False) + # 0. Sidebar action + mainToolBar.addAction(self.sidebarAct) + mainToolBar.addSeparator() + # 1. File actions mainToolBar.addAction(self.newWindowAct) mainToolBar.addAction(self.openAct) @@ -683,7 +738,6 @@ mainToolBar.addSeparator() # 3. View actions - # TODO: not yet implemented mainToolBar.addAction(self.zoomOutAct) mainToolBar.addWidget(self.__zoomSelector) mainToolBar.addAction(self.zoomInAct) @@ -727,11 +781,16 @@ """ Private method to save the PDF Viewer state data. """ - # TODO: save current zoom factor and mode + page mode state = self.saveState() Preferences.setPdfViewer("PdfViewerState", state) splitterState = self.__cw.saveState() Preferences.setPdfViewer("PdfViewerSplitterState", splitterState) + Preferences.setPdfViewer("PdfViewerSidebarVisible", self.sidebarAct.isChecked()) + Preferences.setPdfViewer("PdfViewerZoomFactor", self.__view.zoomFactor()) + Preferences.setPdfViewer("PdfViewerZoomMode", self.__view.zoomMode()) + Preferences.setPdfViewer( + "PdfViewerOpenRecentInNewWindow", self.openRecentNewAct.isChecked() + ) if not self.__fromEric: Preferences.syncPreferences() @@ -740,11 +799,16 @@ """ Private method to restore the PDF Viewer state data. """ - # TODO: restore zoom factor and mode + page mode state = Preferences.getPdfViewer("PdfViewerState") self.restoreState(state) splitterState = Preferences.getPdfViewer("PdfViewerSplitterState") self.__cw.restoreState(splitterState) + self.__toggleSideBar(Preferences.getPdfViewer("PdfViewerSidebarVisible")) + self.__view.setZoomFactor(Preferences.getPdfViewer("PdfViewerZoomFactor")) + self.__view.setZoomMode(Preferences.getPdfViewer("PdfViewerZoomMode")) + self.openRecentNewAct.setChecked( + Preferences.getPdfViewer("PdfViewerOpenRecentInNewWindow") + ) def __setViewerTitle(self, title): """ @@ -849,9 +913,12 @@ self.__loadPdfFile(fileName) @pyqtSlot() - def __openPdfFileNewWindow(self): + def __openPdfFileNewWindow(self, fileName=""): """ Private slot called to open a PDF file in new viewer window. + + @param fileName name of the file to open (defaults to "") + @type str (optional) """ if ( not self.__lastOpenPath @@ -860,12 +927,13 @@ ): self.__lastOpenPath = self.__project.getProjectPath() - fileName = EricFileDialog.getOpenFileName( - self, - self.tr("Open PDF File"), - self.__lastOpenPath, - self.tr("PDF Files (*.pdf);;All Files (*)"), - ) + if not fileName: + fileName = EricFileDialog.getOpenFileName( + self, + self.tr("Open PDF File"), + self.__lastOpenPath, + self.tr("PDF Files (*.pdf);;All Files (*)"), + ) if fileName: viewer = PdfViewerWindow( fileName=fileName, @@ -918,6 +986,16 @@ nav = self.__view.pageNavigator() nav.jump(page, QPointF(), zoomFactor) + @pyqtSlot(QPdfLink) + def __handleSearchResult(self, link): + """ + Private slot to handle the selection of a search result. + + @param link PDF link to navigate to + @type QPdfLink + """ + self.__view.pageNavigator().jump(link) + def __setCurrentFile(self, fileName): """ Private method to register the file name of the current file. @@ -1008,9 +1086,11 @@ @param act reference to the action that triggered @type QAction """ - # TODO: add config option to open recent files in new viewer or the same one fileName = act.data() - self.__loadPdfFile(fileName) + if Preferences.getPdfViewer("PdfViewerOpenRecentInNewWindow"): + self.__openPdfFileNewWindow(fileName) + else: + self.__loadPdfFile(fileName) @pyqtSlot() def __clearRecent(self): @@ -1059,14 +1139,14 @@ """ Private slot to open a dialog showing the document properties. """ - # TODO: not yet implemented + self.__toggleSideBar(True) + self.__info.setCurrentWidget(self.__documentInfoWidget) @pyqtSlot() def __gotoPage(self): """ Private slot to show a dialog to select a page to jump to. """ - # TODO: not yet implemented from .PdfGoToDialog import PdfGoToDialog dlg = PdfGoToDialog( @@ -1124,60 +1204,26 @@ """ self.__view.pageNavigator().forward() - def __zoomInOut(self, zoomIn): - """ - Private method to zoom into or out of the view. - - @param zoomIn flag indicating to zoom into the view - @type bool - """ - zoomFactor = self.__zoomFactorForMode(self.__view.zoomMode()) - - factors = list(PdfZoomSelector.ZoomValues) - factors.append(self.__zoomFactorForMode(QPdfView.ZoomMode.FitInView)) - factors.append(self.__zoomFactorForMode(QPdfView.ZoomMode.FitToWidth)) - if zoomIn: - factors.sort() - if zoomFactor >= factors[-1]: - return - newIndex = next(x for x, val in enumerate(factors) if val > zoomFactor) - else: - factors.sort(reverse=True) - if zoomFactor <= factors[-1]: - return - newIndex = next(x for x, val in enumerate(factors) if val < zoomFactor) - newFactor = factors[newIndex] - if newFactor == self.__zoomFactorForMode(QPdfView.ZoomMode.FitInView): - self.__zoomWholePage(True) - elif newFactor == self.__zoomFactorForMode(QPdfView.ZoomMode.FitToWidth): - self.__zoomPageWidth(True) - else: - self.__view.setZoomFactor(newFactor) - self.__zoomSelector.setZoomFactor(newFactor) - self.__zoomModeChanged(QPdfView.ZoomMode.Custom) - @pyqtSlot() def __zoomIn(self): """ Private slot to zoom into the view. """ - self.__zoomInOut(True) + self.__view.zoomIn() @pyqtSlot() def __zoomOut(self): """ Private slot to zoom out of the view. """ - self.__zoomInOut(False) + self.__view.zoomOut() @pyqtSlot() def __zoomReset(self): """ Private slot to reset the zoom factor of the view. """ - self.__view.setZoomFactor(1.0) - self.__zoomSelector.setZoomFactor(1.0) - self.__zoomModeChanged(QPdfView.ZoomMode.Custom) + self.__view.zoomReset() @pyqtSlot(bool) def __zoomPageWidth(self, checked): @@ -1214,52 +1260,6 @@ self.zoomWholePageAct.setChecked(zoomMode == QPdfView.ZoomMode.FitInView) self.zoomPageWidthAct.setChecked(zoomMode == QPdfView.ZoomMode.FitToWidth) - def __zoomFactorForMode(self, zoomMode): - """ - Private method to calculate the zoom factor iaw. the current zoom mode. - - @param zoomMode zoom mode to get the zoom factor for - @type QPdfView.ZoomMode - @return zoom factor - @rtype float - """ - if zoomMode == QPdfView.ZoomMode.Custom: - return self.__view.zoomFactor() - else: - viewport = self.__view.viewport() - curPage = self.__view.pageNavigator().currentPage() - margins = self.__view.documentMargins() - if zoomMode == QPdfView.ZoomMode.FitToWidth: - pageSize = ( - self.__pdfDocument.pagePointSize(curPage) * self.__screenResolution - ).toSize() - factor = ( - viewport.width() - margins.left() - margins.right() - ) / pageSize.width() - pageSize *= factor - else: - # QPdfView.ZoomMode.FitInView - viewportSize = viewport.size() + QSize( - -margins.left() - margins.right(), -self.__view.pageSpacing() - ) - pageSize = ( - self.__pdfDocument.pagePointSize(curPage) * self.__screenResolution - ).toSize() - pageSize = pageSize.scaled( - viewportSize, Qt.AspectRatioMode.KeepAspectRatio - ) - zoomFactor = pageSize.width() / ( - self.__pdfDocument.pagePointSize(curPage) * self.__screenResolution - ).width() - return zoomFactor - - @pyqtSlot() - def __setFocusToView(self): - """ - Private slot to set the focus to the PDF document view. - """ - self.__view.setFocus(Qt.FocusReason.OtherFocusReason) - @pyqtSlot(QAction) def __displayModeSelected(self, act): """ @@ -1285,3 +1285,47 @@ self.__view.setPageMode(QPdfView.PageMode.MultiPage) self.__continuousPageAct.setChecked(True) return + + @pyqtSlot(bool) + def __toggleSideBar(self, visible): + """ + Private slot to togle the sidebar (info) widget. + + @param visible desired state of the sidebar + @type bool + """ + self.sidebarAct.setChecked(visible) + self.__info.setVisible(visible) + Preferences.setPdfViewer("PdfViewerSidebarVisible", visible) + + @pyqtSlot(bool) + def __toggleOpenRecentNew(self, on): + """ + Private slot to toggle the 'Open Recent File in New Window' action. + + @param on desired state of the action + @type bool + """ + Preferences.setPdfViewer("PdfViewerOpenRecentInNewWindow", on) + + @pyqtSlot() + def __copyAllTextOfPage(self): + """ + Private slot to copy all text of the current page to the system clipboard. + """ + selection = self.__pdfDocument.getAllText( + self.__view.pageNavigator().currentPage() + ) + selection.copyToClipboard() + + @pyqtSlot() + def __copyAllText(self): + """ + Private slot to copy all text of the document to the system clipboard. + """ + textPages = [] + for page in range(self.__pdfDocument.pageCount()): + textPages.append(self.__pdfDocument.getAllText(page).text()) + QGuiApplication.clipboard().setText( + "\r\n".join(textPages), QClipboard.Mode.Clipboard + )
--- a/src/eric7/Preferences/__init__.py Mon Jan 16 11:56:23 2023 +0100 +++ b/src/eric7/Preferences/__init__.py Wed Jan 18 14:31:55 2023 +0100 @@ -39,6 +39,7 @@ QUrl, ) from PyQt6.QtGui import QColor, QFont, QPalette +from PyQt6.QtPdfWidgets import QPdfView from PyQt6.QtWidgets import QApplication try: @@ -1635,7 +1636,12 @@ "PdfViewerState": QByteArray(), "PdfViewerSplitterState": QByteArray(), "RecentNumber": 9, - "PdfViewerDisplayMode": "single" # single or continuous + "PdfViewerDisplayMode": "single", # single or continuous + "PdfViewerSidebarVisible": True, + "PdfViewerZoomMode": QPdfView.ZoomMode.Custom.value, + "PdfViewerZoomFactor": 1.0, + "PdfViewerOpenRecentInNewWindow": False, + "PdfSearchContextLength": 30, } @@ -3854,10 +3860,24 @@ @return the requested user setting @rtype Any """ - if key in ("RecentNumber"): + if key in ("RecentNumber", "PdfSearchContextLength"): return int( Prefs.settings.value("PdfViewer/" + key, Prefs.pdfViewerDefaults[key]) ) + elif key in ("PdfViewerSidebarVisible", "PdfViewerOpenRecentInNewWindow"): + return toBool( + Prefs.settings.value("PdfViewer/" + key, Prefs.pdfViewerDefaults[key]) + ) + elif key in ("PdfViewerZoomFactor",): + return float( + Prefs.settings.value("PdfViewer/" + key, Prefs.pdfViewerDefaults[key]) + ) + elif key == "PdfViewerZoomMode": + return QPdfView.ZoomMode( + int( + Prefs.settings.value("PdfViewer/" + key, Prefs.pdfViewerDefaults[key]) + ) + ) else: return Prefs.settings.value("PdfViewer/" + key, Prefs.pdfViewerDefaults[key]) @@ -3871,6 +3891,8 @@ @param value the value to be set @type Any """ + if key == "PdfViewerZoomMode": + value = value.value Prefs.settings.setValue("PdfViewer/" + key, value)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/sidebarExpandLeft.svg Wed Jan 18 14:31:55 2023 +0100 @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg9" + sodipodi:docname="sidebarExpandLeft.svg" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs13" /> + <sodipodi:namedview + id="namedview11" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="29.272727" + inkscape:cx="11" + inkscape:cy="11" + inkscape:window-width="2580" + inkscape:window-height="1080" + inkscape:window-x="426" + inkscape:window-y="146" + inkscape:window-maximized="0" + inkscape:current-layer="svg9" /> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + <g + class="ColorScheme-Text" + fill="currentColor" + id="g7" + transform="matrix(1.25,0,0,1.25,-2.75,-2.75)"> + <path + d="M 3,3 V 19 H 19 V 3 Z M 8,4 H 18 V 18 H 8 Z" + stroke-linecap="square" + stroke-linejoin="round" + id="path3" /> + <path + d="M 11.353516,6.6464844 15.707031,11 11.353516,15.353516 10.646484,14.646484 14.292969,11 10.646484,7.3535156 Z" + id="path5" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/sidebarExpandLeft.svg Wed Jan 18 14:31:55 2023 +0100 @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg9" + sodipodi:docname="sidebarExpandLeft.svg" + inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs13" /> + <sodipodi:namedview + id="namedview11" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + showgrid="false" + inkscape:zoom="29.272727" + inkscape:cx="11" + inkscape:cy="11" + inkscape:window-width="2580" + inkscape:window-height="1080" + inkscape:window-x="426" + inkscape:window-y="146" + inkscape:window-maximized="0" + inkscape:current-layer="svg9" /> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + <g + class="ColorScheme-Text" + fill="currentColor" + id="g7" + transform="matrix(1.25,0,0,1.25,-2.75,-2.75)"> + <path + d="M 3,3 V 19 H 19 V 3 Z M 8,4 H 18 V 18 H 8 Z" + stroke-linecap="square" + stroke-linejoin="round" + id="path3" /> + <path + d="M 11.353516,6.6464844 15.707031,11 11.353516,15.353516 10.646484,14.646484 14.292969,11 10.646484,7.3535156 Z" + id="path5" /> + </g> +</svg>