239 lines
8.4 KiB
Python
239 lines
8.4 KiB
Python
"""
|
|
WebAppify
|
|
=========
|
|
|
|
WebAppify is a simple module to easily create your own desktop apps of websites. WebAppify uses PySide6 and
|
|
QtWebEngine for displaying the web page, and works on Python 3.10 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.
|
|
"""
|
|
import logging
|
|
import sys
|
|
import platform
|
|
|
|
from PySide6 import QtCore, QtGui, QtWidgets, QtWebEngineCore, QtWebEngineWidgets
|
|
|
|
SETTINGS = [
|
|
QtWebEngineCore.QWebEngineSettings.PluginsEnabled,
|
|
QtWebEngineCore.QWebEngineSettings.JavascriptCanAccessClipboard,
|
|
QtWebEngineCore.QWebEngineSettings.LocalContentCanAccessRemoteUrls
|
|
]
|
|
LOG_LEVELS = {
|
|
QtWebEngineCore.QWebEnginePage.InfoMessageLevel: logging.INFO,
|
|
QtWebEngineCore.QWebEnginePage.WarningMessageLevel: logging.WARNING,
|
|
QtWebEngineCore.QWebEnginePage.ErrorMessageLevel: logging.ERROR
|
|
}
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class WebPage(QtWebEngineCore.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}')
|
|
|
|
|
|
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 <b>Quit</b> 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()
|