"""Main QtPyVCP Application Module
Contains the VCPApplication class with core function and VCP loading logic.
"""
import os
import sys
import importlib as imp
import inspect
from importlib.metadata import entry_points
from PySide6.QtGui import QFontDatabase
from PySide6.QtCore import QTimer, Slot, Qt
from PySide6.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')
[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 PySide6.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_point_data = {}
for entry_point in entry_points(group='qtpyvcp.example_vcp'):
entry_point_data[entry_point.name] = entry_point
for entry_point in entry_points(group='qtpyvcp.vcp'):
entry_point_data[entry_point.name] = entry_point
window = entry_point_data[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 = importlib.import_module(name="python_vcp", package=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))
# Read the actual file contents instead of using file:/// reference
try:
with open(path, 'r', encoding='utf-8') as f:
qss_content = f.read()
self.setStyleSheet(qss_content)
except Exception as e:
LOG.error(f"Failed to load stylesheet {path}: {e}")
# Fallback to file:/// method (works at runtime but not in Designer)
self.setStyleSheet("file:///" + path)
if watch:
from PySide6.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)
return
[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)