diff --git a/projspecbriefcase/LICENSE b/projspecbriefcase/LICENSE new file mode 100644 index 0000000..0300831 --- /dev/null +++ b/projspecbriefcase/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026, Martin Durant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/projspecbriefcase/pyproject.toml b/projspecbriefcase/pyproject.toml new file mode 100644 index 0000000..a5e6728 --- /dev/null +++ b/projspecbriefcase/pyproject.toml @@ -0,0 +1,116 @@ +# This project was generated with 0.3.25 using template: https://github.com/beeware/briefcase-template @ v0.3.25 +[tool.briefcase] +project_name = "projspec-briefcase" +bundle = "com.example" +version = "0.0.1" +url = "https://projspec.readthedocs.io/en/latest/" +license.file = "LICENSE" +author = "Martin Durant" +author_email = "martin.durant@alumni.utoronto.ca" + +[tool.briefcase.app.projspecbriefcase] +formal_name = "projspec-briefcase" +description = "App bundle for projspec" +long_description = """""" +sources = [ + "src/projspecbriefcase", +] + +requires = [ + "PySide6", + "../" +] + +[tool.briefcase.app.projspecbriefcase.macOS] +universal_build = true +# As of Pyside 6.8, PySide enforces a macOS 12 minimum on wheels. +min_os_version = "12.0" +requires = [ + "std-nslog~=1.0.3", +] + +[tool.briefcase.app.projspecbriefcase.linux] +requires = [ +] + +[tool.briefcase.app.projspecbriefcase.linux.system.debian] +system_requires = [ +] + +system_runtime_requires = [ + # Derived from https://doc.qt.io/qt-6/linux-requirements.html + "libxext6", + "libxrender1", + "libx11-xcb1", + "libxkbcommon-x11-0", + "libxcb-image0", + "libxcb-cursor0", + "libxcb-shape0", + "libxcb-randr0", + "libxcb-xfixes0", + "libxcb-sync1", + "libxcb-icccm4", + "libxcb-keysyms1", + "libfontconfig1", + "libsm6", + "libice6", + "libglib2.0-0", + "libgl1", + "libegl1", + "libdbus-1-3", +] + +[tool.briefcase.app.projspecbriefcase.linux.system.rhel] +system_requires = [ +] + +system_runtime_requires = [ + "qt6-qtbase-gui", +] + +[tool.briefcase.app.projspecbriefcase.linux.system.suse] +system_requires = [ +] + +system_runtime_requires = [ + "libgthread-2_0-0", + "libQt6Gui6", +] + +[tool.briefcase.app.projspecbriefcase.linux.system.arch] +system_requires = [ +] + +system_runtime_requires = [ + "qt6-base", +] + +[tool.briefcase.app.projspecbriefcase.linux.appimage] +manylinux = "manylinux_2_28" + +system_requires = [ +# ?? FIXME +] + +linuxdeploy_plugins = [ +] + +[tool.briefcase.app.projspecbriefcase.linux.flatpak] +flatpak_runtime = "org.kde.Platform" +flatpak_runtime_version = "6.9" +flatpak_sdk = "org.kde.Sdk" + +[tool.briefcase.app.projspecbriefcase.windows] +requires = [ +] + +# Mobile deployments +[tool.briefcase.app.projspecbriefcase.iOS] +supported = false + +[tool.briefcase.app.projspecbriefcase.android] +supported = false + +# Web deployments +[tool.briefcase.app.projspecbriefcase.web] +supported = false diff --git a/projspecbriefcase/src/projspecbriefcase/__init__.py b/projspecbriefcase/src/projspecbriefcase/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projspecbriefcase/src/projspecbriefcase/__main__.py b/projspecbriefcase/src/projspecbriefcase/__main__.py new file mode 100644 index 0000000..3db9eb6 --- /dev/null +++ b/projspecbriefcase/src/projspecbriefcase/__main__.py @@ -0,0 +1,5 @@ +from projspec.qtapp.main import main + + +if __name__ == "__main__": + main() diff --git a/src/projspec/qtapp/main.py b/src/projspec/qtapp/main.py index 1a5a000..ed4d885 100644 --- a/src/projspec/qtapp/main.py +++ b/src/projspec/qtapp/main.py @@ -27,11 +27,11 @@ from projspec.utils import class_infos try: - from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtSlot - from PyQt5.QtGui import QIcon - from PyQt5.QtWebChannel import QWebChannel - from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineView - from PyQt5.QtWidgets import ( + from PySide6.QtCore import QObject, QUrl, Signal, Slot + from PySide6.QtGui import QIcon + from PySide6.QtWebChannel import QWebChannel + from PySide6.QtWebEngineWidgets import QWebEngineView + from PySide6.QtWidgets import ( QApplication, QFileDialog, QMainWindow, @@ -41,13 +41,16 @@ ) qt = True -except ImportError: +except ImportError as e: # fallbacks to make this module importable and give a decent message + import traceback + QObject = object QMainWindow = object - pyqtSignal = lambda *_: None - pyqtSlot = lambda *_: lambda *_: None - warnings.warn("PyQt5 not installed", ImportWarning) + Signal = lambda *_: None + Slot = lambda *_: lambda *_: None + print("Failed to import PySide6") + traceback.print_exc() qt = False from projspec.qtapp.views import get_panel_html @@ -75,7 +78,7 @@ class JsBridge(QObject): slot dispatches on ``type``. """ - from_python = pyqtSignal(str) # JSON-encoded outbound message + from_python = Signal(str) # JSON-encoded outbound message def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) @@ -84,7 +87,7 @@ def __init__(self, parent: QObject | None = None) -> None: def set_handler(self, handler) -> None: self._handler = handler - @pyqtSlot(str) + @Slot(str) def handleMessage(self, message: str) -> None: try: data = json.loads(message) @@ -117,38 +120,12 @@ def __init__(self) -> None: channel.registerObject("bridge", self._bridge) self._view.page().setWebChannel(channel) - # Qt WebEngine's default settings block a ``file://`` page from - # loading webfonts referenced by ``data:`` URIs in its CSS. - # Flipping these three switches tells Chromium to treat the page - # the same way it would an HTTP page so ``@font-face`` resolves. - settings = self._view.settings() - for attr in ( - "LocalContentCanAccessRemoteUrls", - "LocalContentCanAccessFileUrls", - "ErrorPageEnabled", - ): - constant = getattr( - QWebEngineSettings.WebAttribute - if hasattr(QWebEngineSettings, "WebAttribute") - else QWebEngineSettings, - attr, - None, - ) - if constant is not None: - settings.setAttribute(constant, True) - central = QWidget(self) layout = QVBoxLayout(central) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._view) self.setCentralWidget(central) - # Load the shared HTML UI. We write to a temp file and ``setUrl`` - # it rather than calling ``setHtml``: the latter gives the page an - # opaque origin that Chromium treats like a cross-origin document, - # breaking anything that touches the page origin (external links, - # relative anchors, future ``fetch`` calls). Loading from a real - # ``file://`` URL sidesteps all of that. import tempfile tmp = tempfile.NamedTemporaryFile( @@ -381,7 +358,7 @@ def _action_reveal_file(self, fn: str) -> None: return target = matches[0] if len(matches) > 1: - from PyQt5.QtWidgets import QInputDialog + from PySide6.QtWidgets import QInputDialog pick, ok = QInputDialog.getItem( self,