From 1c24573b340f45805e43eaeb594ab9fe107a0fb0 Mon Sep 17 00:00:00 2001 From: olari Date: Fri, 11 Jun 2021 21:47:05 +0300 Subject: [PATCH] add main window; webview; webchannel; pdf.js; wip sidebar --- .gitignore | 1 + .gitmodules | 3 + controller.js | 40 +++++++++ main.py | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++ pdf.js | 1 + test.png | Bin 0 -> 5191 bytes 6 files changed, 274 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 controller.js create mode 100644 main.py create mode 160000 pdf.js create mode 100644 test.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ccafd7d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pdf.js"] + path = pdf.js + url = https://github.com/mozilla/pdf.js.git diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..9004387 --- /dev/null +++ b/controller.js @@ -0,0 +1,40 @@ + +const h = (name, props = {}, ...children) => { + const element = document.createElement(name); + + for (const [key, value] of Object.entries(props)) + key.startsWith('on') + ? element.addEventListener(key.substring(2), value) + : element.setAttribute(key, value); + + for (const child of children) + element.appendChild( + typeof(child) === 'string' + ? document.createTextNode(child) + : child); + + return element; +}; + +const qs = s => document.querySelector(s); +const qsa = s => Array.from(document.querySelectorAll(s)) + +const on_command = command => { + switch(command.type) { + case 'alert': alert(command.message); break; + case 'load_pdf': pdfjsLib.getDocument(command.url).promise.then(pdf => PDFViewerApplication.load(pdf)); break; + } +}; + +let call_python; + +new QWebChannel(qt.webChannelTransport, channel => { + call_python = (argument, callback) => + channel.objects.bridge.call_python(JSON.stringify(argument), + result => callback && callback(JSON.parse(result))); + + channel.objects.bridge.call_javascript.connect( + result => on_command(JSON.parse(result))); + + call_python({'type': 'ready'}); +}); diff --git a/main.py b/main.py new file mode 100644 index 0000000..87a03b0 --- /dev/null +++ b/main.py @@ -0,0 +1,229 @@ +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() \ No newline at end of file diff --git a/pdf.js b/pdf.js new file mode 160000 index 0000000..7b4fa0a --- /dev/null +++ b/pdf.js @@ -0,0 +1 @@ +Subproject commit 7b4fa0a03868af03231d587fc1001fafe71c5e9f diff --git a/test.png b/test.png new file mode 100644 index 0000000000000000000000000000000000000000..232c527fa5e33dd54e2a7e79dc3c87d5deea1a1f GIT binary patch literal 5191 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 D1V=K5 literal 0 HcmV?d00001