"""Main QtPyVCP Application Module
Contains the VCPApplication class with core function and VCP loading logic.
"""
import os
import sys
import imp
import inspect
from pkg_resources import iter_entry_points
from qtpy import API
from qtpy.QtGui import QFontDatabase
from qtpy.QtCore import QTimer, Slot, Qt
from qtpy.QtWidgets import QApplication, QStyleFactory
import qtpyvcp
from qtpyvcp.utilities.logger import initBaseLogger
from qtpyvcp.plugins import initialisePlugins, terminatePlugins, getPlugin
from qtpyvcp.widgets.base_widgets.base_widget import VCPPrimitiveWidget
from qtpyvcp.widgets.form_widgets.main_window import VCPMainWindow
# initialize logging. If a base logger was already initialized in a startup
# script (e.g. vcp_launcher.py), then that logger will be returned, otherwise
# this will initialise a base logger with default log level of DEBUG
LOG = initBaseLogger('qtpyvcp')
# Needed to silence this PySide2 warning:
# Qt WebEngine seems to be initialized from a plugin. Please set
# Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute
# before constructing QGuiApplication.
if API == 'pyside2':
from qtpy.QtCore import Qt
QApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
[docs]class VCPApplication(QApplication):
def __init__(self, theme=None, stylesheet=None, custom_fonts=[]):
app_args = (qtpyvcp.OPTIONS.command_line_args or "").split()
super(VCPApplication, self).__init__(app_args)
opts = qtpyvcp.OPTIONS
self.status = getPlugin('status')
# initialize plugins
initialisePlugins()
theme = opts.theme or theme
if theme is not None:
self.setStyle(QStyleFactory.create(theme))
stylesheet = opts.stylesheet or stylesheet
if stylesheet is not None:
self.loadStylesheet(stylesheet, opts.develop)
if custom_fonts:
if isinstance(custom_fonts, str): # single font or location
self.loadCustomFont(custom_fonts)
else: # list of fonts or locations
for font in custom_fonts:
self.loadCustomFont(font)
# self.window = self.loadVCPMainWindow(opts, vcp_file)
# if self.window is not None:
# self.window.show()
if opts.hide_cursor:
from qtpy.QtGui import QCursor
self.setOverrideCursor(QCursor(Qt.BlankCursor))
# Performance monitoring
if opts.perfmon:
import psutil
self.perf = psutil.Process()
self.perf_timer = QTimer()
self.perf_timer.setInterval(2000)
self.perf_timer.timeout.connect(self.logPerformance)
self.perf_timer.start()
self.aboutToQuit.connect(self.terminate)
[docs] def loadVCPMainWindow(self, opts, vcp_file=None):
"""
Loads a VCPMainWindow instance defined by a Qt .ui file, a Python .py
file, or from a VCP python package.
Parameters
----------
vcp_file : str
The path or name of the VCP to load.
opts : OptDict
A OptDict of options to pass to the VCPMainWindow subclass.
Returns
-------
VCPMainWindow instance
"""
vcp = opts.vcp or vcp_file
if vcp is None:
return
if os.path.exists(vcp):
vcp_path = os.path.realpath(vcp)
if os.path.isfile(vcp_path):
directory, filename = os.path.split(vcp_path)
name, ext = os.path.splitext(filename)
if ext == '.ui':
LOG.info("Loading VCP from UI file: yellow<{}>".format(vcp))
return VCPMainWindow(opts=opts, ui_file=vcp_path)
elif ext == '.py':
LOG.info("Loading VCP from PY file: yellow<{}>".format(vcp))
return self.loadPyFile(vcp_path, opts)
elif os.path.isdir(vcp_path):
LOG.info("VCP is a directory")
# TODO: Load from a directory if it has a __main__.py entry point
else:
try:
entry_points = {}
for entry_point in iter_entry_points(group='qtpyvcp.example_vcp'):
entry_points[entry_point.name] = entry_point
for entry_point in iter_entry_points(group='qtpyvcp.vcp'):
entry_points[entry_point.name] = entry_point
window = entry_points[vcp.lower()].load()
return window(opts=opts)
except:
LOG.exception("Failed to load entry point")
LOG.critical("VCP could not be loaded: yellow<{}>".format(vcp))
sys.exit()
[docs] def loadPyFile(self, pyfile, opts):
"""
Load a .py file, performs some sanity checks to try and determine
if the file actually contains a valid VCPMainWindow subclass, and if
the checks pass, create and return an instance.
This is an internal method, users will usually want to use `loadVCP` instead.
Parameters
----------
pyfile : str
The path to a .py file to load.
opts : OptDict
A OptDict of options to pass to the VCPMainWindow subclass.
Returns
-------
VCPMainWindow instance
"""
# Add the pyfile module directory to the python path, so that submodules can be loaded
module_dir = os.path.dirname(os.path.abspath(pyfile))
sys.path.append(module_dir)
# Load the module. It's attributes can be accessed via `python_vcp.attr`
module = imp.load_source('python_vcp', pyfile)
classes = [obj for name, obj in inspect.getmembers(module)
if inspect.isclass(obj)
and issubclass(obj, VCPMainWindow)
and obj != VCPMainWindow]
if len(classes) == 0:
raise ValueError("Invalid File Format."
" {} has no class inheriting from VCPMainWindow.".format(pyfile))
if len(classes) > 1:
LOG.warn("More than one VCPMainWindow class in file yellow<{}>."
" The first occurrence (in alphabetical order) will be used: {}"
.format(pyfile, classes[0].__name__))
cls = classes[0]
# initialize and return the VCPMainWindow subclass
return cls(opts=opts)
[docs] def loadStylesheet(self, stylesheet, watch=False):
"""Loads a QSS stylesheet file containing styles to be applied
to specific Qt and/or QtPyVCP widget classes.
Args:
stylesheet (str) : Path to the .qss stylesheet to load.
watch (bool) : Whether to watch and re-load on .qss file changes.
"""
def load(path):
LOG.info("Loading global stylesheet: yellow<{}>".format(stylesheet))
self.setStyleSheet("file:///" + path)
if watch:
from qtpy.QtCore import QFileSystemWatcher
self.qss_file_watcher = QFileSystemWatcher()
self.qss_file_watcher.addPath(stylesheet)
self.qss_file_watcher.fileChanged.connect(load)
load(stylesheet)
[docs] def loadCustomFont(self, font):
"""Loads custom front from a file or directory."""
if os.path.isfile(font) and os.path.splitext(font)[1] in ['.ttf', '.otf', '.woff', '.woff2']:
self.addApplicationFont(font)
elif os.path.isdir(font):
for ffile in os.listdir(font):
fpath = os.path.join(font, ffile)
self.loadCustomFont(fpath)
[docs] def addApplicationFont(self, font_path):
"""Loads a font file into the font database. The path can specify the
location of a font file or a qresource."""
LOG.debug("Loading custom font: %s" % font_path)
res = QFontDatabase.addApplicationFont(font_path)
# per QT docs -1 is error and 0+ is index to font loaded for later use
if res < 0:
LOG.error("Failed to load font: %s", font_path)
[docs] def getWindow(self, name):
"""Searches for a window by name in the application.
Args:
name (str) : ObjectName of the window.
Returns: VCPMainWindow
"""
for win_name, window in qtpyvcp.WINDOWS.items():
if win_name == name:
return window
raise AttributeError("Could not find window with name: %s" % name)
def terminate(self):
self.terminateWidgets()
terminatePlugins()
def initialiseWidgets(self):
for w in self.allWidgets():
if isinstance(w, VCPPrimitiveWidget):
w.initialize()
def postInitialiseWidgets(self):
for w in self.allWidgets():
if isinstance(w, VCPPrimitiveWidget):
if (hasattr(w, 'postInitialize') and callable(w.postInitialize)):
w.postInitialize()
def terminateWidgets(self):
LOG.debug("Terminating widgets")
for w in self.allWidgets():
if isinstance(w, VCPPrimitiveWidget):
try:
w.terminate()
except Exception:
LOG.exception('Error terminating %s widget', w)