2017-07-02 21:38:19 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
Base classes for components to import. Read comments for some documentation
|
|
|
|
on making a valid component.
|
2017-07-02 21:38:19 -04:00
|
|
|
'''
|
2017-07-02 14:19:15 -04:00
|
|
|
from PyQt5 import uic, QtCore, QtWidgets
|
2017-08-01 17:57:39 -04:00
|
|
|
from PyQt5.QtGui import QColor
|
2017-06-22 18:40:34 -04:00
|
|
|
import os
|
2017-07-30 13:04:02 -04:00
|
|
|
import sys
|
2017-08-01 21:57:36 -04:00
|
|
|
import math
|
2017-07-27 17:49:08 -04:00
|
|
|
import time
|
2017-08-10 16:04:41 -04:00
|
|
|
import logging
|
2017-07-27 17:49:08 -04:00
|
|
|
|
|
|
|
from toolkit.frame import BlankFrame
|
2017-08-01 17:57:39 -04:00
|
|
|
from toolkit import (
|
2017-08-15 22:20:25 -04:00
|
|
|
getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
|
2017-08-01 17:57:39 -04:00
|
|
|
)
|
2017-05-29 20:39:11 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
|
2017-08-10 16:04:41 -04:00
|
|
|
log = logging.getLogger('AVP.ComponentHandler')
|
|
|
|
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
class ComponentMetaclass(type(QtCore.QObject)):
|
|
|
|
'''
|
2017-08-01 17:57:39 -04:00
|
|
|
Checks the validity of each Component class and mutates some attrs.
|
2017-07-20 20:31:38 -04:00
|
|
|
E.g., takes only major version from version string & decorates methods
|
|
|
|
'''
|
2017-07-23 22:55:41 -04:00
|
|
|
|
2017-07-24 21:22:04 -04:00
|
|
|
def initializationWrapper(func):
|
|
|
|
def initializationWrapper(self, *args, **kwargs):
|
|
|
|
try:
|
|
|
|
return func(self, *args, **kwargs)
|
2017-07-25 17:44:59 -04:00
|
|
|
except Exception:
|
2017-07-24 21:22:04 -04:00
|
|
|
try:
|
2017-07-25 22:02:47 -04:00
|
|
|
raise ComponentError(self, 'initialization process')
|
2017-07-24 21:22:04 -04:00
|
|
|
except ComponentError:
|
|
|
|
return
|
|
|
|
return initializationWrapper
|
|
|
|
|
2017-07-23 22:55:41 -04:00
|
|
|
def renderWrapper(func):
|
2017-07-24 21:22:04 -04:00
|
|
|
def renderWrapper(self, *args, **kwargs):
|
2017-07-23 22:55:41 -04:00
|
|
|
try:
|
2017-08-14 10:10:32 -04:00
|
|
|
log.verbose('### %s #%s renders%s frame %s###' % (
|
|
|
|
self.__class__.name, str(self.compPos),
|
|
|
|
'' if args else ' a preview',
|
|
|
|
'' if not args else '%s ' % args[0],
|
|
|
|
))
|
2017-07-23 22:55:41 -04:00
|
|
|
return func(self, *args, **kwargs)
|
2017-07-27 17:49:08 -04:00
|
|
|
except Exception as e:
|
2017-07-23 22:55:41 -04:00
|
|
|
try:
|
2017-07-27 22:15:41 -04:00
|
|
|
if e.__class__.__name__.startswith('Component'):
|
2017-07-27 17:49:08 -04:00
|
|
|
raise
|
|
|
|
else:
|
|
|
|
raise ComponentError(self, 'renderer')
|
2017-07-23 22:55:41 -04:00
|
|
|
except ComponentError:
|
|
|
|
return BlankFrame()
|
2017-07-24 21:22:04 -04:00
|
|
|
return renderWrapper
|
2017-07-23 22:55:41 -04:00
|
|
|
|
|
|
|
def commandWrapper(func):
|
2017-07-24 21:22:04 -04:00
|
|
|
'''Intercepts the command() method to check for global args'''
|
|
|
|
def commandWrapper(self, arg):
|
2017-07-23 22:55:41 -04:00
|
|
|
if arg.startswith('preset='):
|
|
|
|
_, preset = arg.split('=', 1)
|
2017-08-14 18:41:45 -04:00
|
|
|
path = os.path.join(self.core.getPresetDir(self), preset)
|
2017-07-23 22:55:41 -04:00
|
|
|
if not os.path.exists(path):
|
|
|
|
print('Couldn\'t locate preset "%s"' % preset)
|
|
|
|
quit(1)
|
|
|
|
else:
|
|
|
|
print('Opening "%s" preset on layer %s' % (
|
|
|
|
preset, self.compPos)
|
|
|
|
)
|
|
|
|
self.core.openPreset(path, self.compPos, preset)
|
|
|
|
# Don't call the component's command() method
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
return func(self, arg)
|
2017-07-24 21:22:04 -04:00
|
|
|
return commandWrapper
|
2017-07-23 22:55:41 -04:00
|
|
|
|
|
|
|
def propertiesWrapper(func):
|
|
|
|
'''Intercepts the usual properties if the properties are locked.'''
|
2017-07-24 21:22:04 -04:00
|
|
|
def propertiesWrapper(self):
|
2017-07-23 22:55:41 -04:00
|
|
|
if self._lockedProperties is not None:
|
|
|
|
return self._lockedProperties
|
|
|
|
else:
|
2017-07-25 22:02:47 -04:00
|
|
|
try:
|
|
|
|
return func(self)
|
|
|
|
except Exception:
|
|
|
|
try:
|
|
|
|
raise ComponentError(self, 'properties')
|
|
|
|
except ComponentError:
|
|
|
|
return []
|
2017-07-24 21:22:04 -04:00
|
|
|
return propertiesWrapper
|
2017-07-23 22:55:41 -04:00
|
|
|
|
|
|
|
def errorWrapper(func):
|
|
|
|
'''Intercepts the usual error message if it is locked.'''
|
2017-07-24 21:22:04 -04:00
|
|
|
def errorWrapper(self):
|
2017-07-23 22:55:41 -04:00
|
|
|
if self._lockedError is not None:
|
|
|
|
return self._lockedError
|
|
|
|
else:
|
|
|
|
return func(self)
|
2017-07-24 21:22:04 -04:00
|
|
|
return errorWrapper
|
2017-07-23 22:55:41 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
def __new__(cls, name, parents, attrs):
|
2017-07-23 01:53:54 -04:00
|
|
|
if 'ui' not in attrs:
|
2017-07-23 17:14:21 -04:00
|
|
|
# Use module name as ui filename by default
|
2017-07-23 01:53:54 -04:00
|
|
|
attrs['ui'] = '%s.ui' % os.path.splitext(
|
|
|
|
attrs['__module__'].split('.')[-1]
|
|
|
|
)[0]
|
2017-07-20 20:31:38 -04:00
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
# if parents[0] == QtCore.QObject: else:
|
2017-07-23 22:55:41 -04:00
|
|
|
decorate = (
|
|
|
|
'names', # Class methods
|
|
|
|
'error', 'audio', 'properties', # Properties
|
2017-07-24 21:22:04 -04:00
|
|
|
'preFrameRender', 'previewRender',
|
2017-07-27 17:49:08 -04:00
|
|
|
'frameRender', 'command',
|
2017-07-23 22:55:41 -04:00
|
|
|
)
|
2017-07-20 20:31:38 -04:00
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
# Auto-decorate methods
|
|
|
|
for key in decorate:
|
2017-07-20 20:31:38 -04:00
|
|
|
if key not in attrs:
|
|
|
|
continue
|
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
if key in ('names'):
|
|
|
|
attrs[key] = classmethod(attrs[key])
|
|
|
|
|
|
|
|
if key in ('audio'):
|
|
|
|
attrs[key] = property(attrs[key])
|
|
|
|
|
|
|
|
if key == 'command':
|
2017-07-23 22:55:41 -04:00
|
|
|
attrs[key] = cls.commandWrapper(attrs[key])
|
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
if key in ('previewRender', 'frameRender'):
|
2017-07-23 22:55:41 -04:00
|
|
|
attrs[key] = cls.renderWrapper(attrs[key])
|
2017-07-23 17:14:21 -04:00
|
|
|
|
2017-07-24 21:22:04 -04:00
|
|
|
if key == 'preFrameRender':
|
|
|
|
attrs[key] = cls.initializationWrapper(attrs[key])
|
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
if key == 'properties':
|
2017-07-23 22:55:41 -04:00
|
|
|
attrs[key] = cls.propertiesWrapper(attrs[key])
|
2017-07-23 17:14:21 -04:00
|
|
|
|
|
|
|
if key == 'error':
|
2017-07-23 22:55:41 -04:00
|
|
|
attrs[key] = cls.errorWrapper(attrs[key])
|
2017-07-23 01:53:54 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
# Turn version string into a number
|
|
|
|
try:
|
|
|
|
if 'version' not in attrs:
|
2017-08-10 16:04:41 -04:00
|
|
|
log.error(
|
2017-07-20 20:31:38 -04:00
|
|
|
'No version attribute in %s. Defaulting to 1' %
|
|
|
|
attrs['name'])
|
|
|
|
attrs['version'] = 1
|
|
|
|
else:
|
|
|
|
attrs['version'] = int(attrs['version'].split('.')[0])
|
|
|
|
except ValueError:
|
2017-08-10 16:04:41 -04:00
|
|
|
log.critical('%s component has an invalid version string:\n%s' % (
|
2017-07-20 20:31:38 -04:00
|
|
|
attrs['name'], str(attrs['version'])))
|
|
|
|
except KeyError:
|
2017-08-10 16:04:41 -04:00
|
|
|
log.critical('%s component has no version string.' % attrs['name'])
|
2017-07-20 20:31:38 -04:00
|
|
|
else:
|
|
|
|
return super().__new__(cls, name, parents, attrs)
|
|
|
|
quit(1)
|
|
|
|
|
|
|
|
|
|
|
|
class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
The base class for components to inherit.
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
2017-06-12 22:34:37 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
name = 'Component'
|
2017-07-24 21:22:04 -04:00
|
|
|
# ui = 'name_Of_Non_Default_Ui_File'
|
2017-07-23 17:14:21 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
version = '1.0.0'
|
2017-07-23 01:53:54 -04:00
|
|
|
# The major version (before the first dot) is used to determine
|
2017-07-20 20:31:38 -04:00
|
|
|
# preset compatibility; the rest is ignored so it can be non-numeric.
|
|
|
|
|
|
|
|
modified = QtCore.pyqtSignal(int, dict)
|
2017-07-23 17:14:21 -04:00
|
|
|
_error = QtCore.pyqtSignal(str, str)
|
2017-07-20 20:31:38 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
def __init__(self, moduleIndex, compPos, core):
|
2017-06-12 22:34:37 -04:00
|
|
|
super().__init__()
|
|
|
|
self.moduleIndex = moduleIndex
|
|
|
|
self.compPos = compPos
|
2017-07-23 01:53:54 -04:00
|
|
|
self.core = core
|
|
|
|
self.currentPreset = None
|
|
|
|
|
|
|
|
self._trackedWidgets = {}
|
|
|
|
self._presetNames = {}
|
2017-07-23 17:14:21 -04:00
|
|
|
self._commandArgs = {}
|
2017-08-01 17:57:39 -04:00
|
|
|
self._colorWidgets = {}
|
2017-08-01 21:57:36 -04:00
|
|
|
self._colorFuncs = {}
|
2017-08-01 17:57:39 -04:00
|
|
|
self._relativeWidgets = {}
|
2017-08-03 18:08:49 -04:00
|
|
|
# pixel values stored as floats
|
2017-08-01 21:57:36 -04:00
|
|
|
self._relativeValues = {}
|
2017-08-03 18:08:49 -04:00
|
|
|
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
|
|
|
|
self._relativeMaximums = {}
|
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
self._lockedProperties = None
|
|
|
|
self._lockedError = None
|
2017-08-03 18:08:49 -04:00
|
|
|
self._lockedSize = None
|
2017-07-20 20:31:38 -04:00
|
|
|
|
|
|
|
# Stop lengthy processes in response to this variable
|
|
|
|
self.canceled = False
|
2017-06-07 23:22:55 -04:00
|
|
|
|
2017-05-29 20:39:11 -04:00
|
|
|
def __str__(self):
|
2017-07-20 20:31:38 -04:00
|
|
|
return self.__class__.name
|
2017-05-30 19:31:10 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
def __repr__(self):
|
2017-08-03 18:08:49 -04:00
|
|
|
try:
|
|
|
|
preset = self.savePreset()
|
|
|
|
except Exception as e:
|
2017-08-06 21:52:44 -04:00
|
|
|
preset = '%s occurred while saving preset' % str(e)
|
2017-07-20 20:31:38 -04:00
|
|
|
return '%s\n%s\n%s' % (
|
2017-08-03 18:08:49 -04:00
|
|
|
self.__class__.name, str(self.__class__.version), preset
|
2017-07-20 20:31:38 -04:00
|
|
|
)
|
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
2017-08-01 17:57:39 -04:00
|
|
|
# Render Methods
|
2017-07-27 17:49:08 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
|
|
|
def previewRender(self):
|
|
|
|
image = BlankFrame(self.width, self.height)
|
|
|
|
return image
|
|
|
|
|
|
|
|
def preFrameRender(self, **kwargs):
|
|
|
|
'''
|
|
|
|
Must call super() when subclassing
|
|
|
|
Triggered only before a video is exported (video_thread.py)
|
2017-07-29 13:08:28 -04:00
|
|
|
self.audioFile = filepath to the main input audio file
|
2017-07-27 17:49:08 -04:00
|
|
|
self.completeAudioArray = a list of audio samples
|
|
|
|
self.sampleSize = number of audio samples per video frame
|
|
|
|
self.progressBarUpdate = signal to set progress bar number
|
|
|
|
self.progressBarSetText = signal to set progress bar text
|
|
|
|
Use the latter two signals to update the MainWindow if needed
|
|
|
|
for a long initialization procedure (i.e., for a visualizer)
|
|
|
|
'''
|
|
|
|
for key, value in kwargs.items():
|
|
|
|
setattr(self, key, value)
|
|
|
|
|
|
|
|
def frameRender(self, frameNo):
|
|
|
|
audioArrayIndex = frameNo * self.sampleSize
|
|
|
|
image = BlankFrame(self.width, self.height)
|
|
|
|
return image
|
|
|
|
|
2017-07-27 22:15:41 -04:00
|
|
|
def postFrameRender(self):
|
2017-07-27 17:49:08 -04:00
|
|
|
pass
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
# Properties
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
2017-06-04 13:00:36 -04:00
|
|
|
|
2017-07-09 21:27:29 -04:00
|
|
|
def properties(self):
|
|
|
|
'''
|
|
|
|
Return a list of properties to signify if your component is
|
2017-07-11 06:06:22 -04:00
|
|
|
non-animated ('static'), returns sound ('audio'), or has
|
|
|
|
encountered an error in configuration ('error').
|
2017-07-09 21:27:29 -04:00
|
|
|
'''
|
|
|
|
return []
|
|
|
|
|
2017-07-11 06:06:22 -04:00
|
|
|
def error(self):
|
|
|
|
'''
|
|
|
|
Return a string containing an error message, or None for a default.
|
2017-07-23 17:14:21 -04:00
|
|
|
Or tuple of two strings for a message with details.
|
2017-07-27 17:49:08 -04:00
|
|
|
Alternatively use lockError(msgString) within properties()
|
|
|
|
to skip this method entirely.
|
2017-07-11 06:06:22 -04:00
|
|
|
'''
|
|
|
|
return
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
def audio(self):
|
2017-07-13 21:59:23 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
Return audio to mix into master as a tuple with two elements:
|
|
|
|
The first element can be:
|
|
|
|
- A string (path to audio file),
|
|
|
|
- Or an object that returns audio data through a pipe
|
|
|
|
The second element must be a dictionary of ffmpeg filters/options
|
|
|
|
to apply to the input stream. See the filter docs for ideas:
|
|
|
|
https://ffmpeg.org/ffmpeg-filters.html
|
2017-07-13 21:59:23 -04:00
|
|
|
'''
|
2017-06-04 13:00:36 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
2017-07-27 17:49:08 -04:00
|
|
|
# Idle Methods
|
2017-07-20 20:31:38 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
def widget(self, parent):
|
|
|
|
'''
|
|
|
|
Call super().widget(*args) to create the component widget
|
|
|
|
which also auto-connects any common widgets (e.g., checkBoxes)
|
|
|
|
to self.update(). Then in a subclass connect special actions
|
2017-08-06 21:52:44 -04:00
|
|
|
(e.g., pushButtons to select a file) and initialize
|
2017-07-23 01:53:54 -04:00
|
|
|
'''
|
|
|
|
self.parent = parent
|
|
|
|
self.settings = parent.settings
|
|
|
|
self.page = self.loadUi(self.__class__.ui)
|
|
|
|
|
|
|
|
# Connect widget signals
|
|
|
|
widgets = {
|
|
|
|
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
|
|
|
|
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
|
|
|
|
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
|
|
|
|
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
|
|
|
|
}
|
|
|
|
widgets['spinBox'].extend(
|
|
|
|
self.page.findChildren(QtWidgets.QDoubleSpinBox)
|
|
|
|
)
|
2017-07-30 13:04:02 -04:00
|
|
|
for widgetList in widgets.values():
|
|
|
|
for widget in widgetList:
|
|
|
|
connectWidget(widget, self.update)
|
2017-07-23 01:53:54 -04:00
|
|
|
|
|
|
|
def update(self):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
2017-08-15 22:20:25 -04:00
|
|
|
A component update triggered by the user changing a widget value
|
|
|
|
Call super() at the END when subclassing this.
|
2017-07-23 01:53:54 -04:00
|
|
|
'''
|
2017-08-15 22:20:25 -04:00
|
|
|
oldWidgetVals = {
|
|
|
|
attr: getattr(self, attr)
|
|
|
|
for attr in self._trackedWidgets
|
|
|
|
}
|
|
|
|
newWidgetVals = {
|
|
|
|
attr: getWidgetValue(widget)
|
|
|
|
if attr not in self._colorWidgets else rgbFromString(widget.text())
|
|
|
|
for attr, widget in self._trackedWidgets.items()
|
|
|
|
}
|
2017-08-16 20:44:37 -04:00
|
|
|
modifiedWidgets = {
|
|
|
|
attr: val
|
|
|
|
for attr, val in newWidgetVals.items()
|
|
|
|
if val != oldWidgetVals[attr]
|
|
|
|
}
|
|
|
|
|
|
|
|
if modifiedWidgets:
|
|
|
|
action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets)
|
2017-08-15 22:20:25 -04:00
|
|
|
self.parent.undoStack.push(action)
|
|
|
|
|
|
|
|
def _update(self):
|
|
|
|
'''An internal component update that is not undoable'''
|
|
|
|
|
|
|
|
newWidgetVals = {
|
|
|
|
attr: getWidgetValue(widget)
|
|
|
|
for attr, widget in self._trackedWidgets.items()
|
|
|
|
}
|
|
|
|
self.setAttrs(newWidgetVals)
|
|
|
|
self.sendUpdateSignal()
|
|
|
|
|
|
|
|
def setAttrs(self, attrDict):
|
|
|
|
'''
|
|
|
|
Sets attrs (linked to trackedWidgets) in this preset to
|
|
|
|
the values in the attrDict. Mutates certain widget values if needed
|
|
|
|
'''
|
|
|
|
for attr, val in attrDict.items():
|
2017-08-01 17:57:39 -04:00
|
|
|
if attr in self._colorWidgets:
|
2017-08-01 21:57:36 -04:00
|
|
|
# Color Widgets: text stored as tuple & update the button color
|
2017-08-15 22:20:25 -04:00
|
|
|
if type(val) is tuple:
|
|
|
|
rgbTuple = val
|
|
|
|
else:
|
|
|
|
rgbTuple = rgbFromString(val)
|
2017-08-01 17:57:39 -04:00
|
|
|
btnStyle = (
|
|
|
|
"QPushButton { background-color : %s; outline: none; }"
|
2017-08-01 21:57:36 -04:00
|
|
|
% QColor(*rgbTuple).name())
|
2017-08-01 17:57:39 -04:00
|
|
|
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
2017-08-01 21:57:36 -04:00
|
|
|
setattr(self, attr, rgbTuple)
|
|
|
|
|
|
|
|
elif attr in self._relativeWidgets:
|
|
|
|
# Relative widgets: number scales to fit export resolution
|
2017-08-03 18:08:49 -04:00
|
|
|
self.updateRelativeWidget(attr)
|
2017-08-15 22:20:25 -04:00
|
|
|
setattr(self, attr, val)
|
2017-08-01 21:57:36 -04:00
|
|
|
|
2017-08-01 17:57:39 -04:00
|
|
|
else:
|
2017-08-01 21:57:36 -04:00
|
|
|
# Normal tracked widget
|
2017-08-15 22:20:25 -04:00
|
|
|
setattr(self, attr, val)
|
2017-08-01 17:57:39 -04:00
|
|
|
|
2017-08-10 21:57:06 -04:00
|
|
|
def sendUpdateSignal(self):
|
2017-07-23 01:53:54 -04:00
|
|
|
if not self.core.openingProject:
|
|
|
|
self.parent.drawPreview()
|
|
|
|
saveValueStore = self.savePreset()
|
|
|
|
saveValueStore['preset'] = self.currentPreset
|
|
|
|
self.modified.emit(self.compPos, saveValueStore)
|
|
|
|
|
|
|
|
def loadPreset(self, presetDict, presetName=None):
|
|
|
|
'''
|
|
|
|
Subclasses should take (presetDict, *args) as args.
|
|
|
|
Must use super().loadPreset(presetDict, *args) first,
|
2017-07-09 01:10:06 -04:00
|
|
|
then update self.page widgets using the preset dict.
|
2017-06-22 18:40:34 -04:00
|
|
|
'''
|
2017-06-12 22:34:37 -04:00
|
|
|
self.currentPreset = presetName \
|
2017-06-23 23:00:24 -04:00
|
|
|
if presetName is not None else presetDict['preset']
|
2017-07-23 01:53:54 -04:00
|
|
|
for attr, widget in self._trackedWidgets.items():
|
2017-07-30 13:04:02 -04:00
|
|
|
key = attr if attr not in self._presetNames \
|
2017-07-23 01:53:54 -04:00
|
|
|
else self._presetNames[attr]
|
2017-07-30 13:04:02 -04:00
|
|
|
val = presetDict[key]
|
2017-08-01 17:57:39 -04:00
|
|
|
|
|
|
|
if attr in self._colorWidgets:
|
|
|
|
widget.setText('%s,%s,%s' % val)
|
|
|
|
btnStyle = (
|
|
|
|
"QPushButton { background-color : %s; outline: none; }"
|
|
|
|
% QColor(*val).name()
|
|
|
|
)
|
|
|
|
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
2017-08-03 20:43:23 -04:00
|
|
|
elif attr in self._relativeWidgets:
|
|
|
|
self._relativeValues[attr] = val
|
|
|
|
pixelVal = self.pixelValForAttr(attr, val)
|
|
|
|
setWidgetValue(widget, pixelVal)
|
2017-08-01 17:57:39 -04:00
|
|
|
else:
|
|
|
|
setWidgetValue(widget, val)
|
2017-07-23 01:53:54 -04:00
|
|
|
|
|
|
|
def savePreset(self):
|
|
|
|
saveValueStore = {}
|
|
|
|
for attr, widget in self._trackedWidgets.items():
|
2017-08-03 20:43:23 -04:00
|
|
|
presetAttrName = (
|
2017-07-23 01:53:54 -04:00
|
|
|
attr if attr not in self._presetNames
|
|
|
|
else self._presetNames[attr]
|
2017-08-03 20:43:23 -04:00
|
|
|
)
|
|
|
|
if attr in self._relativeWidgets:
|
|
|
|
try:
|
|
|
|
val = self._relativeValues[attr]
|
|
|
|
except AttributeError:
|
|
|
|
val = self.floatValForAttr(attr)
|
|
|
|
else:
|
|
|
|
val = getattr(self, attr)
|
|
|
|
|
|
|
|
saveValueStore[presetAttrName] = val
|
2017-07-23 01:53:54 -04:00
|
|
|
return saveValueStore
|
2017-06-22 18:40:34 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
def commandHelp(self):
|
|
|
|
'''Help text as string for this component's commandline arguments'''
|
|
|
|
|
|
|
|
def command(self, arg=''):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
2017-07-23 01:53:54 -04:00
|
|
|
Configure a component using an arg from the commandline. This is
|
|
|
|
never called if global args like 'preset=' are found in the arg.
|
|
|
|
So simply check for any non-global args in your component and
|
|
|
|
call super().command() at the end to get a Help message.
|
2017-06-22 18:40:34 -04:00
|
|
|
'''
|
2017-07-23 01:53:54 -04:00
|
|
|
print(
|
|
|
|
self.__class__.name, 'Usage:\n'
|
|
|
|
'Open a preset for this component:\n'
|
|
|
|
' "preset=Preset Name"'
|
|
|
|
)
|
|
|
|
self.commandHelp()
|
|
|
|
quit(0)
|
2017-06-22 18:40:34 -04:00
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
# "Private" Methods
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
def trackWidgets(self, trackDict, **kwargs):
|
|
|
|
'''
|
|
|
|
Name widgets to track in update(), savePreset(), loadPreset(), and
|
|
|
|
command(). Requires a dict of attr names as keys, widgets as values
|
|
|
|
|
|
|
|
Optional args:
|
|
|
|
'presetNames': preset variable names to replace attr names
|
|
|
|
'commandArgs': arg keywords that differ from attr names
|
|
|
|
|
|
|
|
NOTE: Any kwarg key set to None will selectively disable tracking.
|
|
|
|
'''
|
|
|
|
self._trackedWidgets = trackDict
|
|
|
|
for kwarg in kwargs:
|
|
|
|
try:
|
2017-08-01 17:57:39 -04:00
|
|
|
if kwarg in (
|
|
|
|
'presetNames',
|
|
|
|
'commandArgs',
|
|
|
|
'colorWidgets',
|
|
|
|
'relativeWidgets',
|
|
|
|
):
|
2017-07-27 17:49:08 -04:00
|
|
|
setattr(self, '_%s' % kwarg, kwargs[kwarg])
|
|
|
|
else:
|
|
|
|
raise ComponentError(
|
|
|
|
self, 'Nonsensical keywords to trackWidgets.')
|
|
|
|
except ComponentError:
|
|
|
|
continue
|
|
|
|
|
2017-08-01 17:57:39 -04:00
|
|
|
if kwarg == 'colorWidgets':
|
|
|
|
def makeColorFunc(attr):
|
|
|
|
def pickColor_():
|
|
|
|
self.pickColor(
|
|
|
|
self._trackedWidgets[attr],
|
|
|
|
self._colorWidgets[attr]
|
|
|
|
)
|
|
|
|
return pickColor_
|
|
|
|
self._colorFuncs = {
|
|
|
|
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
|
|
|
|
}
|
|
|
|
for attr, func in self._colorFuncs.items():
|
|
|
|
self._colorWidgets[attr].clicked.connect(func)
|
|
|
|
self._colorWidgets[attr].setStyleSheet(
|
|
|
|
"QPushButton {"
|
|
|
|
"background-color : #FFFFFF; outline: none; }"
|
|
|
|
)
|
|
|
|
|
2017-08-03 18:08:49 -04:00
|
|
|
if kwarg == 'relativeWidgets':
|
|
|
|
# store maximum values of spinBoxes to be scaled appropriately
|
|
|
|
for attr in kwargs[kwarg]:
|
|
|
|
self._relativeMaximums[attr] = \
|
|
|
|
self._trackedWidgets[attr].maximum()
|
|
|
|
self.updateRelativeWidgetMaximum(attr)
|
|
|
|
|
2017-08-01 17:57:39 -04:00
|
|
|
def pickColor(self, textWidget, button):
|
|
|
|
'''Use color picker to get color input from the user.'''
|
|
|
|
dialog = QtWidgets.QColorDialog()
|
|
|
|
dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
|
|
|
|
color = dialog.getColor()
|
|
|
|
if color.isValid():
|
|
|
|
RGBstring = '%s,%s,%s' % (
|
|
|
|
str(color.red()), str(color.green()), str(color.blue()))
|
|
|
|
btnStyle = "QPushButton{background-color: %s; outline: none;}" \
|
|
|
|
% color.name()
|
|
|
|
textWidget.setText(RGBstring)
|
|
|
|
button.setStyleSheet(btnStyle)
|
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
def lockProperties(self, propList):
|
|
|
|
self._lockedProperties = propList
|
|
|
|
|
|
|
|
def lockError(self, msg):
|
|
|
|
self._lockedError = msg
|
|
|
|
|
2017-08-03 18:08:49 -04:00
|
|
|
def lockSize(self, w, h):
|
|
|
|
self._lockedSize = (w, h)
|
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
def unlockProperties(self):
|
|
|
|
self._lockedProperties = None
|
|
|
|
|
|
|
|
def unlockError(self):
|
|
|
|
self._lockedError = None
|
|
|
|
|
2017-08-03 18:08:49 -04:00
|
|
|
def unlockSize(self):
|
|
|
|
self._lockedSize = None
|
|
|
|
|
2017-06-24 23:12:41 -04:00
|
|
|
def loadUi(self, filename):
|
2017-07-20 20:31:38 -04:00
|
|
|
'''Load a Qt Designer ui file to use for this component's widget'''
|
2017-07-23 01:53:54 -04:00
|
|
|
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
|
2017-07-20 20:31:38 -04:00
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
@property
|
|
|
|
def width(self):
|
2017-08-03 18:08:49 -04:00
|
|
|
if self._lockedSize is None:
|
|
|
|
return int(self.settings.value('outputWidth'))
|
|
|
|
else:
|
|
|
|
return self._lockedSize[0]
|
2017-07-27 17:49:08 -04:00
|
|
|
|
|
|
|
@property
|
|
|
|
def height(self):
|
2017-08-03 18:08:49 -04:00
|
|
|
if self._lockedSize is None:
|
|
|
|
return int(self.settings.value('outputHeight'))
|
|
|
|
else:
|
|
|
|
return self._lockedSize[1]
|
2017-07-27 17:49:08 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
def cancel(self):
|
|
|
|
'''Stop any lengthy process in response to this variable.'''
|
|
|
|
self.canceled = True
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self.canceled = False
|
2017-07-23 17:14:21 -04:00
|
|
|
self.unlockProperties()
|
|
|
|
self.unlockError()
|
2017-06-24 23:12:41 -04:00
|
|
|
|
2017-08-03 20:43:23 -04:00
|
|
|
def relativeWidgetAxis(func):
|
|
|
|
def relativeWidgetAxis(self, attr, *args, **kwargs):
|
|
|
|
if 'axis' not in kwargs:
|
|
|
|
axis = self.width
|
|
|
|
if 'height' in attr.lower() \
|
|
|
|
or 'ypos' in attr.lower() or attr == 'y':
|
|
|
|
axis = self.height
|
|
|
|
kwargs['axis'] = axis
|
|
|
|
return func(self, attr, *args, **kwargs)
|
|
|
|
return relativeWidgetAxis
|
|
|
|
|
|
|
|
@relativeWidgetAxis
|
|
|
|
def pixelValForAttr(self, attr, val=None, **kwargs):
|
|
|
|
if val is None:
|
|
|
|
val = self._relativeValues[attr]
|
|
|
|
return math.ceil(kwargs['axis'] * val)
|
|
|
|
|
|
|
|
@relativeWidgetAxis
|
|
|
|
def floatValForAttr(self, attr, val=None, **kwargs):
|
|
|
|
if val is None:
|
|
|
|
val = self._trackedWidgets[attr].value()
|
|
|
|
return val / kwargs['axis']
|
|
|
|
|
|
|
|
def setRelativeWidget(self, attr, floatVal):
|
|
|
|
'''Set a relative widget using a float'''
|
|
|
|
pixelVal = self.pixelValForAttr(attr, floatVal)
|
|
|
|
self._trackedWidgets[attr].setValue(pixelVal)
|
|
|
|
|
2017-08-03 18:08:49 -04:00
|
|
|
def updateRelativeWidget(self, attr):
|
|
|
|
try:
|
|
|
|
oldUserValue = getattr(self, attr)
|
|
|
|
except AttributeError:
|
|
|
|
oldUserValue = self._trackedWidgets[attr].value()
|
|
|
|
newUserValue = self._trackedWidgets[attr].value()
|
2017-08-03 20:43:23 -04:00
|
|
|
newRelativeVal = self.floatValForAttr(attr, newUserValue)
|
2017-08-03 18:08:49 -04:00
|
|
|
|
|
|
|
if attr in self._relativeValues:
|
|
|
|
oldRelativeVal = self._relativeValues[attr]
|
|
|
|
if oldUserValue == newUserValue \
|
|
|
|
and oldRelativeVal != newRelativeVal:
|
|
|
|
# Float changed without pixel value changing, which
|
|
|
|
# means the pixel value needs to be updated
|
2017-08-10 16:04:41 -04:00
|
|
|
log.debug('Updating %s #%s\'s relative widget: %s' % (
|
|
|
|
self.name, self.compPos, attr))
|
2017-08-03 18:08:49 -04:00
|
|
|
self._trackedWidgets[attr].blockSignals(True)
|
|
|
|
self.updateRelativeWidgetMaximum(attr)
|
2017-08-03 20:43:23 -04:00
|
|
|
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
|
|
|
|
self._trackedWidgets[attr].setValue(pixelVal)
|
2017-08-03 18:08:49 -04:00
|
|
|
self._trackedWidgets[attr].blockSignals(False)
|
|
|
|
|
|
|
|
if attr not in self._relativeValues \
|
|
|
|
or oldUserValue != newUserValue:
|
|
|
|
self._relativeValues[attr] = newRelativeVal
|
|
|
|
|
|
|
|
def updateRelativeWidgetMaximum(self, attr):
|
|
|
|
maxRes = int(self.core.resolutions[0].split('x')[0])
|
|
|
|
newMaximumValue = self.width * (
|
|
|
|
self._relativeMaximums[attr] /
|
|
|
|
maxRes
|
|
|
|
)
|
|
|
|
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
|
|
|
|
|
2017-06-23 23:00:24 -04:00
|
|
|
|
2017-07-25 22:02:47 -04:00
|
|
|
class ComponentError(RuntimeError):
|
|
|
|
'''Gives the MainWindow a traceback to display, and cancels the export.'''
|
2017-07-25 17:44:59 -04:00
|
|
|
|
2017-07-25 22:02:47 -04:00
|
|
|
prevErrors = []
|
2017-07-27 17:49:08 -04:00
|
|
|
lastTime = time.time()
|
2017-07-25 17:44:59 -04:00
|
|
|
|
2017-07-30 13:04:02 -04:00
|
|
|
def __init__(self, caller, name, msg=None):
|
|
|
|
if msg is None and sys.exc_info()[0] is not None:
|
|
|
|
msg = str(sys.exc_info()[1])
|
|
|
|
else:
|
|
|
|
msg = 'Unknown error.'
|
2017-08-10 16:04:41 -04:00
|
|
|
log.error("ComponentError by %s's %s: %s" % (
|
2017-07-30 13:04:02 -04:00
|
|
|
caller.name, name, msg))
|
|
|
|
|
|
|
|
# Don't create multiple windows for quickly repeated messages
|
2017-07-25 22:02:47 -04:00
|
|
|
if len(ComponentError.prevErrors) > 1:
|
|
|
|
ComponentError.prevErrors.pop()
|
|
|
|
ComponentError.prevErrors.insert(0, name)
|
2017-07-27 17:49:08 -04:00
|
|
|
curTime = time.time()
|
|
|
|
if name in ComponentError.prevErrors[1:] \
|
2017-07-29 23:45:37 -04:00
|
|
|
and curTime - ComponentError.lastTime < 1.0:
|
2017-07-25 17:44:59 -04:00
|
|
|
return
|
2017-07-27 17:49:08 -04:00
|
|
|
ComponentError.lastTime = time.time()
|
2017-07-25 17:44:59 -04:00
|
|
|
|
2017-07-23 17:14:21 -04:00
|
|
|
from toolkit import formatTraceback
|
|
|
|
if sys.exc_info()[0] is not None:
|
|
|
|
string = (
|
2017-07-30 13:04:02 -04:00
|
|
|
"%s component (#%s): %s encountered %s %s: %s" % (
|
2017-07-23 17:14:21 -04:00
|
|
|
caller.__class__.name,
|
2017-07-30 13:04:02 -04:00
|
|
|
str(caller.compPos),
|
2017-07-23 17:14:21 -04:00
|
|
|
name,
|
|
|
|
'an' if any([
|
|
|
|
sys.exc_info()[0].__name__.startswith(vowel)
|
2017-07-30 21:29:06 -04:00
|
|
|
for vowel in ('A', 'I', 'U', 'O', 'E')
|
2017-07-23 17:14:21 -04:00
|
|
|
]) else 'a',
|
|
|
|
sys.exc_info()[0].__name__,
|
2017-07-29 13:08:28 -04:00
|
|
|
str(sys.exc_info()[1])
|
2017-07-23 17:14:21 -04:00
|
|
|
)
|
|
|
|
)
|
|
|
|
detail = formatTraceback(sys.exc_info()[2])
|
|
|
|
else:
|
|
|
|
string = name
|
2017-07-29 13:08:28 -04:00
|
|
|
detail = "Attributes:\n%s" % (
|
2017-07-23 17:14:21 -04:00
|
|
|
"\n".join(
|
|
|
|
[m for m in dir(caller) if not m.startswith('_')]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2017-07-27 17:49:08 -04:00
|
|
|
super().__init__(string)
|
2017-07-27 22:15:41 -04:00
|
|
|
caller.lockError(string)
|
2017-07-25 22:02:47 -04:00
|
|
|
caller._error.emit(string, detail)
|
2017-08-15 22:20:25 -04:00
|
|
|
|
|
|
|
|
|
|
|
class ComponentUpdate(QtWidgets.QUndoCommand):
|
|
|
|
'''Command object for making a component action undoable'''
|
2017-08-16 20:44:37 -04:00
|
|
|
def __init__(self, parent, oldWidgetVals, modifiedVals):
|
2017-08-15 22:20:25 -04:00
|
|
|
super().__init__(
|
|
|
|
'Changed %s component #%s' % (
|
|
|
|
parent.name, parent.compPos
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.parent = parent
|
2017-08-16 20:44:37 -04:00
|
|
|
self.oldWidgetVals = {
|
|
|
|
attr: val
|
|
|
|
for attr, val in oldWidgetVals.items()
|
|
|
|
if attr in modifiedVals
|
|
|
|
}
|
|
|
|
self.modifiedVals = modifiedVals
|
|
|
|
|
|
|
|
# Determine if this update is mergeable
|
|
|
|
self.id_ = -1
|
|
|
|
if len(self.modifiedVals) == 1:
|
|
|
|
attr, val = self.modifiedVals.popitem()
|
|
|
|
widget = self.parent._trackedWidgets[attr]
|
|
|
|
if type(widget) is QtWidgets.QLineEdit:
|
|
|
|
self.id_ = 10
|
|
|
|
elif type(widget) is QtWidgets.QSpinBox \
|
|
|
|
or type(widget) is QtWidgets.QDoubleSpinBox:
|
|
|
|
self.id_ = 20
|
|
|
|
self.modifiedVals[attr] = val
|
|
|
|
else:
|
|
|
|
log.warning(
|
|
|
|
'%s component settings changed at once. (%s)' % (
|
|
|
|
len(self.modifiedVals), repr(self.modifiedVals)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def id(self):
|
|
|
|
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
|
|
|
|
return self.id_
|
|
|
|
|
|
|
|
def mergeWith(self, other):
|
|
|
|
self.modifiedVals.update(other.modifiedVals)
|
|
|
|
return True
|
2017-08-15 22:20:25 -04:00
|
|
|
|
|
|
|
def redo(self):
|
2017-08-16 20:44:37 -04:00
|
|
|
self.parent.setAttrs(self.modifiedVals)
|
2017-08-15 22:20:25 -04:00
|
|
|
self.parent.sendUpdateSignal()
|
|
|
|
|
|
|
|
def undo(self):
|
|
|
|
self.parent.setAttrs(self.oldWidgetVals)
|
|
|
|
with blockSignals(self.parent):
|
2017-08-16 20:44:37 -04:00
|
|
|
for attr, val in self.oldWidgetVals.items():
|
|
|
|
widget = self.parent._trackedWidgets[attr]
|
2017-08-15 22:20:25 -04:00
|
|
|
if attr in self.parent._colorWidgets:
|
|
|
|
val = '%s,%s,%s' % val
|
|
|
|
setWidgetValue(widget, val)
|
|
|
|
self.parent.sendUpdateSignal()
|