From 1471b7340395bba570383e37a792369de6e25879 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 10 Jun 2022 11:12:04 -0700 Subject: [PATCH] Update WebAppify to the latest tech - Migrate to Hatch - Migrate to Woodpecker CI - Migrate to only Python 3 - Add .editorconfig - Expand .gitignore --- .drone.yml | 10 -- .editorconfig | 8 ++ setup.cfg => .flake8 | 3 - .gitignore | 5 +- .woodpecker.yml | 23 ++++ pyproject.toml | 57 +++++++++ setup.py | 45 ------- webappify.py | 273 ------------------------------------------ webappify/__init__.py | 240 +++++++++++++++++++++++++++++++++++++ 9 files changed, 332 insertions(+), 332 deletions(-) delete mode 100644 .drone.yml create mode 100644 .editorconfig rename setup.cfg => .flake8 (57%) create mode 100644 .woodpecker.yml create mode 100644 pyproject.toml delete mode 100644 setup.py delete mode 100644 webappify.py create mode 100644 webappify/__init__.py diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 76eb74a..0000000 --- a/.drone.yml +++ /dev/null @@ -1,10 +0,0 @@ -kind: pipeline -name: default - -steps: -- name: test - image: python:3 - commands: - - pip install -e . - - pip install pytest - - pytest diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..02b4883 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +max_line_length = 120 + +[*.py] +indent_style = space +indent_size = 4 diff --git a/setup.cfg b/.flake8 similarity index 57% rename from setup.cfg rename to .flake8 index 96a01b4..6deafc2 100644 --- a/setup.cfg +++ b/.flake8 @@ -1,5 +1,2 @@ -[wheel] -universal = 1 - [flake8] max-line-length = 120 diff --git a/.gitignore b/.gitignore index 3a34b64..61d5f42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ -dist *.egg-info +dist build +.venv +__pycache__ +*.py[co] diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..e5fa7c6 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,23 @@ +pipeline: + # tests: + # image: python:3 + # commands: + # - pip install -U pip wheel pytest PyQt5 PyQtWebEngine + # - pytest + # when: + # branch: master + build: + image: python:3 + commands: + - pip install -U pip wheel hatch hatch-vcs + - hatch build + when: + branch: master + release: + image: python:3 + commands: + - pip install -U pip wheel hatch hatch-vcs + - hatch build + - hatch publish + when: + event: tag diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f9b2aad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +requires = [ + "hatchling>=1.3.1", + "hatch-vcs" +] +build-backend = "hatchling.build" + +[project] +name = "WebAppify" +description = "Create desktop apps of your favourite websites" +readme = "README.rst" +license = "MIT" +authors = [ + { name = "Raoul Snyman", email = "raoul@snyman.info" }, +] +keywords = [ + "Qt", + "website", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: MacOS X", + "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications", + "Environment :: X11 Applications :: Qt", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Desktop Environment", + "Topic :: Internet :: WWW/HTTP :: Browsers", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "PyQt5", + "PyQtWebEngine", +] +dynamic = [ + "version", +] + +[project.urls] +Homepage = "https://git.snyman.info/raoul/webappify" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build] +include = [ + "/webappify", +] + +[tool.hatch.build.targets.sdist] +[tool.hatch.build.targets.wheel] diff --git a/setup.py b/setup.py deleted file mode 100644 index c045526..0000000 --- a/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -The webappify package -""" -import os -from codecs import open -from setuptools import setup - -HERE = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(HERE, 'README.rst'), encoding='utf8') as f: - LONG_DESCRIPTION = f.read() - - -setup( - name='WebAppify', - version='0.3', - description='Create desktop apps of your favourite websites', - long_description=LONG_DESCRIPTION, - url='https://launchpad.net/webappify', - author='Raoul Snyman', - author_email='raoul@snyman.info', - license='MIT', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: MacOS X', - 'Environment :: Win32 (MS Windows)', - 'Environment :: X11 Applications', - 'Environment :: X11 Applications :: Qt', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Desktop Environment', - 'Topic :: Internet :: WWW/HTTP :: Browsers', - 'Topic :: Software Development :: Libraries :: Python Modules' - ], - keywords='Qt website', - py_modules=['webappify'], - install_requires=['PyQt5', 'PyQtWebEngine'] -) diff --git a/webappify.py b/webappify.py deleted file mode 100644 index 5ef49d8..0000000 --- a/webappify.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -WebAppify -========= - -WebAppify is a simple module to easily create your own desktop apps of websites. WebAppify uses PyQt5 and QtWebKit or -QtWebEngine for displaying the web page, and works on Python 2.7 and Python 3.4 and up. - -To create your own desktop web app, import and set up the WebApp class. - -.. code:: python - - from webappify import WebApp - - app = WebApp('OpenStreetMap', 'https://www.openstreetmap.org', 'osm.png') - app.run() - -This will create a window with the website, using the icon provided. - -.. note:: - - If your site needs Flash Player, you'll need the appropriate Flash Player plugin installed system-wide. For QtWebKit - you will need the NPAPI plugin, and for QtWebEngine you will need the PPAPI plugin. -""" -import sys -import platform - -from PyQt5 import QtCore, QtGui, QtWidgets - -IS_PY2 = sys.version_info[0] == 2 - -try: - from PyQt5 import QtWebEngineWidgets - HAS_WEBENGINE = True -except ImportError: - HAS_WEBENGINE = False - -try: - from PyQt5 import QtWebKit, QtWebKitWidgets - HAS_WEBKIT = True -except ImportError: - HAS_WEBKIT = False - -if HAS_WEBENGINE: - SETTINGS = [ - QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, - QtWebEngineWidgets.QWebEngineSettings.JavascriptCanAccessClipboard, - QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessRemoteUrls - ] - WebView = QtWebEngineWidgets.QWebEngineView -elif HAS_WEBKIT: - SETTINGS = [ - QtWebKit.QWebSettings.PluginsEnabled, - QtWebKit.QWebSettings.JavascriptCanOpenWindows, - QtWebKit.QWebSettings.JavascriptCanCloseWindows, - QtWebKit.QWebSettings.JavascriptCanAccessClipboard, - QtWebKit.QWebSettings.OfflineStorageDatabaseEnabled, - QtWebKit.QWebSettings.OfflineWebApplicationCacheEnabled, - QtWebKit.QWebSettings.LocalStorageEnabled, - QtWebKit.QWebSettings.LocalContentCanAccessRemoteUrls - ] - WebView = QtWebKitWidgets.QWebView - - class WebPage(QtWebKitWidgets.QWebPage): - """Custom class for overriding the user agent to make WebKit look like Chrome""" - def userAgentForUrl(self, url): - return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' \ - 'Chrome/28.0.1500.52 Safari/537.36' -else: - print('Cannot detect either QtWebEngine or QtWebKit!') - sys.exit(1) - - -class WebWindow(QtWidgets.QWidget): - """ - A window with a single web view and nothing else - """ - def __init__(self, app, title, url, icon, canMinimizeToTray=False): - """ - Create the window - """ - super(WebWindow, self).__init__(None) - self.hasShownWarning = False - self.app = app - self.icon = QtGui.QIcon(icon) - self.canMinimizeToTray = canMinimizeToTray - self.setWindowTitle(title) - self.setWindowIcon(self.icon) - self.setContentsMargins(0, 0, 0, 0) - self.layout = QtWidgets.QVBoxLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - self.webview = WebView(self) - if not HAS_WEBENGINE and HAS_WEBKIT: - self.webview.setPage(WebPage(self.webview)) - for setting in SETTINGS: - self.webview.settings().setAttribute(setting, True) - self.webview.setUrl(QtCore.QUrl(url)) - self.layout.addWidget(self.webview) - self.webview.titleChanged.connect(self.onTitleChanged) - - def _showWarning(self): - """ - Show a balloon message to inform the user that the app is minimized - """ - if not self.hasShownWarning: - self.trayIcon.showMessage(self.windowTitle(), 'This program will continue running in the system tray. ' - 'To close the program, choose Quit in the context menu of the system ' - 'tray icon.', QtWidgets.QSystemTrayIcon.Information, 5000) - self.hasShownWarning = True - - def _updateTrayMenu(self): - """ - Update the enabled/disabled status of the items in the tray icon menu - """ - if not self.canMinimizeToTray: - return - self.restoreAction.setEnabled(not self.isVisible()) - self.minimizeAction.setEnabled(self.isVisible() and not self.isMinimized()) - self.maximizeAction.setEnabled(self.isVisible() and not self.isMaximized()) - - def _raiseWindow(self): - """ - Raise the Window, depending on the version of Python - """ - # Get the "raise" method depending on Python 2 or 3 - if IS_PY2: - raiser = getattr(self, 'raise_') - else: - raiser = getattr(self, 'raise') - raiser() - - def _restoreWindow(self): - """ - Restore the window and activate it - """ - self.showNormal() - self.activateWindow() - self._raiseWindow() - - def _maximizeWindow(self): - """ - Restore the window and activate it - """ - self.showMaximized() - self.activateWindow() - self._raiseWindow() - - def _getTrayMenu(self): - """ - Create and return the menu for the tray icon - """ - # Create the actions for the menu - self.restoreAction = QtWidgets.QAction('&Restore', self) - self.restoreAction.triggered.connect(self._restoreWindow) - self.minimizeAction = QtWidgets.QAction('Mi&nimize', self) - self.minimizeAction.triggered.connect(self.close) - self.maximizeAction = QtWidgets.QAction('Ma&ximize', self) - self.maximizeAction.triggered.connect(self._maximizeWindow) - self.quitAction = QtWidgets.QAction('&Quit', self) - self.quitAction.triggered.connect(self.app.quit) - # Create the menu and add the actions - trayIconMenu = QtWidgets.QMenu(self) - trayIconMenu.addAction(self.restoreAction) - trayIconMenu.addAction(self.minimizeAction) - trayIconMenu.addAction(self.maximizeAction) - trayIconMenu.addSeparator() - trayIconMenu.addAction(self.quitAction) - return trayIconMenu - - def setupTrayIcon(self): - """ - Set up the tray icon - """ - self.trayIcon = QtWidgets.QSystemTrayIcon(self.icon, self) - self.trayIcon.setContextMenu(self._getTrayMenu()) - self.trayIcon.activated.connect(self.onTrayIconActivated) - self.trayIcon.show() - - def closeEvent(self, event): - """ - Override the close event to minimize to the tray - """ - # If we don't want to minimize to the tray, just close the window as per usual - if not self.canMinimizeToTray: - super(WebWindow, self).closeEvent(event) - return - # If we want to minimize to the tray, then just hide the window - if platform.platform().lower() == 'darwin' and (not event.spontaneous() or not self.isVisible()): - return - else: - self._showWarning() - self.hide() - event.ignore() - # Update the menu to match - self._updateTrayMenu() - - def showEvent(self, event): - """ - Override the show event to catch max/min/etc events and update the tray icon menu accordingly - """ - super(WebWindow, self).showEvent(event) - self._updateTrayMenu() - - def hideEvent(self, event): - """ - Override the hide event to catch max/min/etc events and update the tray icon menu accordingly - """ - super(WebWindow, self).hideEvent(event) - self._updateTrayMenu() - - def changeEvent(self, event): - """ - Catch the minimize event and close the form - """ - if self.canMinimizeToTray: - if event.type() == QtCore.QEvent.WindowStateChange and self.windowState() & QtCore.Qt.WindowMinimized: - self.close() - super(WebWindow, self).changeEvent(event) - - def onTitleChanged(self, title): - """ - React to title changes - """ - if title: - self.setWindowTitle(title) - if self.canMinimizeToTray: - self.trayIcon.setToolTip(title) - - def onTrayIconActivated(self, reason): - """ - React to the tray icon being activated - """ - if reason == QtWidgets.QSystemTrayIcon.Trigger: - if self.isVisible(): - self.close() - else: - self.showNormal() - - -class WebApp(QtWidgets.QApplication): - """ - A generic application to open a web page in a desktop app - """ - def __init__(self, title, url, icon, canMinimizeToTray=False): - """ - Create an application which loads a URL into a window - """ - super(WebApp, self).__init__(sys.argv) - self.window = None - self.trayIcon = None - self.title = title - self.url = url - self.icon = icon - self.canMinimizeToTray = QtWidgets.QSystemTrayIcon.isSystemTrayAvailable() and canMinimizeToTray - if self.canMinimizeToTray: - self.setQuitOnLastWindowClosed(False) - self.setWindowIcon(QtGui.QIcon(self.icon)) - self.setApplicationName(title) - self.setApplicationDisplayName(title) - - def run(self): - """ - Set up the window and the tray icon, and run the app - """ - self.window = WebWindow(self, self.title, self.url, self.icon, self.canMinimizeToTray) - if self.canMinimizeToTray: - self.window.setupTrayIcon() - self.window.showMaximized() - # Get the "exec" method depending on Python 2 or 3 - if IS_PY2: - runner = getattr(self, 'exec_') - else: - runner = getattr(self, 'exec') - return runner() diff --git a/webappify/__init__.py b/webappify/__init__.py new file mode 100644 index 0000000..49d0569 --- /dev/null +++ b/webappify/__init__.py @@ -0,0 +1,240 @@ +""" +WebAppify +========= + +WebAppify is a simple module to easily create your own desktop apps of websites. WebAppify uses PyQt5 and QtWebKit or +QtWebEngine for displaying the web page, and works on Python 2.7 and Python 3.4 and up. + +To create your own desktop web app, import and set up the WebApp class. + +.. code:: python + + from webappify import WebApp + + app = WebApp('OpenStreetMap', 'https://www.openstreetmap.org', 'osm.png') + app.run() + +This will create a window with the website, using the icon provided. + +.. note:: + + If your site needs Flash Player, you'll need the appropriate Flash Player plugin installed system-wide. For QtWebKit + you will need the NPAPI plugin, and for QtWebEngine you will need the PPAPI plugin. +""" +import logging +import sys +import platform + +from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets + +SETTINGS = [ + QtWebEngineWidgets.QWebEngineSettings.PluginsEnabled, + QtWebEngineWidgets.QWebEngineSettings.JavascriptCanAccessClipboard, + QtWebEngineWidgets.QWebEngineSettings.LocalContentCanAccessRemoteUrls +] +LOG_LEVELS = { + QtWebEngineWidgets.QWebEnginePage.InfoMessageLevel: logging.INFO, + QtWebEngineWidgets.QWebEnginePage.WarningMessageLevel: logging.WARNING, + QtWebEngineWidgets.QWebEnginePage.ErrorMessageLevel: logging.ERROR +} + +log = logging.getLogger(__name__) + + +class WebPage(QtWebEngineWidgets.QWebEnginePage): + """ + A custom QWebEnginePage which logs JS console messages to the Python logging system + """ + def javaScriptConsoleMessage(self, level, message, line_number, source_id): + """ + Custom logger to log console messages to the Python logging system + """ + log.log(LOG_LEVELS[level], f'{source_id}:{line_number} {message}') + print(message) + + +class WebWindow(QtWidgets.QWidget): + """ + A window with a single web view and nothing else + """ + def __init__(self, app, title, url, icon, can_minimize_to_tray=False, canMinimizeToTray=False): + """ + Create the window + """ + super(WebWindow, self).__init__(None) + self._has_shown_warning = False + self.app = app + self.icon = QtGui.QIcon(icon) + self.can_minimize_to_tray = can_minimize_to_tray or canMinimizeToTray + self.setWindowTitle(title) + self.setWindowIcon(self.icon) + self.setContentsMargins(0, 0, 0, 0) + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + self.webview = QtWebEngineWidgets.QWebEngineView(self) + self.webview.setPage(WebPage(self.webview)) + for setting in SETTINGS: + self.webview.settings().setAttribute(setting, True) + self.webview.setUrl(QtCore.QUrl(url)) + self.layout.addWidget(self.webview) + self.webview.titleChanged.connect(self.on_title_changed) + + def _show_warning(self): + """ + Show a balloon message to inform the user that the app is minimized + """ + if not self._has_shown_warning: + self.tray_icon.showMessage(self.windowTitle(), 'This program will continue running in the system tray. ' + 'To close the program, choose Quit in the context menu of the system ' + 'tray icon.', QtWidgets.QSystemTrayIcon.Information, 5000) + self._has_shown_warning = True + + def _update_tray_menu(self): + """ + Update the enabled/disabled status of the items in the tray icon menu + """ + if not self.can_minimize_to_tray: + return + self.restore_action.setEnabled(not self.isVisible()) + self.minimize_action.setEnabled(self.isVisible() and not self.isMinimized()) + self.maximize_action.setEnabled(self.isVisible() and not self.isMaximized()) + + def _restore_window(self): + """ + Restore the window and activate it + """ + self.showNormal() + self.activateWindow() + self.raise_() + + def _maximize_window(self): + """ + Restore the window and activate it + """ + self.showMaximized() + self.activateWindow() + self.raise_() + + def _get_tray_menu(self): + """ + Create and return the menu for the tray icon + """ + # Create the actions for the menu + self.restore_action = QtWidgets.QAction('&Restore', self) + self.restore_action.triggered.connect(self._restore_window) + self.minimize_action = QtWidgets.QAction('Mi&nimize', self) + self.minimize_action.triggered.connect(self.close) + self.maximize_action = QtWidgets.QAction('Ma&ximize', self) + self.maximize_action.triggered.connect(self._maximize_window) + self.quit_action = QtWidgets.QAction('&Quit', self) + self.quit_action.triggered.connect(self.app.quit) + # Create the menu and add the actions + tray_icon_menu = QtWidgets.QMenu(self) + tray_icon_menu.addAction(self.restore_action) + tray_icon_menu.addAction(self.minimize_action) + tray_icon_menu.addAction(self.maximize_action) + tray_icon_menu.addSeparator() + tray_icon_menu.addAction(self.quit_action) + return tray_icon_menu + + def setup_tray_icon(self): + """ + Set up the tray icon + """ + self.tray_icon = QtWidgets.QSystemTrayIcon(self.icon, self) + self.tray_icon.setContextMenu(self._get_tray_menu()) + self.tray_icon.activated.connect(self.on_tray_icon_activated) + self.tray_icon.show() + + def closeEvent(self, event): + """ + Override the close event to minimize to the tray + """ + # If we don't want to minimize to the tray, just close the window as per usual + if not self.can_minimize_to_tray: + super(WebWindow, self).closeEvent(event) + return + # If we want to minimize to the tray, then just hide the window + if platform.platform().lower() == 'darwin' and (not event.spontaneous() or not self.isVisible()): + return + else: + self._show_warning() + self.hide() + event.ignore() + # Update the menu to match + self._update_tray_menu() + + def showEvent(self, event): + """ + Override the show event to catch max/min/etc events and update the tray icon menu accordingly + """ + super(WebWindow, self).showEvent(event) + self._update_tray_menu() + + def hideEvent(self, event): + """ + Override the hide event to catch max/min/etc events and update the tray icon menu accordingly + """ + super(WebWindow, self).hideEvent(event) + self._update_tray_menu() + + def changeEvent(self, event): + """ + Catch the minimize event and close the form + """ + if self.can_minimize_to_tray: + if event.type() == QtCore.QEvent.WindowStateChange and self.windowState() & QtCore.Qt.WindowMinimized: + self.close() + super(WebWindow, self).changeEvent(event) + + def on_title_changed(self, title): + """ + React to title changes + """ + if title: + self.setWindowTitle(title) + if self.can_minimize_to_tray: + self.tray_icon.setToolTip(title) + + def on_tray_icon_activated(self, reason): + """ + React to the tray icon being activated + """ + if reason == QtWidgets.QSystemTrayIcon.Trigger: + if self.isVisible(): + self.close() + else: + self.showNormal() + + +class WebApp(QtWidgets.QApplication): + """ + A generic application to open a web page in a desktop app + """ + def __init__(self, title, url, icon, can_minimize_to_tray=False, canMinimizeToTray=False): + """ + Create an application which loads a URL into a window + """ + super(WebApp, self).__init__(sys.argv) + self.window = None + self.tray_icon = None + self.title = title + self.url = url + self.icon = icon + self.can_minimize_to_tray = QtWidgets.QSystemTrayIcon.isSystemTrayAvailable() and \ + (can_minimize_to_tray or canMinimizeToTray) + if self.can_minimize_to_tray: + self.setQuitOnLastWindowClosed(False) + self.setWindowIcon(QtGui.QIcon(self.icon)) + self.setApplicationName(title) + self.setApplicationDisplayName(title) + + def run(self): + """ + Set up the window and the tray icon, and run the app + """ + self.window = WebWindow(self, self.title, self.url, self.icon, self.can_minimize_to_tray) + if self.can_minimize_to_tray: + self.window.setup_tray_icon() + self.window.showMaximized() + return self.exec()