commit 1c24573b340f45805e43eaeb594ab9fe107a0fb0 Author: olari Date: Fri Jun 11 21:47:05 2021 +0300 add main window; webview; webchannel; pdf.js; wip sidebar 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 0000000..232c527 Binary files /dev/null and b/test.png differ