文档查看器示例¶
一个用于显示和打印JSON、文本和PDF文件的小部件应用程序。
文档查看器演示了如何使用带有静态和动态工具栏、菜单和操作的QMainWindow。

AbstractViewer
提供了一个通用的API来查看、保存和打印文档。可以查询文档和查看器的属性:
文档是否有内容?
它被修改了吗?
是否支持概览(缩略图或书签)?
AbstractViewer
提供了受保护的方法,供派生类在主窗口上创建操作和菜单。为了在主窗口上显示这些资源,它们被设置为父窗口。AbstractViewer
负责移除和销毁它创建的UI资源。它继承自 QObject
以实现信号和槽。
当查看器接收到主窗口上所有必要的UI资产信息后,会发出uiInitialized()
信号。
当文档打印功能被启用或禁用时,会发出printingEnabledChanged()
信号。这通常发生在新文档成功加载后,或者例如所有内容被移除时。
printStatusChanged
信号在启动打印过程后通知其进度的变化。
documentLoaded()
信号通知应用程序文档已成功加载。
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtCore import QObject
from PySide6.QtWidgets import (QDialog, QMenu)
from PySide6.QtCore import Signal, Slot
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
MENU_NAME = "qtFileMenu"
class AbstractViewer(QObject):
uiInitialized = Signal()
printingEnabledChanged = Signal(bool)
showMessage = Signal(str, int)
documentLoaded = Signal(str)
def __init__(self):
super().__init__()
self._file = None
self._widget = None
self._menus = []
self._toolBars = []
self._printingEnabled = False
self._actions = []
self._fileMenu = None
def __del__(self):
self.cleanup()
def viewerName(self):
return ""
def saveState(self):
return False
def restoreState(self, state):
return False
def supportedMimeTypes():
return []
def init(self, file, widget, mainWindow):
self._file = file
self._widget = widget
self._uiAssets_mainWindow = mainWindow
def isEmpty(self):
return not self.hasContent()
def isPrintingEnabled(self):
return self._printingEnabled
def hasContent(self):
return False
def supportsOverview(self):
return False
def isModified(self):
return False
def saveDocument(self):
return False
def saveDocumentAs(self):
return False
def actions(self):
return self._actions
def widget(self):
return self._widget
def menus(self):
return self._menus
def mainWindow(self):
return self._uiAssets_mainWindow
def statusBar(self):
return self.mainWindow().statusBar()
def menuBar(self):
return self.mainWindow().menuBar()
def maybeEnablePrinting(self):
self.maybeSetPrintingEnabled(True)
def disablePrinting(self):
self.maybeSetPrintingEnabled(False)
def isDefaultViewer(self):
return False
def viewer(self):
return self
def statusMessage(self, message, type="", timeout=8000):
msg = self.viewerName()
if type:
msg += "/" + type
msg += ": " + message
self.showMessage.emit(msg, timeout)
def addToolBar(self, title):
bar = self.mainWindow().addToolBar(title)
name = title.replace(' ', '')
bar.setObjectName(name)
self._toolBars.append(bar)
return bar
def addMenu(self, title):
menu = QMenu(title, self.menuBar())
menu.setObjectName(title)
self.menuBar().insertMenu(self._uiAssets_help, menu)
self._menus.append(menu)
return menu
def cleanup(self):
# delete all objects created by the viewer which need to be displayed
# and therefore parented on MainWindow
if self._file:
self._file = None
self._menus.clear()
self._toolBars.clear()
def fileMenu(self):
if self._fileMenu:
return self._fileMenu
menus = self.mainWindow().findChildren(QMenu)
for menu in menus:
if menu.objectName() == MENU_NAME:
self._fileMenu = menu
return self._fileMenu
self._fileMenu = self.addMenu("File")
self._fileMenu.setObjectName(MENU_NAME)
return self._fileMenu
@Slot()
def print_(self):
type = "Printing"
if not self.hasContent():
self.statusMessage("No content to print.", type)
return
printer = QPrinter(QPrinter.HighResolution)
dlg = QPrintDialog(printer, self.mainWindow())
dlg.setWindowTitle("Print Document")
if dlg.exec() == QDialog.Accepted:
self.printDocument(printer)
else:
self.statusMessage("Printing canceled!", type)
return
state = printer.printerState()
message = self.viewerName() + " :"
if state == QPrinter.PrinterState.Aborted:
message += "Printing aborted."
elif state == QPrinter.PrinterState.Active:
message += "Printing active."
elif state == QPrinter.PrinterState.Idle:
message += "Printing completed."
elif state == QPrinter.PrinterState.Error:
message += "Printing error."
self.statusMessage(message, type)
def maybeSetPrintingEnabled(self, enabled):
if enabled == self._printingEnabled:
return
self._printingEnabled = enabled
self.printingEnabledChanged.emit(enabled)
def initViewer(self, back, forward, help, tabs):
self._uiAssets_back = back
self._uiAssets_forward = forward
self._uiAssets_help = help
self._uiAssets_tabs = tabs
# Tabs can be populated individually by the viewer, if it
# supports overview
tabs.clear()
tabs.setVisible(self.supportsOverview())
self.uiInitialized.emit()
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="demos/documentviewer">
<file>images/copy@2x.png</file>
<file>images/copy.png</file>
<file>images/cut@2x.png</file>
<file>images/cut.png</file>
<file>images/go-next-view@2x.png</file>
<file>images/go-next-view-page@2x.png</file>
<file>images/go-next-view-page.png</file>
<file>images/go-next-view.png</file>
<file>images/go-previous-view@2x.png</file>
<file>images/go-previous-view-page@2x.png</file>
<file>images/go-previous-view-page.png</file>
<file>images/go-previous-view.png</file>
<file>images/magnifier@2x.png</file>
<file>images/magnifier.png</file>
<file>images/open@2x.png</file>
<file>images/open.png</file>
<file>images/paste@2x.png</file>
<file>images/paste.png</file>
<file>images/print2x.png</file>
<file>images/print.png</file>
<file>images/qt-logo@2x.png</file>
<file>images/qt-logo.png</file>
<file>images/zoom-fit-best@2x.png</file>
<file>images/zoom-fit-best.png</file>
<file>images/zoom-fit-width@2x.png</file>
<file>images/zoom-fit-width.png</file>
<file>images/zoom-in@2x.png</file>
<file>images/zoom-in.png</file>
<file>images/zoom-original@2x.png</file>
<file>images/zoom-original.png</file>
<file>images/zoom-out@2x.png</file>
<file>images/zoom-out.png</file>
<file>images/zoom-previous@2x.png</file>
<file>images/zoom-previous.png</file>
<file>images/document-open-recent.svgz</file>
<file>images/go-next.svgz</file>
<file>images/go-previous.svgz</file>
<file>images/help-about.svgz</file>
</qresource>
</RCC>
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
"""PySide6 port of the Qt Document Viewer demo from Qt v6.x"""
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QCoreApplication
from mainwindow import MainWindow
DESCRIPTION = "A viewer for JSON, PDF and text files"
if __name__ == "__main__":
app = QApplication([])
QCoreApplication.setOrganizationName("QtExamples")
QCoreApplication.setApplicationName("DocumentViewer")
QCoreApplication.setApplicationVersion("1.0")
arg_parser = ArgumentParser(description=DESCRIPTION,
formatter_class=RawTextHelpFormatter)
arg_parser.add_argument("file", type=str, nargs="?",
help="JSON, PDF or text file to open")
args = arg_parser.parse_args()
fileName = args.file
w = MainWindow()
w.show()
if args.file and not w.openFile(args.file):
sys.exit(-1)
sys.exit(app.exec())
MainWindow
类提供了一个带有菜单、操作和工具栏的应用程序屏幕。它可以打开一个文件,自动检测其内容类型。它还维护了一个之前打开的文件列表,使用 QSettings
来存储并在启动时重新加载设置。MainWindow
根据打开文件的内容类型创建一个合适的查看器,并提供打印文档的支持。
MainWindow的
构造函数初始化了在Qt Designer中创建的用户界面。mainwindow.ui
文件在左侧提供了一个QTabWidget
,显示书签和缩略图。在右侧,有一个QScrollArea
用于查看文件内容。
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtWidgets import (QDialog, QFileDialog, QMainWindow, QMessageBox)
from PySide6.QtCore import (QDir, QFile, QFileInfo, QSettings, Slot)
from ui_mainwindow import Ui_MainWindow
from viewerfactory import ViewerFactory
from recentfiles import RecentFiles
from recentfilemenu import RecentFileMenu
settingsDir = "WorkingDir"
settingsMainWindow = "MainWindow"
settingsViewers = "Viewers"
settingsFiles = "RecentFiles"
ABOUT_TEXT = """A Widgets application to display and print JSON,
text and PDF files. Demonstrates various features to use
in widget applications: Using QSettings, query and save
user preferences, manage file histories and control cursor
behavior when hovering over widgets.
"""
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MainWindow()
self._currentDir = QDir()
self._viewer = None
self._recentFiles = RecentFiles()
self.ui.setupUi(self)
self.ui.actionOpen.triggered.connect(self.onActionOpenTriggered)
self.ui.actionAbout.triggered.connect(self.onActionAboutTriggered)
self.ui.actionAboutQt.triggered.connect(self.onActionAboutQtTriggered)
self._recentFiles = RecentFiles(self.ui.actionRecent)
self._recentFiles.countChanged.connect(self._recentFilesCountChanged)
self.readSettings()
self._factory = ViewerFactory(self.ui.viewArea, self)
viewers = ", ".join(self._factory.viewerNames())
self.statusBar().showMessage(f'Available viewers: {viewers}')
menu = RecentFileMenu(self, self._recentFiles)
self.ui.actionRecent.setMenu(menu)
menu.fileOpened.connect(self.openFile)
button = self.ui.mainToolBar.widgetForAction(self.ui.actionRecent)
if button:
self.ui.actionRecent.triggered.connect(button.showMenu)
@Slot(int)
def _recentFilesCountChanged(self, count):
self.ui.actionRecent.setText(f"{count} recent files")
def closeEvent(self, event):
self.saveSettings()
@Slot(int)
def onActionOpenTriggered(self):
fileDialog = QFileDialog(self, "Open Document",
self._currentDir.absolutePath())
while (fileDialog.exec() == QDialog.Accepted
and not self.openFile(fileDialog.selectedFiles()[0])):
pass
@Slot(str)
def openFile(self, fileName):
file = QFile(fileName)
if not file.exists():
nf = QDir.toNativeSeparators(fileName)
self.statusBar().showMessage(f"File {nf} could not be opened")
return False
fileInfo = QFileInfo(file)
self._currentDir = fileInfo.dir()
self._recentFiles.addFile(fileInfo.absoluteFilePath())
# If a viewer is already open, clean it up and save its settings
self.resetViewer()
self._viewer = self._factory.viewer(file)
if not self._viewer:
nf = QDir.toNativeSeparators(fileName)
self.statusBar().showMessage(f"File {nf} can't be opened.")
return False
self.ui.actionPrint.setEnabled(self._viewer.hasContent())
self._viewer.printingEnabledChanged.connect(self.ui.actionPrint.setEnabled)
self.ui.actionPrint.triggered.connect(self._viewer.print_)
self._viewer.showMessage.connect(self.statusBar().showMessage)
self._viewer.initViewer(self.ui.actionBack, self.ui.actionForward,
self.ui.menuHelp.menuAction(),
self.ui.tabWidget)
self.restoreViewerSettings()
self.ui.scrollArea.setWidget(self._viewer.widget())
return True
@Slot()
def onActionAboutTriggered(self):
viewerNames = ", ".join(self._factory.viewerNames())
mimeTypes = '\n'.join(self._factory.supportedMimeTypes())
text = ABOUT_TEXT
text += f"\nThis version has loaded the following plugins:\n{viewerNames}\n"
text += f"\n\nIt supports the following mime types:\n{mimeTypes}"
defaultViewer = self._factory.defaultViewer()
if defaultViewer:
n = defaultViewer.viewerName()
text += f"\n\nOther mime types will be displayed with {n}."
QMessageBox.about(self, "About Document Viewer Demo", text)
@Slot()
def onActionAboutQtTriggered(self):
QMessageBox.aboutQt(self)
def readSettings(self):
settings = QSettings()
# Restore working directory
if settings.contains(settingsDir):
self._currentDir = QDir(settings.value(settingsDir))
else:
self._currentDir = QDir.current()
# Restore QMainWindow state
if settings.contains(settingsMainWindow):
mainWindowState = settings.value(settingsMainWindow)
self.restoreState(mainWindowState)
# Restore recent files
self._recentFiles.restoreFromSettings(settings, settingsFiles)
def saveSettings(self):
settings = QSettings()
# Save working directory
settings.setValue(settingsDir, self._currentDir.absolutePath())
# Save QMainWindow state
settings.setValue(settingsMainWindow, self.saveState())
# Save recent files
self._recentFiles.saveSettings(settings, settingsFiles)
settings.sync()
def saveViewerSettings(self):
if not self._viewer:
return
settings = QSettings()
settings.beginGroup(settingsViewers)
settings.setValue(self._viewer.viewerName(), self._viewer.saveState())
settings.endGroup()
settings.sync()
def resetViewer(self):
if not self._viewer:
return
self.saveViewerSettings()
self._viewer.cleanup()
def restoreViewerSettings(self):
if not self._viewer:
return
settings = QSettings()
settings.beginGroup(settingsViewers)
viewerSettings = settings.value(self._viewer.viewerName())
settings.endGroup()
if viewerSettings:
self._viewer.restoreState(viewerSettings)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>983</width>
<height>602</height>
</rect>
</property>
<property name="windowTitle">
<string>Document Viewer Demo</string>
</property>
<property name="windowIcon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/qt-logo.png</normaloff>:/demos/documentviewer/images/qt-logo.png</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<property name="enabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="viewArea" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="tabPosition">
<enum>QTabWidget::TabPosition::West</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="bookmarkTab">
<attribute name="title">
<string>Pages</string>
</attribute>
</widget>
<widget class="QWidget" name="pagesTab">
<attribute name="title">
<string>Bookmarks</string>
</attribute>
</widget>
</widget>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>800</width>
<height>0</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>798</width>
<height>472</height>
</rect>
</property>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>983</width>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="qtFileMenu">
<property name="title">
<string>File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionRecent"/>
<addaction name="actionPrint"/>
<addaction name="actionQuit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout"/>
<addaction name="actionAboutQt"/>
</widget>
<addaction name="qtFileMenu"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QToolBar" name="mainToolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionOpen"/>
<addaction name="actionRecent"/>
<addaction name="actionPrint"/>
<addaction name="separator"/>
<addaction name="actionBack"/>
<addaction name="actionForward"/>
<addaction name="separator"/>
</widget>
<action name="actionOpen">
<property name="icon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/open.png</normaloff>:/demos/documentviewer/images/open.png</iconset>
</property>
<property name="text">
<string>Open</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionAbout">
<property name="icon">
<iconset theme="help-about" resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/help-about.svgz</normaloff>:/demos/documentviewer/images/help-about.svgz</iconset>
</property>
<property name="text">
<string>about documentviewer</string>
</property>
<property name="toolTip">
<string>Show information about the Document Viewer deomo.</string>
</property>
<property name="shortcut">
<string>Ctrl+H</string>
</property>
</action>
<action name="actionForward">
<property name="icon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/go-next.svgz</normaloff>:/demos/documentviewer/images/go-next.svgz</iconset>
</property>
<property name="text">
<string>actionForward</string>
</property>
<property name="toolTip">
<string>One step forward</string>
</property>
<property name="shortcut">
<string>Right</string>
</property>
</action>
<action name="actionBack">
<property name="icon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/go-previous.svgz</normaloff>:/demos/documentviewer/images/go-previous.svgz</iconset>
</property>
<property name="text">
<string>actionBack</string>
</property>
<property name="toolTip">
<string>One step back</string>
</property>
<property name="shortcut">
<string>Left</string>
</property>
</action>
<action name="actionPrint">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="document-print" resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/print2x.png</normaloff>:/demos/documentviewer/images/print2x.png</iconset>
</property>
<property name="text">
<string>Print</string>
</property>
<property name="toolTip">
<string>Print current file</string>
</property>
<property name="shortcut">
<string>Ctrl+P</string>
</property>
</action>
<action name="actionAboutQt">
<property name="icon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/qt-logo.png</normaloff>
<normalon>:/demos/documentviewer/images/qt-logo.png</normalon>:/demos/documentviewer/images/qt-logo.png</iconset>
</property>
<property name="text">
<string>About Qt</string>
</property>
<property name="toolTip">
<string>Show Qt license information</string>
</property>
<property name="shortcut">
<string>Ctrl+I</string>
</property>
</action>
<action name="actionRecent">
<property name="icon">
<iconset resource="documentviewer.qrc">
<normaloff>:/demos/documentviewer/images/document-open-recent.svgz</normaloff>:/demos/documentviewer/images/document-open-recent.svgz</iconset>
</property>
<property name="text">
<string>Recently opened...</string>
</property>
<property name="shortcut">
<string>Meta+R</string>
</property>
</action>
<action name="actionQuit">
<property name="icon">
<iconset theme="application-exit"/>
</property>
<property name="text">
<string>Quit</string>
</property>
<property name="toolTip">
<string>Quit the application</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
</widget>
<resources>
<include location="documentviewer.qrc"/>
</resources>
<connections>
<connection>
<sender>actionQuit</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>491</x>
<y>300</y>
</hint>
</hints>
</connection>
</connections>
</ui>
JsonViewer
在 QTreeView
中显示一个 JSON 文件。内部,它通过字符串将文件内容加载到数据结构中,并使用 JsonItemModel 填充自定义树模型。
JSON查看器演示了如何实现从QAbstractItemModel
继承的自定义项模型。
JsonViewer
使用文档的顶层对象作为导航的书签。其他节点(键和值)可以作为额外的书签添加,或从书签列表中移除。一个 QLineEdit
被用作搜索字段,以便在 JSON 树中进行导航。
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import json
from PySide6.QtWidgets import (QLabel, QLineEdit, QListWidget,
QListWidgetItem, QMenu, QTreeView)
from PySide6.QtGui import (QAction, QIcon, QKeySequence,
QPixmap, QTextDocument)
from PySide6.QtCore import (QAbstractItemModel, QDir,
QIODevice, QModelIndex,
QPoint, QSize, Qt, Slot)
from abstractviewer import AbstractViewer
def resizeToContents(tree):
for i in range(0, tree.header().count()):
tree.resizeColumnToContents(i)
class JsonTreeItem:
def __init__(self, parent=None):
self._key = ""
self._value = None
self._children = []
self._parent = parent
def key(self):
return self._key
def value(self):
return self._value
def appendChild(self, item):
self._children.append(item)
def child(self, row):
return self._children[row]
def parent(self):
return self._parent
def childCount(self):
return len(self._children)
def row(self):
if self._parent:
return self._parent._children.index(self)
return 0
def setKey(self, key):
self._key = key
def setValue(self, value):
self._value = value
@staticmethod
def load(value, parent=None):
rootItem = JsonTreeItem(parent)
rootItem.setKey("root")
if isinstance(value, dict):
for key, val in value.items():
child = JsonTreeItem.load(val, rootItem)
child.setKey(key)
rootItem.appendChild(child)
elif isinstance(value, list):
for index, val in enumerate(value):
child = JsonTreeItem.load(val, rootItem)
child.setKey(f"{index}")
rootItem.appendChild(child)
else:
rootItem.setValue(value)
return rootItem
class JsonItemModel(QAbstractItemModel):
def columnCount(self, index=QModelIndex()):
return 2
def itemFromIndex(self, index):
return index.internalPointer()
def __init__(self, doc, parent):
super().__init__(parent)
self._textItem = JsonTreeItem()
# Append header lines
self._headers = ["Key", "Value"]
# Reset the model. Root can either be a value or an array.
self.beginResetModel()
self._textItem = JsonTreeItem.load(doc) if doc else JsonTreeItem()
self.endResetModel()
def data(self, index, role):
if not index.isValid():
return None
item = self.itemFromIndex(index)
if role == Qt.ItemDataRole.DisplayRole:
if index.column() == 0:
return item.key()
if index.column() == 1:
return item.value()
elif role == Qt.ItemDataRole.EditRole:
if index.column() == 1:
return item.value()
return None
def headerData(self, section, orientation, role):
return (self._headers[section]
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal
else None)
def index(self, row, column, parent=QModelIndex()):
if not self.hasIndex(row, column, parent):
return None
parentItem = JsonTreeItem()
if not parent.isValid():
parentItem = self._textItem
else:
parentItem = self.itemFromIndex(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
return None
def parent(self, index):
if not index.isValid():
return None
childItem = self.itemFromIndex(index)
parentItem = childItem.parent()
if parentItem == self._textItem:
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent=QModelIndex()):
parentItem = JsonTreeItem()
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self._textItem
else:
parentItem = self.itemFromIndex(parent)
return parentItem.childCount()
class JsonViewer(AbstractViewer):
def __init__(self):
super().__init__()
self._tree = QTreeView()
self._toplevel = None
self._text = ""
self._searchKey = None
self.uiInitialized.connect(self.setupJsonUi)
def init(self, file, parent, mainWindow):
self._tree = QTreeView(parent)
super().init(file, self._tree, mainWindow)
def viewerName(self):
return "JsonViewer"
def supportedMimeTypes(self):
return ["application/json"]
@Slot()
def setupJsonUi(self):
# Build Menus and toolbars
menu = self.addMenu("Json")
tb = self.addToolBar("Json Actions")
zoomInIcon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn)
a = menu.addAction(zoomInIcon, "&+Expand all", self._tree.expandAll)
tb.addAction(a)
a.setPriority(QAction.LowPriority)
a.setShortcut(QKeySequence.New)
zoomOutIcon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomOut)
a = menu.addAction(zoomOutIcon, "&-Collapse all", self._tree.collapseAll)
tb.addAction(a)
a.setPriority(QAction.LowPriority)
a.setShortcut(QKeySequence.New)
if not self._searchKey:
self._searchKey = QLineEdit(tb)
label = QLabel(tb)
magnifier = QPixmap(":/icons/images/magnifier.png").scaled(QSize(28, 28))
label.setPixmap(magnifier)
tb.addWidget(label)
tb.addWidget(self._searchKey)
self._searchKey.textEdited.connect(self._tree.keyboardSearch)
if not self.openJsonFile():
return
# Populate bookmarks with toplevel
self._uiAssets_tabs.clear()
self._toplevel = QListWidget(self._uiAssets_tabs)
self._uiAssets_tabs.addTab(self._toplevel, "Bookmarks")
for i in range(0, self._tree.model().rowCount()):
index = self._tree.model().index(i, 0)
self._toplevel.addItem(index.data())
item = self._toplevel.item(i)
item.setData(Qt.ItemDataRole.UserRole, index)
item.setToolTip(f"Toplevel Item {i}")
self._toplevel.setAcceptDrops(True)
self._tree.setDragEnabled(True)
self._tree.setContextMenuPolicy(Qt.CustomContextMenu)
self._toplevel.setContextMenuPolicy(Qt.CustomContextMenu)
self._toplevel.itemClicked.connect(self.onTopLevelItemClicked)
self._toplevel.itemDoubleClicked.connect(self.onTopLevelItemDoubleClicked)
self._toplevel.customContextMenuRequested.connect(self.onBookmarkMenuRequested)
self._tree.customContextMenuRequested.connect(self.onJsonMenuRequested)
# Connect back and forward
self._uiAssets_back.triggered.connect(self._back)
self._uiAssets_forward.triggered.connect(self._forward)
@Slot()
def _back(self):
index = self._tree.indexAbove(self._tree.currentIndex())
if index.isValid():
self._tree.setCurrentIndex(index)
@Slot()
def _forward(self):
current = self._tree.currentIndex()
next = self._tree.indexBelow(current)
if next.isValid():
self._tree.setCurrentIndex(next)
return
# Expand last item to go beyond
if not self._tree.isExpanded(current):
self._tree.expand(current)
next = self._tree.indexBelow(current)
if next.isValid():
self._tree.setCurrentIndex(next)
def openJsonFile(self):
self.disablePrinting()
file_name = QDir.toNativeSeparators(self._file.fileName())
type = "open"
self._file.open(QIODevice.ReadOnly)
self._text = self._file.readAll().data().decode("utf-8")
self._file.close()
data = None
message = None
try:
data = json.loads(self._text)
message = f"Json document {file_name} opened"
model = JsonItemModel(data, self)
self._tree.setModel(model)
except ValueError as e:
message = f"Unable to parse Json document from {file_name}: {e}"
self.statusMessage(message, type)
self.maybeEnablePrinting()
return self._tree.model() is not None
def indexOf(self, item):
return QModelIndex(item.data(Qt.ItemDataRole.UserRole))
@Slot(QListWidgetItem)
def onTopLevelItemClicked(self, item):
"""Move to the clicked toplevel index"""
# return in the unlikely case that the tree has not been built
if not self._tree.model():
return
index = self.indexOf(item)
if not index.isValid():
return
self._tree.setCurrentIndex(index)
@Slot(QListWidgetItem)
def onTopLevelItemDoubleClicked(self, item):
"""Toggle double clicked index between collaps/expand"""
# return in the unlikely case that the tree has not been built
if not self._tree.model():
return
index = self.indexOf(item)
if not index.isValid():
return
if self._tree.isExpanded(index):
self._tree.collapse(index)
return
# Make sure the node and all parents are expanded
while index.isValid():
self._tree.expand(index)
index = index.parent()
@Slot(QPoint)
def onJsonMenuRequested(self, pos):
index = self._tree.indexAt(pos)
if not index.isValid():
return
# Don't show a context menu, if the index is already a bookmark
for i in range(0, self._toplevel.count()):
if self.indexOf(self._toplevel.item(i)) == index:
return
menu = QMenu(self._tree)
action = QAction("Add bookmark")
action.setData(index)
menu.addAction(action)
action.triggered.connect(self.onBookmarkAdded)
menu.exec(self._tree.mapToGlobal(pos))
@Slot(QPoint)
def onBookmarkMenuRequested(self, pos):
item = self._toplevel.itemAt(pos)
if not item:
return
# Don't delete toplevel items
index = self.indexOf(item)
if not index.parent().isValid():
return
menu = QMenu()
action = QAction("Delete bookmark")
action.setData(self._toplevel.row(item))
menu.addAction(action)
action.triggered.connect(self.onBookmarkDeleted)
menu.exec(self._toplevel.mapToGlobal(pos))
@Slot()
def onBookmarkAdded(self):
action = self.sender()
if not action:
return
index = action.data()
if not index.isValid():
return
item = QListWidgetItem(index.data(Qt.ItemDataRole.DisplayRole), self._toplevel)
item.setData(Qt.ItemDataRole.UserRole, index)
# Set a tooltip that shows where the item is located in the tree
parent = index.parent()
tooltip = index.data(Qt.ItemDataRole.DisplayRole).toString()
while parent.isValid():
tooltip = parent.data(Qt.ItemDataRole.DisplayRole).toString() + "." + tooltip
parent = parent.parent()
item.setToolTip(tooltip)
@Slot()
def onBookmarkDeleted(self):
action = self.sender()
if not action:
return
row = action.data().toInt()
if row < 0 or row >= self._toplevel.count():
return
self._toplevel.takeItem(row)
def hasContent(self):
return bool(self._text)
def supportsOverview(self):
return True
def printDocument(self, printer):
if not self.hasContent():
return
doc = QTextDocument(self._text)
doc.print_(printer)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from math import sqrt
from PySide6.QtWidgets import (QListView, QTreeView)
from PySide6.QtGui import QIcon, QPainter
from PySide6.QtCore import (QDir, QIODevice, QModelIndex,
QPointF, Slot)
from PySide6.QtPrintSupport import QPrinter
from PySide6.QtPdf import QPdfDocument, QPdfBookmarkModel
from PySide6.QtPdfWidgets import QPdfView, QPdfPageSelector
from abstractviewer import AbstractViewer
from pdfviewer.zoomselector import ZoomSelector
ZOOM_MULTIPLIER = sqrt(2.0)
class PdfViewer(AbstractViewer):
def __init__(self):
super().__init__()
self.uiInitialized.connect(self.initPdfViewer)
self._toolBar = None
self._zoomSelector = None
self._pageSelector = None
self._document = None
self._pdfView = None
self._actionForward = None
self._actionBack = None
self._bookmarks = None
self._pages = None
def init(self, file, parent, mainWindow):
self._pdfView = QPdfView(parent)
super().init(file, self._pdfView, mainWindow)
self._document = QPdfDocument(self)
def supportedMimeTypes(self):
return ["application/pdf"]
def initPdfViewer(self):
self._toolBar = self.addToolBar("PDF")
self._zoomSelector = ZoomSelector(self._toolBar)
nav = self._pdfView.pageNavigator()
self._pageSelector = QPdfPageSelector(self._toolBar)
self._toolBar.insertWidget(self._uiAssets_forward, self._pageSelector)
self._pageSelector.setDocument(self._document)
self._pageSelector.currentPageChanged.connect(self.pageSelected)
nav.currentPageChanged.connect(self._pageSelector.setCurrentPage)
nav.backAvailableChanged.connect(self._uiAssets_back.setEnabled)
self._actionBack = self._uiAssets_back
self._actionForward = self._uiAssets_forward
self._uiAssets_back.triggered.connect(self.onActionBackTriggered)
self._uiAssets_forward.triggered.connect(self.onActionForwardTriggered)
self._toolBar.addSeparator()
self._toolBar.addWidget(self._zoomSelector)
actionZoomIn = self._toolBar.addAction("Zoom in")
actionZoomIn.setToolTip("Increase zoom level")
icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn,
QIcon(":/demos/documentviewer/images/zoom-in.png"))
actionZoomIn.setIcon(icon)
self._toolBar.addAction(actionZoomIn)
actionZoomIn.triggered.connect(self.onActionZoomInTriggered)
actionZoomOut = self._toolBar.addAction("Zoom out")
actionZoomOut.setToolTip("Decrease zoom level")
icon = QIcon.fromTheme(QIcon.ThemeIcon.ZoomIn,
QIcon(":/demos/documentviewer/images/zoom-out.png"))
actionZoomOut.setIcon(icon)
self._toolBar.addAction(actionZoomOut)
actionZoomOut.triggered.connect(self.onActionZoomOutTriggered)
nav.backAvailableChanged.connect(self._actionBack.setEnabled)
nav.forwardAvailableChanged.connect(self._actionForward.setEnabled)
self._zoomSelector.zoomModeChanged.connect(self._pdfView.setZoomMode)
self._zoomSelector.zoomFactorChanged.connect(self._pdfView.setZoomFactor)
self._zoomSelector.reset()
bookmarkModel = QPdfBookmarkModel(self)
bookmarkModel.setDocument(self._document)
self._uiAssets_tabs.clear()
self._bookmarks = QTreeView(self._uiAssets_tabs)
self._bookmarks.activated.connect(self.bookmarkSelected)
self._bookmarks.setModel(bookmarkModel)
self._pdfView.setDocument(self._document)
self._pdfView.setPageMode(QPdfView.PageMode.MultiPage)
self.openPdfFile()
if not self._document.pageCount():
return
self._pages = QListView(self._uiAssets_tabs)
self._pages.setModel(self._document.pageModel())
self._pages.selectionModel().currentRowChanged.connect(self._currentRowChanged)
self._pdfView.pageNavigator().currentPageChanged.connect(self._pageChanged)
self._uiAssets_tabs.addTab(self._pages, "Pages")
self._uiAssets_tabs.addTab(self._bookmarks, "Bookmarks")
def viewerName(self):
return "PdfViewer"
@Slot(QModelIndex, QModelIndex)
def _currentRowChanged(self, current, previous):
if previous == current:
return
nav = self._pdfView.pageNavigator()
row = current.row()
if nav.currentPage() == row:
return
nav.jump(row, QPointF(), nav.currentZoom())
@Slot(int)
def _pageChanged(self, page):
if self._pages.currentIndex().row() == page:
return
self._pages.setCurrentIndex(self._pages.model().index(page, 0))
@Slot()
def openPdfFile(self):
self.disablePrinting()
if self._file.open(QIODevice.ReadOnly):
self._document.load(self._file)
documentTitle = self._document.metaData(QPdfDocument.MetaDataField.Title)
if not documentTitle:
documentTitle = "PDF Viewer"
self.statusMessage(documentTitle)
self.pageSelected(0)
file_name = QDir.toNativeSeparators(self._file.fileName())
self.statusMessage(f"Opened PDF file {file_name}")
self.maybeEnablePrinting()
def hasContent(self):
return self._document if self._document.pageCount() > 0 else False
def supportsOverview(self):
return True
def printDocument(self, printer):
if not self.hasContent():
return
painter = QPainter()
painter.begin(printer)
pageRect = printer.pageRect(QPrinter.Unit.DevicePixel).toRect()
pageSize = pageRect.size()
for i in range(0, self._document.pageCount()):
if i > 0:
printer.newPage()
page = self._document.render(i, pageSize)
painter.drawImage(pageRect, page)
painter.end()
@Slot(QModelIndex)
def bookmarkSelected(self, index):
if not index.isValid():
return
page = index.data(int(QPdfBookmarkModel.Role.Page))
zoomLevel = index.data(int(QPdfBookmarkModel.Role.Level)).toReal()
self._pdfView.pageNavigator().jump(page, QPointF(), zoomLevel)
@Slot(int)
def pageSelected(self, page):
nav = self._pdfView.pageNavigator()
nav.jump(page, QPointF(), nav.currentZoom())
@Slot()
def onActionZoomInTriggered(self):
self._pdfView.setZoomFactor(self._pdfView.zoomFactor() * ZOOM_MULTIPLIER)
@Slot()
def onActionZoomOutTriggered(self):
self._pdfView.setZoomFactor(self._pdfView.zoomFactor() / ZOOM_MULTIPLIER)
@Slot()
def onActionPreviousPageTriggered(self):
nav = self._pdfView.pageNavigator()
nav.jump(nav.currentPage() - 1, QPointF(), nav.currentZoom())
@Slot()
def onActionNextPageTriggered(self):
nav = self._pdfView.pageNavigator()
nav.jump(nav.currentPage() + 1, QPointF(), nav.currentZoom())
@Slot()
def onActionBackTriggered(self):
self._pdfView.pageNavigator().back()
@Slot()
def onActionForwardTriggered(self):
self._pdfView.pageNavigator().forward()
# Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtWidgets import QComboBox
from PySide6.QtCore import Signal, Slot
from PySide6.QtPdfWidgets import QPdfView
ZOOM_LEVELS = ["Fit Width", "Fit Page", "12%", "25%", "33%", "50%", "66%",
"75%", "100%", "125%", "150%", "200%", "400%"]
class ZoomSelector(QComboBox):
zoomModeChanged = Signal(QPdfView.ZoomMode)
zoomFactorChanged = Signal(float)
def __init__(self, parent):
super().__init__(parent)
self.setEditable(True)
for z in ZOOM_LEVELS:
self.addItem(z)
self.currentTextChanged.connect(self.onCurrentTextChanged)
self.lineEdit().editingFinished.connect(self._editingFinished)
@Slot()
def _editingFinished(self):
self.onCurrentTextChanged(self.lineEdit().text())
@Slot(float)
def setZoomFactor(self, zoomFactor):
z = int(100 * zoomFactor)
self.setCurrentText(f"{z}%")
@Slot()
def reset(self):
self.setCurrentIndex(8) # 100%
@Slot(str)
def onCurrentTextChanged(self, text):
if text == "Fit Width":
self.zoomModeChanged.emit(QPdfView.ZoomMode.FitToWidth)
elif text == "Fit Page":
self.zoomModeChanged.emit(QPdfView.ZoomMode.FitInView)
else:
factor = 1.0
withoutPercent = text.replace('%', '')
zoomLevel = int(withoutPercent)
if zoomLevel:
factor = zoomLevel / 100.0
self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom)
self.zoomFactorChanged.emit(factor)
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtWidgets import (QDialog, QFileDialog,
QPlainTextEdit)
from PySide6.QtGui import QAction, QGuiApplication, QIcon, QKeySequence
from PySide6.QtCore import QDir, QFile, QTextStream, Qt, Slot
from abstractviewer import AbstractViewer
class TxtViewer(AbstractViewer):
def __init__(self):
super().__init__()
self.uiInitialized.connect(self.setupTxtUi)
def init(self, file, parent, mainWindow):
self._textEdit = QPlainTextEdit(parent)
super().init(file, self._textEdit, mainWindow)
def viewerName(self):
return "TxtViewer"
def supportedMimeTypes(self):
return ["text/plain"]
@Slot()
def setupTxtUi(self):
editMenu = self.addMenu("Edit")
editToolBar = self.addToolBar("Edit")
cutIcon = QIcon.fromTheme(QIcon.ThemeIcon.EditCut,
QIcon(":/demos/documentviewer/images/cut.png"))
cutAct = QAction(cutIcon, "Cut", self)
cutAct.setShortcuts(QKeySequence.Cut)
cutAct.setStatusTip("Cut the current selection's contents to the clipboard")
cutAct.triggered.connect(self._textEdit.cut)
editMenu.addAction(cutAct)
editToolBar.addAction(cutAct)
copyIcon = QIcon.fromTheme(QIcon.ThemeIcon.EditCopy,
QIcon(":/demos/documentviewer/images/copy.png"))
copyAct = QAction(copyIcon, "Copy", self)
copyAct.setShortcuts(QKeySequence.Copy)
copyAct.setStatusTip("Copy the current selection's contents to the clipboard")
copyAct.triggered.connect(self._textEdit.copy)
editMenu.addAction(copyAct)
editToolBar.addAction(copyAct)
pasteIcon = QIcon.fromTheme(QIcon.ThemeIcon.EditPaste,
QIcon(":/demos/documentviewer/images/paste.png"))
pasteAct = QAction(pasteIcon, "Paste", self)
pasteAct.setShortcuts(QKeySequence.Paste)
pasteAct.setStatusTip("Paste the clipboard's contents into the current selection")
pasteAct.triggered.connect(self._textEdit.paste)
editMenu.addAction(pasteAct)
editToolBar.addAction(pasteAct)
self.menuBar().addSeparator()
cutAct.setEnabled(False)
copyAct.setEnabled(False)
self._textEdit.copyAvailable.connect(cutAct.setEnabled)
self._textEdit.copyAvailable.connect(copyAct.setEnabled)
self.openFile()
self._textEdit.textChanged.connect(self._textChanged)
self._uiAssets_back.triggered.connect(self._back)
self._uiAssets_forward.triggered.connect(self._forward)
@Slot()
def _textChanged(self):
self.maybeSetPrintingEnabled(self.hasContent())
@Slot()
def _back(self):
bar = self._textEdit.verticalScrollBar()
if bar.value() > bar.minimum():
bar.setValue(bar.value() - 1)
@Slot()
def _forward(self):
bar = self._textEdit.verticalScrollBar()
if bar.value() < bar.maximum():
bar.setValue(bar.value() + 1)
def openFile(self):
type = "open"
file_name = QDir.toNativeSeparators(self._file.fileName())
if not self._file.open(QFile.ReadOnly | QFile.Text):
err = self._file.errorString()
self.statusMessage(f"Cannot read file {file_name}:\n{err}.", type)
return
in_str = QTextStream(self._file)
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
if self._textEdit.toPlainText():
self._textEdit.clear()
self.disablePrinting()
self._textEdit.setPlainText(in_str.readAll())
QGuiApplication.restoreOverrideCursor()
self.statusMessage(f"File {file_name} loaded.", type)
self.maybeEnablePrinting()
def hasContent(self):
return bool(self._textEdit.toPlainText())
def printDocument(self, printer):
if not self.hasContent():
return
self._textEdit.print_(printer)
def saveFile(self, file):
file_name = QDir.toNativeSeparators(self._file.fileName())
errorMessage = ""
QGuiApplication.setOverrideCursor(Qt.WaitCursor)
if file.open(QFile.WriteOnly | QFile.Text):
out = QTextStream(file)
out << self._textEdit.toPlainText()
else:
error = file.errorString()
errorMessage = f"Cannot open file {file_name} for writing:\n{error}."
QGuiApplication.restoreOverrideCursor()
if errorMessage:
self.statusMessage(errorMessage)
return False
self.statusMessage(f"File {file_name} saved")
return True
def saveDocumentAs(self):
dialog = QFileDialog(self.mainWindow())
dialog.setWindowModality(Qt.WindowModal)
dialog.setAcceptMode(QFileDialog.AcceptSave)
if dialog.exec() != QDialog.Accepted:
return False
files = dialog.selectedFiles()
self._file.setFileName(files[0])
return self.saveDocument()
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtWidgets import QMenu
from PySide6.QtCore import Signal, Slot
class RecentFileMenu(QMenu):
fileOpened = Signal(str)
def __init__(self, parent, recent):
super().__init__(parent)
self._recentFiles = recent
self._recentFiles.changed.connect(self.updateList)
self._recentFiles.destroyed.connect(self.deleteLater)
self.updateList()
@Slot()
def updateList(self):
for a in self.actions():
del a
if not self._recentFiles:
self.addAction("<no recent files>")
return
for fileName in self._recentFiles.recentFiles():
action = self.addAction(fileName)
action.triggered.connect(self._emitFileOpened)
@Slot()
def _emitFileOpened(self):
action = self.sender()
self.fileOpened.emit(action.text())
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from enum import Enum, auto
from PySide6.QtCore import QFileInfo, QObject, QSettings, Signal, Slot
DEFAULT_MAX_FILES = 10
# Test if file exists and can be opened
def testFileAccess(fileName):
return QFileInfo(fileName).isReadable()
class RemoveReason(Enum):
Other = auto()
Duplicate = auto()
class EmitPolicy(Enum):
EmitWhenChanged = auto(),
NeverEmit = auto()
s_maxFiles = "maxFiles"
s_openMode = "openMode"
s_fileNames = "fileNames"
s_file = "file"
class RecentFiles(QObject):
countChanged = Signal(int)
changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._maxFiles = DEFAULT_MAX_FILES
self._files = []
# Access to QStringList member functions
def recentFiles(self):
return self._files
def isEmpty(self):
return not self._files
# Properties
def maxFiles(self):
return self._maxFiles
def setMaxFiles(self, maxFiles):
self._maxFiles = maxFiles
def addFile(self, fileName):
self._addFile(fileName, EmitPolicy.EmitWhenChanged)
def removeFile(self, fileName):
idx = self._files.find(fileName)
self._removeFile(idx, RemoveReason.Other)
@Slot()
def clear(self):
if self.isEmpty():
return
self._files.clear()
self.countChanged.emit(0)
def _addFile(self, fileName, policy):
if not testFileAccess(fileName):
return
# Remember size, as cleanup can result in a change without size change
c = len(self._files)
# Clean dangling and duplicate files
i = 0
while i < len(self._files):
file = self._files[i]
if not testFileAccess(file):
self._removeFile(file, RemoveReason.Other)
elif file == fileName:
self._removeFile(file, RemoveReason.Duplicate)
else:
i += 1
# Cut tail
while len(self._files) > self._maxFiles:
self.removeFile((len(self._files) - 1), RemoveReason.Other)
self._files.insert(0, fileName)
if policy == EmitPolicy.NeverEmit:
return
if policy == EmitPolicy.EmitWhenChanged:
self.changed.emit()
if c != len(self._files):
self.countChanged.emit(len(self._files))
@Slot(list)
def addFiles(self, files):
if files.isEmpty():
return
if len(files) == 1:
self.addFile(files[0])
return
c = len(self._files)
for file in files:
self.addFile(file, EmitPolicy.NeverEmit)
self.changed.emit()
if len(self._files) != c:
self.countChanged.emit(len(self._files))
def _removeFile(self, p, reason):
index = p
if isinstance(p, str):
index = self._files.index(p) if p in self._files else -1
if index < 0 or index >= len(self._files):
return
del self._files[index]
# No emit for duplicate removal, add emits changed later.
if reason != RemoveReason.Duplicate:
self.changed.emit()
@Slot(QSettings, str)
def saveSettings(self, settings, key):
settings.beginGroup(key)
settings.setValue(s_maxFiles, self.maxFiles())
if self._files:
settings.beginWriteArray(s_fileNames, len(self._files))
for index, file in enumerate(self._files):
settings.setArrayIndex(index)
settings.setValue(s_file, file)
settings.endArray()
settings.endGroup()
@Slot(QSettings, str)
def restoreFromSettings(self, settings, key):
settings.beginGroup(key)
self.setMaxFiles(settings.value(s_maxFiles, DEFAULT_MAX_FILES, int))
self._files.clear() # clear list without emitting
numberFiles = settings.beginReadArray(s_fileNames)
for index in range(0, numberFiles):
settings.setArrayIndex(index)
absoluteFilePath = settings.value(s_file)
self._addFile(absoluteFilePath, EmitPolicy.NeverEmit)
settings.endArray()
settings.endGroup()
if self._files:
self.changed.emit()
return True
ViewerFactory
类管理已知文件类型的查看器。它在构建时加载所有可用的查看器,并提供一个公共API来查询加载的插件、它们的名称以及支持的MIME类型。
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from enum import Enum, auto
from PySide6.QtWidgets import (QMessageBox)
from PySide6.QtCore import (QFileInfo, QMimeDatabase, QTimer)
from txtviewer.txtviewer import TxtViewer
from jsonviewer.jsonviewer import JsonViewer
from pdfviewer.pdfviewer import PdfViewer
class DefaultPolicy(Enum):
NeverDefault = auto()
DefaultToTxtViewer = auto()
DefaultToCustomViewer = auto()
class ViewerFactory:
def __init__(self, displayWidget, mainWindow,
policy=DefaultPolicy.NeverDefault):
self._viewers = {}
self._defaultViewer = None
self._defaultWarning = True
self._defaultPolicy = policy
self._displayWidget = displayWidget
self._mainWindow = mainWindow
self._mimeTypes = []
for v in [PdfViewer(), JsonViewer(), TxtViewer()]:
self._viewers[v.viewerName()] = v
if v.isDefaultViewer():
self._defaultViewer = v
def defaultPolicy(self):
return self._defaultPolicy
def setDefaultPolicy(self, policy):
self._defaultPolicy = policy
def defaultWarning(self):
return self._defaultWarning
def setDefaultWarning(self, on):
self._defaultWarning = on
def viewer(self, file):
info = QFileInfo(file)
db = QMimeDatabase()
mimeType = db.mimeTypeForFile(info)
viewer = self.viewerForMimeType(mimeType)
if not viewer:
print(f"Mime type {mimeType.name()} not supported.")
return None
viewer.init(file, self._displayWidget, self._mainWindow)
return viewer
def viewerNames(self, showDefault=False):
if not showDefault:
return self._viewers.keys()
list = []
for name, viewer in self._viewers.items():
if ((self._defaultViewer and viewer.isDefaultViewer())
or (not self._defaultViewer and name == "TxtViewer")):
name += "(default)"
list.append(name)
return list
def viewers(self):
return self._viewers.values()
def findViewer(self, viewerName):
for viewer in self.viewers():
if viewer.viewerName() == viewerName:
return viewer
print(f"Plugin {viewerName} not loaded.")
return None
def viewerForMimeType(self, mimeType):
for viewer in self.viewers():
for type in viewer.supportedMimeTypes():
if mimeType.inherits(type):
return viewer
viewer = self.defaultViewer()
if self._defaultWarning:
mbox = QMessageBox()
mbox.setIcon(QMessageBox.Warning)
name = mimeType.name()
viewer_name = viewer.viewerName()
m = f"Mime type {name} not supported. Falling back to {viewer_name}."
mbox.setText(m)
mbox.setStandardButtons(QMessageBox.Ok)
QTimer.singleShot(8000, mbox.close)
mbox.exec()
return viewer
def defaultViewer(self):
if self._defaultPolicy == DefaultPolicy.NeverDefault:
return None
if self._defaultPolicy == DefaultPolicy.DefaultToCustomViewer and self._defaultViewer:
return self._defaultViewer
return self.findViewer("TxtViewer")
def supportedMimeTypes(self):
if not self._mimeTypes:
for viewer in self.viewers():
self._mimeTypes.extend(viewer.supportedMimeTypes())
return self._mimeTypes