From 4a2776317182f566bd48e324eebb1b06bf69cecc Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 13 Jun 2017 15:46:41 -0700 Subject: [PATCH] Version 0.2: Add minimize to tray --- .bzrignore | 1 + CHANGELOG.rst | 18 ++++ README.rst | 20 +++- setup.cfg | 5 + setup.py | 8 +- webappify.py | 241 ++++++++++++++++++++++++++++++++++++++++++ webappify/__init__.py | 142 ------------------------- 7 files changed, 289 insertions(+), 146 deletions(-) create mode 100644 CHANGELOG.rst create mode 100644 webappify.py delete mode 100644 webappify/__init__.py diff --git a/.bzrignore b/.bzrignore index 8a09f29..3a34b64 100644 --- a/.bzrignore +++ b/.bzrignore @@ -1,2 +1,3 @@ dist *.egg-info +build diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..8ccfff3 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,18 @@ +Changelog +========= + +Version 0.2 +----------- + +Released: 2017-06-13 + +- Added the ability tpo minimize to a system tray icon +- Moved the module into a file instead of a directory +- Updated setup.py for the module move + +Version 0.1 +----------- + +Released: 2017-05-17 + +- Initial version diff --git a/README.rst b/README.rst index 2b34e50..5546b57 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,8 @@ WebAppify ========= -WebAppify is a simple module to easily create your own desktop apps of websites. +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. @@ -13,3 +14,20 @@ To create your own desktop web app, import and set up the WebApp class. app.run() This will create a window with the website, using the icon provided. + +Additional Options +------------------ + +Version 0.2 comes with the option of minimizing to the system tray. Simply pass ``canMinimizeToTray=True`` to the class +and a tray icon will be installed with the necessary menu options. + +.. code:: python + + app = WebApp('OpenStreetMap', 'https://www.openstreetmap.org', 'osm.png', canMinimizeToTray=True) + +Clicking on the tray icon will show the window, while right-clicking will show the menu. + +.. 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. diff --git a/setup.cfg b/setup.cfg index e69de29..96a01b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -0,0 +1,5 @@ +[wheel] +universal = 1 + +[flake8] +max-line-length = 120 diff --git a/setup.py b/setup.py index 07ed206..7bd1cca 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ The webappify package """ import os from codecs import open -from setuptools import setup, find_packages +from setuptools import setup HERE = os.path.abspath(os.path.dirname(__file__)) @@ -13,7 +13,7 @@ with open(os.path.join(HERE, 'README.rst'), encoding='utf8') as f: setup( name='WebAppify', - version='0.1', + version='0.2', description='Create desktop apps of your favourite websites', long_description=LONG_DESCRIPTION, url='https://launchpad.net/webappify', @@ -29,6 +29,8 @@ setup( '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', @@ -38,6 +40,6 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules' ], keywords='Qt website', - packages=find_packages(), + py_modules=['webappify'], install_requires=['PyQt5'] ) diff --git a/webappify.py b/webappify.py new file mode 100644 index 0000000..61dbb0e --- /dev/null +++ b/webappify.py @@ -0,0 +1,241 @@ +""" +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 + +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 = icon + self.canMinimizeToTray = canMinimizeToTray + self.setWindowTitle(title) + self.setWindowIcon(QtGui.QIcon(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.') + 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 _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.showNormal) + 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.showMaximized) + 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(QtGui.QIcon(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: + 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 sys.version_info[0] == 2: + runner = getattr(self, 'exec_') + else: + runner = getattr(self, 'exec') + return runner() diff --git a/webappify/__init__.py b/webappify/__init__.py deleted file mode 100644 index 9ba6787..0000000 --- a/webappify/__init__.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -The :mod:`webapp` module contains a WebApp class which can be used to create simple "app windows" -for websites. To use it, do this:: - - from webapp import WebApp - - app = WebApp('GMail', 'https://mail.google.com', 'gmail.png') - app.run() - -.. note: - - If your site needs Flash Player, you'll need the appropriate Flash Player plugin installed - system-wide. For WebKit, this is the NPAPI plugin, and for WebEngine, this is the PPAPI plugin. - -""" -import sys - -from PyQt5 import QtCore, QtGui, QtWidgets - -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.AutoLoadImages, - QtWebKit.QWebSettings.JavascriptEnabled, - QtWebKit.QWebSettings.JavaEnabled, - QtWebKit.QWebSettings.PluginsEnabled, - QtWebKit.QWebSettings.JavascriptCanOpenWindows, - QtWebKit.QWebSettings.JavascriptCanCloseWindows, - QtWebKit.QWebSettings.JavascriptCanAccessClipboard, - QtWebKit.QWebSettings.DeveloperExtrasEnabled, - QtWebKit.QWebSettings.OfflineStorageDatabaseEnabled, - QtWebKit.QWebSettings.OfflineWebApplicationCacheEnabled, - QtWebKit.QWebSettings.LocalStorageEnabled, - QtWebKit.QWebSettings.LocalContentCanAccessRemoteUrls, - QtWebKit.QWebSettings.LocalContentCanAccessFileUrls, - QtWebKit.QWebSettings.AcceleratedCompositingEnabled - ] - 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): - """ - Window - """ - def __init__(self, title, url, icon, parent=None): - """ - Create the window - """ - super().__init__(parent) - self.setWindowTitle(title) - self.setWindowIcon(QtGui.QIcon(icon)) - self.setContentsMargins(0, 0, 0, 0) - self.layout = QtWidgets.QVBoxLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) - self.webview = WebView(self) - if HAS_WEBKIT: - self.webview.setPage(WebPage(self.webview)) - for setting in SETTINGS: - self.webview.settings().setAttribute(setting, True) - self.webview.titleChanged.connect(self.onTitleChanged) - self.webview.setUrl(QtCore.QUrl(url)) - self.layout.addWidget(self.webview) - - def onTitleChanged(self, title): - """ - React to title changes - """ - if title: - self.setWindowTitle(title) - - -class WebApp(QtWidgets.QApplication): - """ - A generic application to open a web page in a desktop app - """ - def __init__(self, title, url, icon, hasTray=False, canMinimizeToTray=False): - """ - Create an application which loads a URL into a window - """ - super().__init__(sys.argv) - self.beforeHooks = [] - self.afterHooks = [] - self.window = None - self.title = title - self.url = url - self.icon = icon - self.hasTray = hasTray - self.canMinimizeToTray = canMinimizeToTray - self.setWindowIcon(QtGui.QIcon(self.icon)) - - def setupTrayIcon(self): - """ - Set up the tray icon - """ - if not QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): - # No reason to continue if the OS doesn't support system tray icons - return - self.trayIcon = QtWidgets.QSystemTrayIcon(QtGui.QIcon(self.icon), self.window) - self.trayIcon.show() - - def addBeforeHook(self, hook): - """ - Add a function to run before setting everything up - """ - if hook: - self.beforeHooks.append(hook) - - def run(self): - """ - Run the app - """ - self.window = WebWindow(self.title, self.url, self.icon) - if self.hasTray: - self.setupTrayIcon() - self.window.showMaximized() - return self.exec()