568 lines
24 KiB
Python
568 lines
24 KiB
Python
import os
|
|
import shutil
|
|
import sys
|
|
import subprocess
|
|
|
|
from qtpy import QtGui, QtCore, QtWidgets, uic
|
|
import traceback
|
|
from qtvcp.widgets.widget_baseclass import _HalWidgetBase
|
|
# Set up logging
|
|
from . import logger
|
|
|
|
log = logger.getLogger(__name__)
|
|
|
|
# Force the log level for this module
|
|
#log.setLevel(logger.DEBUG) # One of DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
|
|
# PyQt6 raises KeyError when a .ui file signal connection references an overloaded
|
|
# signal type that no longer exists (e.g. 'bool' subscript on a non-overloaded signal).
|
|
# Patch _handle_connections to skip bad connections rather than aborting the whole load.
|
|
try:
|
|
from PyQt6.uic import uiparser as _uiparser
|
|
from PyQt6 import QtCore as _QtCore
|
|
|
|
def _tolerant_handle_connections(self, el):
|
|
def name2object(obj):
|
|
if obj == self.uiname:
|
|
return self.toplevelWidget
|
|
else:
|
|
return getattr(self.toplevelWidget, obj)
|
|
|
|
for conn in el:
|
|
signal = conn.findtext('signal')
|
|
signal_name, signal_args = signal.split('(')
|
|
signal_args = signal_args[:-1].replace(' ', '')
|
|
sender = name2object(conn.findtext('sender'))
|
|
bound_signal = getattr(sender, signal_name)
|
|
slot = self.factory.getSlot(name2object(conn.findtext('receiver')),
|
|
conn.findtext('slot').split('(')[0])
|
|
try:
|
|
if signal_args == '':
|
|
bound_signal.connect(slot)
|
|
else:
|
|
signal_args_list = signal_args.split(',')
|
|
if len(signal_args_list) == 1:
|
|
bound_signal[signal_args_list[0]].connect(slot)
|
|
else:
|
|
bound_signal[tuple(signal_args_list)].connect(slot)
|
|
except KeyError:
|
|
log.warning('Skipping ui signal connection {}({}) - no matching PyQt6 overload'.format(
|
|
signal_name, signal_args))
|
|
|
|
_QtCore.QMetaObject.connectSlotsByName(self.toplevelWidget)
|
|
|
|
_uiparser.UIParser._handle_connections = _tolerant_handle_connections
|
|
except ImportError:
|
|
pass # Not using PyQt6
|
|
|
|
class Trampoline(object):
|
|
def __init__(self, methods):
|
|
self.methods = methods
|
|
|
|
def __call__(self, *a, **kw):
|
|
for m in self.methods:
|
|
m(*a, **kw)
|
|
|
|
|
|
class MyEventFilter(QtCore.QObject):
|
|
def __init__(self, window):
|
|
super(MyEventFilter, self).__init__()
|
|
self.w = window
|
|
self.has_key_p_handler = 'keypress_event__' in dir(self.w.handler_instance)
|
|
self.has_key_r_handler = 'keyrelease_event__' in dir(self.w.handler_instance)
|
|
self.has_process_key_handler = 'processed_key_event__' in dir(self.w.handler_instance)
|
|
|
|
def process_event(self, event, pressed):
|
|
shift = ctrl = False
|
|
if (event.modifiers() & QtCore.Qt.ShiftModifier):
|
|
shift = True
|
|
if (event.modifiers() & QtCore.Qt.ControlModifier):
|
|
ctrl = True
|
|
code = event.key()
|
|
key = event.text()
|
|
return (pressed, key, code, shift, ctrl)
|
|
|
|
def eventFilter(self, receiver, event):
|
|
# in pyqt5 QWindow gets all events before the widgets inside it do
|
|
# we need the widgets inside it to get their events first
|
|
# this line line provides that.
|
|
# (pyqt4 did not require this)
|
|
if isinstance(receiver, QtGui.QWindow):
|
|
return super(MyEventFilter, self).eventFilter(receiver, event)
|
|
# Run in try statement so if we want to event to run through normal event routines,
|
|
# we just raise an error and run the super class event handler.
|
|
# This is necessary because if the widget (such as dialogs) are owned by c++ rather then python,
|
|
# it causes an error and the keystrokes don't get to the widgets.
|
|
try:
|
|
if (event.type() == QtCore.QEvent.KeyPress):
|
|
handled = False
|
|
handled = self.w.keyPressTrap(event)
|
|
if (self.has_key_p_handler):
|
|
handled = self.w.handler_instance.keypress_event__(receiver, event)
|
|
elif self.has_process_key_handler:
|
|
p, k, c, s, ctrl = self.process_event(event, True)
|
|
handled = self.w.handler_instance.processed_key_event__(receiver, event, p, k, c, s, ctrl)
|
|
if handled: return True
|
|
elif (event.type() == QtCore.QEvent.KeyRelease):
|
|
handled = False
|
|
handled = self.w.keyReleaseTrap(event)
|
|
if (self.has_key_r_handler):
|
|
handled = self.w.handler_instance.keyrelease_event__(event)
|
|
elif self.has_process_key_handler:
|
|
p, k, c, s, ctrl = self.process_event(event, False)
|
|
handled = self.w.handler_instance.processed_key_event__(receiver, event, p, k, c, s, ctrl)
|
|
if handled: return True
|
|
elif event.type() in (QtCore.QEvent.FocusIn, QtCore.QEvent.FocusOut):
|
|
self.w.handler_instance.processed_focus_event__(receiver, event)
|
|
# Call Base Class Method to Continue Normal Event Processing
|
|
return super(MyEventFilter, self).eventFilter(receiver, event)
|
|
except:
|
|
return super(MyEventFilter, self).eventFilter(receiver, event)
|
|
|
|
|
|
class _VCPWindow(QtWidgets.QMainWindow):
|
|
def __init__(self, halcomp=None, path=None):
|
|
super(_VCPWindow, self).__init__()
|
|
# only initialize once for all instances
|
|
if self.__class__._instanceNum >= 1:
|
|
return
|
|
self.__class__._instanceNum += 1
|
|
|
|
self._idName = "TopObject" # initial name, will be replaced
|
|
self.halcomp = halcomp
|
|
self.has_closing_handler = False
|
|
self.setFocus()
|
|
self.PATHS = path
|
|
self.PREFS_ = None
|
|
self.originalCloseEvent_ = self.closeEvent
|
|
self._halWidgetList = []
|
|
self._VCPWindowList = []
|
|
self._DialogList = []
|
|
self.settings = QtCore.QSettings('QtVcp', path.BASENAME)
|
|
log.info('Qsettings file path: yellow<{}>'.format(self.settings.fileName()))
|
|
# make an instance with embedded variables so they
|
|
# are available to all subclassed objects
|
|
_HalWidgetBase(halcomp, path, self)
|
|
|
|
def registerHalWidget(self, widget):
|
|
self._halWidgetList.append(widget)
|
|
|
|
def getRegisteredHalWidgetList(self):
|
|
return self._halWidgetList
|
|
|
|
def registerDialog(self, widget):
|
|
self._DialogList.append(widget)
|
|
|
|
def getRegisteredDialogList(self):
|
|
return self._DialogList
|
|
|
|
# These catch events if using a plain VCP panel and there is no handler file
|
|
def keyPressEvent(self, e):
|
|
self.keyPressTrap(e)
|
|
|
|
def keyReleaseEvent(self, e):
|
|
self.keyReleaseTrap(e)
|
|
|
|
# These can get class patched by xembed library to catch events
|
|
def keyPressTrap(self, e):
|
|
return False
|
|
|
|
def keyReleaseTrap(self, e):
|
|
return False
|
|
|
|
# call closing function on main and embedded windows.
|
|
def shutdown(self):
|
|
for i in (self._VCPWindowList):
|
|
if 'closing_cleanup__' in dir(i):
|
|
log.debug('Calling handler file closing_cleanup__ function of {}.'.format(i._idName))
|
|
i.closing_cleanup__()
|
|
|
|
def sync_qsettings(self):
|
|
try:
|
|
self.settings.sync()
|
|
log.debug('Qsettings sync called:')
|
|
except Exception as e:
|
|
log.debug('Error with Qsettings sync function:\n {}'.format(e))
|
|
|
|
# resource files are compiled from the qrs file
|
|
# with an installed version of linuxcnc we can't save the resource
|
|
# file in the installed directory without being root,
|
|
# so we always make a directory in the config folder to put it in
|
|
def load_resources(self):
|
|
def qrccompile(qrcname, qrcpy):
|
|
log.info('Compiling qrc: {} to \n {}'.format(qrcname, qrcpy))
|
|
try:
|
|
subprocess.call(["pyrcc5", "-o", "{}".format(qrcpy), "{}".format(qrcname)])
|
|
return
|
|
except FileNotFoundError:
|
|
pass
|
|
try:
|
|
rcc = shutil.which("pyside6-rcc")
|
|
if not rcc:
|
|
rcc = shutil.which("rcc", path="{}{}{}".format(os.getenv("PATH", ""), os.pathsep, "/usr/lib/qt6/libexec")) or "rcc"
|
|
subprocess.call([rcc, "-g", "python", "-o", "{}".format(qrcpy), "{}".format(qrcname)])
|
|
# rcc generates PySide6 imports; rewrite to use qtpy
|
|
with open(qrcpy, 'r') as f:
|
|
content = f.read()
|
|
content = content.replace('from PySide6 import QtCore', 'from qtpy import QtCore')
|
|
with open(qrcpy, 'w') as f:
|
|
f.write(content)
|
|
except OSError as e:
|
|
log.error(
|
|
'{}, pyrcc5/rcc error. try in terminal: sudo apt install pyqt5-dev-tools or qt6-base-dev-tools to install dev tools'.format(
|
|
e))
|
|
msg = QtWidgets.QMessageBox()
|
|
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
|
msg.setText("QTvcp qrc compiling ERROR! ")
|
|
msg.setInformativeText(
|
|
'Qrc Compile error, try: "sudo apt install pyqt5-dev-tools" or "qt6-base-dev-tools" to install dev tools')
|
|
msg.setWindowTitle("Error")
|
|
msg.setDetailedText('You can continue but some images may be missing')
|
|
msg.setStandardButtons(QtWidgets.QMessageBox.Retry | QtWidgets.QMessageBox.Abort)
|
|
msg.show()
|
|
retval = msg.exec()
|
|
if retval == QtWidgets.QMessageBox.Abort: # cancel button
|
|
log.critical("Canceled from qrc compiling Error Dialog.\n")
|
|
raise SystemError('pyrcc5/rcc compiling error: try: "sudo apt install pyqt5-dev-tools" or "qt6-base-dev-tools"')
|
|
|
|
qrcname = self.PATHS.QRC
|
|
qrcpy = self.PATHS.QRCPY
|
|
|
|
# Is there a qrc file in directory?
|
|
if qrcname is not None:
|
|
qrcTime = os.stat(qrcname).st_mtime
|
|
if qrcpy is not None and os.path.isfile(qrcpy):
|
|
pyTime = os.stat(qrcpy).st_mtime
|
|
# is py older then qrc file?
|
|
if pyTime < qrcTime:
|
|
qrccompile(qrcname, qrcpy)
|
|
# there is a qrc file but no resources.py file...compile it
|
|
else:
|
|
if not os.path.isfile(qrcpy):
|
|
# make the missing directory
|
|
try:
|
|
os.makedirs(os.path.split(qrcpy)[0])
|
|
except Exception as e:
|
|
log.warning('Could not make directory {} resource file: {}'.format(os.path.split(qrcpy)[0], e))
|
|
qrccompile(qrcname, qrcpy)
|
|
|
|
# is there a resource.py in the directory?
|
|
# if so add a path to it so we can import it.
|
|
if qrcpy is not None and os.path.isfile(qrcpy):
|
|
try:
|
|
sys.path.insert(0, os.path.split(qrcpy)[0])
|
|
import importlib
|
|
importlib.import_module('resources', os.path.split(qrcpy)[0])
|
|
log.info('Imported resources.py file: yellow<{}>'.format(qrcpy))
|
|
except Exception as e:
|
|
log.warning('Could not load {} resource file: yellow<{}>'.format(qrcpy, e))
|
|
else:
|
|
log.info('No resource file to load: yellow<{}>'.format(qrcpy))
|
|
|
|
def instance(self, filename):
|
|
self.load_resources()
|
|
try:
|
|
instance = uic.loadUi(filename, self)
|
|
except AttributeError as e:
|
|
formatted_lines = traceback.format_exc().splitlines()
|
|
if 'slotname' in formatted_lines[-2]:
|
|
log.critical('Missing slot name in handler file: {}'.format(e))
|
|
message = '''A widget in the ui file, was assigned a signal \
|
|
call to a missing function name in the handler file?\n
|
|
There may be more. Some functions might not work.\n
|
|
Python Error:\n {}'''.format(str(e))
|
|
rtn = QtWidgets.QMessageBox.critical(None, "QTVCP Error", message)
|
|
else:
|
|
log.critical(e)
|
|
raise
|
|
|
|
def apply_styles(self, fname=None):
|
|
if self.PATHS.IS_SCREEN:
|
|
DIR = self.PATHS.SCREENDIR
|
|
BNAME = self.PATHS.BASENAME
|
|
else:
|
|
DIR = self.PATHS.PANELDIR
|
|
BNAME = self.PATHS.BASENAME
|
|
# apply one word system theme
|
|
if fname in (list(QtWidgets.QStyleFactory.keys())):
|
|
QtWidgets.QApplication.instance().setStyle(fname)
|
|
log.info('Applied System Style name: yellow<{}>'.format(fname))
|
|
return
|
|
|
|
# Check for Preference file specified qss
|
|
if fname is None:
|
|
if not self.PREFS_ is None:
|
|
path = self.PREFS_.getpref('style_QSS_Path', 'DEFAULT', str, 'BOOK_KEEPING')
|
|
if path.lower() == 'none':
|
|
return
|
|
if not path.lower() == 'default':
|
|
fname = path
|
|
|
|
# check for default base named qss file
|
|
if fname is None:
|
|
if self.PATHS.QSS is not None:
|
|
fname = self.PATHS.QSS
|
|
|
|
# if qss file is not a file, try expanding/adding a leading path
|
|
if fname is not None:
|
|
if not os.path.isfile(fname):
|
|
temp = os.path.join(os.path.expanduser(fname))
|
|
qssname = os.path.join(DIR, BNAME, fname + '.qss')
|
|
if not os.path.isfile(fname):
|
|
qssname = temp
|
|
else:
|
|
qssname = fname
|
|
else:
|
|
return
|
|
try:
|
|
qss_file = open(qssname).read()
|
|
# qss files aren't friendly about changing image paths
|
|
qss_file = qss_file.replace('url(:/newPrefix/images', 'url({}'.format(self.PATHS.IMAGEDIR))
|
|
self.setStyleSheet(qss_file)
|
|
log.info('Applied Style Sheet path: yellow<{}>'.format(qssname))
|
|
return
|
|
except:
|
|
if fname:
|
|
themes = ''
|
|
log.error('QSS Filepath Error: {}'.format(qssname))
|
|
log.error("{} theme not available.".format(fname))
|
|
current_theme = str(QtWidgets.QApplication.instance().style().objectName())
|
|
for i in (list(QtWidgets.QStyleFactory.keys())):
|
|
themes += (', {}'.format(i))
|
|
log.error('QTvcp Available system themes: green<{}> {}'.format(current_theme, themes))
|
|
|
|
def load_extension(self, handlerpath, obj = ''):
|
|
methods, self[obj].handler_module, self[obj].handler_instance = self._load_handlers([handlerpath], self.halcomp, self[obj])
|
|
for i in methods:
|
|
self[obj][i] = methods[i]
|
|
# See if the handler file has a closing function to call first
|
|
self[obj].has_closing_handler = 'closing_cleanup__' in dir(self[obj].handler_instance)
|
|
|
|
def _load_handlers(self, usermod, halcomp, widgets):
|
|
hdl_func = 'get_handlers'
|
|
mod = None
|
|
object = None
|
|
|
|
def add_handler(method, f):
|
|
if method in handlers:
|
|
handlers[method].append(f)
|
|
else:
|
|
handlers[method] = [f]
|
|
|
|
handlers = {}
|
|
for u in usermod:
|
|
(directory, filename) = os.path.split(u)
|
|
(basename, extension) = os.path.splitext(filename)
|
|
if directory == '':
|
|
directory = '.'
|
|
if directory not in sys.path:
|
|
sys.path.insert(0, directory)
|
|
log.debug('Adding import dir: yellow<{}>'.format(directory))
|
|
|
|
try:
|
|
mod = __import__(basename)
|
|
# inject/class patch function to read an override file
|
|
# this will be called from qtvcp.py later
|
|
mod.HandlerClass.call_user_command_ = self.call_user_command_
|
|
except ImportError as e:
|
|
log.critical("Module '{}' skipped - import error: ".format(basename), exc_info=e)
|
|
raise
|
|
continue
|
|
log.debug("Module '{}' imported green<OK>".format(mod.__name__))
|
|
|
|
try:
|
|
# look for 'get_handlers' function
|
|
h = getattr(mod, hdl_func, None)
|
|
if h and callable(h):
|
|
log.debug("Module '{}' : '{}' function found.".format(mod.__name__, hdl_func))
|
|
objlist = h(halcomp, widgets, self.PATHS) # this sets the handler class signature
|
|
else:
|
|
# the module has no get_handlers() callable.
|
|
# in this case we permit any callable except class Objects in the module to register as handler
|
|
log.debug("Module '{}': no '{}' function - registering only functions as callbacks."
|
|
.format(mod.__name__, hdl_func))
|
|
objlist = [mod]
|
|
# extract callback candidates
|
|
for object in objlist:
|
|
log.debug("Registering handlers in module {} object {}".format(mod.__name__, object))
|
|
if isinstance(object, dict):
|
|
methods = list(dict.items())
|
|
else:
|
|
methods = [(n, getattr(object, n, None)) for n in dir(object)]
|
|
for method, f in methods:
|
|
if method.startswith('_'):
|
|
continue
|
|
if callable(f):
|
|
log.verbose("Register callback '{}'".format(method))
|
|
add_handler(method, f)
|
|
|
|
except Exception as e:
|
|
log.exception("Trouble looking for handlers in '{}':".format(basename), exc_info=e)
|
|
# we require a working handler file!
|
|
raise
|
|
|
|
# Wrap lists in Trampoline, unwrap single functions
|
|
for n, v in list(handlers.items()):
|
|
if len(v) == 1:
|
|
handlers[n] = v[0]
|
|
else:
|
|
handlers[n] = Trampoline(v)
|
|
|
|
return handlers, mod, object
|
|
|
|
# this is the function that is injected into the handler file to read an override file
|
|
# this will be called from qtvcp.py later
|
|
def call_user_command_(self, klass, rcfile = "~/.qtvcprc"):
|
|
# substitute for any keywords
|
|
rcfile.replace('CONFIGFOLDER',self.PATHS.CONFIGPATH)
|
|
rcfile.replace('WORKINGFOLDER',self.PATHS.WORKINGDIR)
|
|
rcfile = os.path.expanduser(rcfile)
|
|
|
|
if os.path.exists(rcfile):
|
|
log.info('Handler Override file found at: yellow<{}>'.format(rcfile))
|
|
try:
|
|
local = {'self': klass, 'rcfile': rcfile}
|
|
exec(compile(open(rcfile, "rb").read(), rcfile, 'exec'),local)
|
|
except Exception as e:
|
|
log.exception(e)
|
|
else:
|
|
log.info('No Handler Override file at: yellow<{}>'.format(rcfile))
|
|
|
|
# facilitate the main screen to create another window panel
|
|
def makeMainPage(self, name):
|
|
return MainPage(main=self,name=name )
|
|
|
|
# This is used to build a window that a qtvcp panel can be built from.
|
|
# The idea is to embed the panel into the main screen
|
|
# This allow the completely separate panels to be incorporated in
|
|
# as the user sees fit.
|
|
# the big deal is to still allow the panel to have a functioning handler file
|
|
class MainPage(QtWidgets.QMainWindow):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__()
|
|
for key, value in kwargs.items():
|
|
if key =='main':
|
|
self.halcomp = value.halcomp
|
|
self.PATHS = value.PATHS
|
|
self.MAIN = value
|
|
elif key == 'name':
|
|
self._idName = value
|
|
|
|
def instance(self, filename):
|
|
try:
|
|
instance = uic.loadUi(filename, self)
|
|
except AttributeError as e:
|
|
formatted_lines = traceback.format_exc().splitlines()
|
|
if 'slotname' in formatted_lines[-2]:
|
|
log.critical('{}: Missing slot name in handler file: {}'.format(self._idName, e))
|
|
message = '''A widget in the ui file, {} was assigned a signal \
|
|
call to a missing function name in the handler file?\n
|
|
There may be more. Some functions might not work.\n
|
|
Python Error:\n {}'''.format(self._idName, str(e))
|
|
rtn = QtWidgets.QMessageBox.critical(None, "QTVCP Error", message)
|
|
else:
|
|
log.critical(e)
|
|
raise
|
|
|
|
def load_extension(self, handlerpath):
|
|
methods, self.handler_module, self.handler_instance = self._load_handlers([handlerpath], self.halcomp, self)
|
|
for i in methods:
|
|
self[i] = methods[i]
|
|
# See if the handler file has a closing function to call first
|
|
self.has_closing_handler = 'closing_cleanup__' in dir(self.handler_instance)
|
|
|
|
def _load_handlers(self, usermod, halcomp, widgets):
|
|
hdl_func = 'get_handlers'
|
|
mod = None
|
|
object = None
|
|
|
|
def add_handler(method, f):
|
|
if method in handlers:
|
|
handlers[method].append(f)
|
|
else:
|
|
handlers[method] = [f]
|
|
|
|
handlers = {}
|
|
for u in usermod:
|
|
(directory, filename) = os.path.split(u)
|
|
(basename, extension) = os.path.splitext(filename)
|
|
if directory == '':
|
|
directory = '.'
|
|
if directory not in sys.path:
|
|
sys.path.insert(0, directory)
|
|
log.debug('{}: adding import dir: yellow<{}>'.format(self._idName, directory))
|
|
|
|
try:
|
|
mod = __import__(basename)
|
|
except ImportError as e:
|
|
log.critical("{}: module '{}' skipped - import error: "
|
|
.format(self._idName, basename), exc_info=e)
|
|
raise
|
|
continue
|
|
log.debug("{}: module '{}' imported green<OK>".format(self._idName, mod.__name__))
|
|
|
|
try:
|
|
# look for 'get_handlers' function
|
|
h = getattr(mod, hdl_func, None)
|
|
if h and callable(h):
|
|
log.debug("{}: module '{}' : '{}' function found".format(self._idName, mod.__name__, hdl_func))
|
|
objlist = h(halcomp, widgets, self.PATHS) # this sets the handler class signature
|
|
else:
|
|
# the module has no get_handlers() callable.
|
|
# in this case we permit any callable except class Objects in the module to register as handler
|
|
log.debug("{}: module '{}': no '{}' function - registering only functions as callbacks"
|
|
.format(self._idName, mod.__name__, hdl_func))
|
|
objlist = [mod]
|
|
# extract callback candidates
|
|
for object in objlist:
|
|
log.debug("{}: Registering handlers in module {} object {}"
|
|
.format(self._idName, mod.__name__, object))
|
|
if isinstance(object, dict):
|
|
methods = list(dict.items())
|
|
else:
|
|
methods = [(n, getattr(object, n, None)) for n in dir(object)]
|
|
for method, f in methods:
|
|
if method.startswith('_'):
|
|
continue
|
|
if callable(f):
|
|
log.debug("{}: Register callback '{}'".format(self._idName, method))
|
|
add_handler(method, f)
|
|
|
|
except Exception as e:
|
|
log.exception("{}: Trouble looking for handlers in '{}':"
|
|
.format(self._idName, basename), exc_info=e)
|
|
# we require a working handler file!
|
|
raise
|
|
|
|
# Wrap lists in Trampoline, unwrap single functions
|
|
for n, v in list(handlers.items()):
|
|
if len(v) == 1:
|
|
handlers[n] = v[0]
|
|
else:
|
|
handlers[n] = Trampoline(v)
|
|
|
|
return handlers, mod, object
|
|
|
|
def __getitem__(self, item):
|
|
return getattr(self, item)
|
|
|
|
def __setitem__(self, item, value):
|
|
return setattr(self, item, value)
|
|
|
|
class VCPWindow(_VCPWindow):
|
|
_instance = [None]
|
|
_instanceNum = 0
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
if not cls._instance[0]:
|
|
cls._instance[0] = _VCPWindow.__new__(cls, *args, **kwargs)
|
|
return cls._instance[0]
|
|
|
|
def __getitem__(self, item):
|
|
if item == '':
|
|
return self
|
|
return getattr(self, item)
|
|
|
|
def __setitem__(self, item, value):
|
|
return setattr(self, item, value)
|