""" 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()