import sys import json from dataclasses import dataclass from pathlib import Path from typing import Any from PySide2.QtCore import QAbstractItemModel, QModelIndex, QObject, QUrl, Qt, Signal, Slot from PySide2.QtGui import QIcon, QImage, QPicture, QPixmap from PySide2.QtWebChannel import QWebChannel from PySide2.QtWebEngineWidgets import QWebEngineView from PySide2.QtWidgets import ( QApplication, QDockWidget, QFileDialog, QMainWindow, QPushButton, QTreeView, QVBoxLayout, QWidget ) def qurl_from_local(fpath): return QUrl.fromLocalFile(str(Path(fpath).absolute())) def file_url_from_local(fpath): return f'file://{Path(fpath).absolute()}' class Bridge(QObject): @Slot(str, result=str) def call_python(self, data): return json.dumps(self.handler(json.loads(data))) call_javascript = Signal(str) @dataclass class TreeItem: name: str parent: "TreeItem" children: list["TreeItem"] def row(self): return self.parent.children.index(self) if self.parent else 0 @staticmethod def load(value, parent=None): name, children = value item = TreeItem(name, parent, []) for child in children: if len(child) == 2: item.children.append(TreeItem.load(child, item)) return item class LibraryModel(QAbstractItemModel): def __init__(self): super().__init__() self.root = TreeItem.load( ['root', [ ['first', [ ['second', [ ['third', []]]]]]]]) def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any: if not index.isValid(): return None if role == Qt.DisplayRole or role == Qt.EditRole: item = index.internalPointer() return item.name elif role == Qt.DecorationRole: img = QImage() img.load('test.png') return QIcon(QPixmap.fromImage(img)) def insertRows(self, row: int, count: int, parent: QModelIndex) -> bool: item: TreeItem = parent.internalPointer() self.beginInsertRows(parent, row, row) item.children.insert(row, TreeItem('', item, [])) self.endInsertRows() return True def setData(self, index: QModelIndex, value: str, role: Qt.ItemDataRole): if not index.isValid(): return False item: TreeItem = index.internalPointer() item.name = value self.dataChanged.emit(index, index, Qt.EditRole) return True def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex: if not self.hasIndex(row, column, parent): return QModelIndex() if not parent.isValid(): parentItem = self.root else: parentItem = parent.internalPointer() if len(parentItem.children) > row: return self.createIndex(row, column, parentItem.children[row]) else: return QModelIndex() def parent(self, index: QModelIndex) -> QModelIndex: if not index.isValid(): return QModelIndex() childItem = index.internalPointer() parentItem = childItem.parent if parentItem == self.root: return QModelIndex() return self.createIndex(parentItem.row(), 0, parentItem) def rowCount(self, parent=QModelIndex()): if not parent.isValid(): parentItem = self.root else: parentItem = parent.internalPointer() return len(parentItem.children) def columnCount(self, parent=QModelIndex()): return 1 def flags(self, index: QModelIndex) -> Qt.ItemFlags: flags = super().flags(index) return Qt.ItemIsEditable | flags class MainWindow(QMainWindow): def __init__(self): super().__init__() self.widget = QWidget() self.setCentralWidget(self.widget) self._layout = QVBoxLayout() self.widget.setLayout(self._layout) self.web_view = QWebEngineView() self._layout.addWidget(self.web_view) self.library_model = LibraryModel() self.library_view = QTreeView() self.library_view.setHeaderHidden(True) self.library_view.setModel(self.library_model) def on_insert_row(p: QModelIndex, f, l): self.library_view.setExpanded(p, True) self.library_view.edit(p.child(f, 0)) self.library_model.rowsInserted.connect(on_insert_row) self.dock_widget = QDockWidget('Library') self.dock_qwidget = QWidget() self.dock_widget.setWidget(self.dock_qwidget) self.dock_layout = QVBoxLayout() self.dock_qwidget.setLayout(self.dock_layout) self.new_node = QPushButton("New node") self.dock_layout.addWidget(self.library_view) self.dock_layout.addWidget(self.new_node) self.new_node.clicked.connect(lambda: self.library_model.insertRow(0, self.library_view.currentIndex()) ) self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_widget) self.web_view.load(qurl_from_local('pdf.js/build/generic/web/viewer.html')) def on_load(): def load_javascript_file(filepath): self.web_view.page().runJavaScript( 'var s = document.createElement("script");' f's.src="{filepath}";' 'document.body.appendChild(s);' ) load_javascript_file('qrc:///qtwebchannel/qwebchannel.js') load_javascript_file(file_url_from_local('controller.js')) self.web_view.loadFinished.connect(on_load) self.channel = QWebChannel() self.web_view.page().setWebChannel(self.channel) self.bridge = Bridge() self.channel.registerObject('bridge', self.bridge) def on_command(command): if command['type'] == 'ready': print('webchannel ready') self.bridge.handler = on_command def call_javascript(command): self.bridge.call_javascript.emit(json.dumps(command)) self.button = QPushButton('Load PDF') self._layout.addWidget(self.button) self.button.clicked.connect(lambda: call_javascript({'type': 'load_pdf', 'url': file_url_from_local(QFileDialog.getOpenFileName()[0])})) def main(): app = QApplication() window = MainWindow() availableGeometry = app.desktop().availableGeometry(window) window.resize(availableGeometry.width() * 2 / 3, availableGeometry.height() * 2 / 3) window.show() sys.exit(app.exec_()) if __name__ == '__main__': main()