import json import sys 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.QtWidgets import ( QApplication, QDockWidget, QFileDialog, QMainWindow, QPushButton, QTreeView, QVBoxLayout, QWidget, ) from PySide2.QtGui import QIcon, QImage, QPixmap from PySide2.QtWebChannel import QWebChannel from PySide2.QtWebEngineWidgets import QWebEngineView 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 = "Untitled" parent: "TreeItem" = None 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): UNTITLED_NODE_NAME = "Untitled" def __init__(self): super().__init__() self.root = TreeItem.load(["root", [["first", [["second", [["third", []]]]]]]]) def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex: if not self.hasIndex(row, column, parent): return QModelIndex() parent_item = parent.internalPointer() if parent.isValid() else self.root if len(parent_item.children) > row: return self.createIndex(row, column, parent_item.children[row]) return QModelIndex() def parent(self, index: QModelIndex) -> QModelIndex: if not index.isValid(): return QModelIndex() child_item = index.internalPointer() parent_item = child_item.parent if parent_item == self.root: return QModelIndex() return self.createIndex(parent_item.row(), 0, parent_item) def flags(self, index: QModelIndex) -> Qt.ItemFlags: if index.isValid(): return ( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled ) else: return Qt.ItemIsEnabled def rowCount(self, parent=QModelIndex()): parent_item = parent.internalPointer() if parent.isValid() else self.root return len(parent_item.children) def columnCount(self, parent=QModelIndex()): return 1 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 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 insertRows(self, row: int, count: int, parent: QModelIndex) -> bool: item: TreeItem = parent.internalPointer() or self.root self.beginInsertRows(parent, row, row + count) item.children.insert(row, TreeItem(LibraryModel.UNTITLED_NODE_NAME, item, [])) self.endInsertRows() return True def removeRows(self, row: int, count: int, parent: QModelIndex) -> bool: item: TreeItem = parent.internalPointer() or self.root self.beginRemoveRows(parent, row, row + count) del item.children[row] self.endRemoveRows() return True def supportedDragActions(self) -> Qt.DropActions: return Qt.CopyAction | Qt.MoveAction def supportedDropActions(self) -> Qt.DropActions: return Qt.CopyAction | Qt.MoveAction 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.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.library_model = LibraryModel() self.library_view = QTreeView() self.dock_layout.addWidget(self.library_view) self.library_view.setModel(self.library_model) self.library_view.setDragEnabled(True) self.library_view.setAcceptDrops(True) self.library_view.setDefaultDropAction(Qt.MoveAction) self.library_view.setHeaderHidden(True) 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.new_node = QPushButton("New node") self.dock_layout.addWidget(self.new_node) self.new_node.clicked.connect( lambda: self.library_model.insertRow(0, self.library_view.currentIndex()) ) self.delete_node = QPushButton("Delete node") self.dock_layout.addWidget(self.delete_node) self.delete_node.clicked.connect( lambda: self.library_model.removeRow( self.library_view.currentIndex().row(), self.library_view.currentIndex().parent(), ) ) 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()