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-06-22 18:40:34 -04:00
|
|
|
import os
|
2017-05-29 20:39:11 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
from presetmanager import getPresetDir
|
|
|
|
|
|
|
|
|
|
|
|
def commandWrapper(func):
|
|
|
|
'''Intercepts each component's command() method to check for global args'''
|
|
|
|
def decorator(self, arg):
|
|
|
|
if arg.startswith('preset='):
|
|
|
|
_, preset = arg.split('=', 1)
|
|
|
|
path = os.path.join(getPresetDir(self), preset)
|
|
|
|
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)
|
|
|
|
return decorator
|
2017-06-13 22:47:18 -04:00
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
|
|
|
|
class ComponentMetaclass(type(QtCore.QObject)):
|
|
|
|
'''
|
|
|
|
Checks the validity of each Component class imported, and
|
|
|
|
mutates some attributes for easier use by the core program.
|
|
|
|
E.g., takes only major version from version string & decorates methods
|
|
|
|
'''
|
|
|
|
def __new__(cls, name, parents, attrs):
|
2017-07-23 01:53:54 -04:00
|
|
|
if 'ui' not in attrs:
|
|
|
|
# use module name as ui filename by default
|
|
|
|
attrs['ui'] = '%s.ui' % os.path.splitext(
|
|
|
|
attrs['__module__'].split('.')[-1]
|
|
|
|
)[0]
|
2017-07-20 20:31:38 -04:00
|
|
|
|
|
|
|
# Turn certain class methods into properties and classmethods
|
2017-07-23 01:53:54 -04:00
|
|
|
for key in ('error', 'properties', 'audio'):
|
2017-07-20 20:31:38 -04:00
|
|
|
if key not in attrs:
|
|
|
|
continue
|
|
|
|
attrs[key] = property(attrs[key])
|
|
|
|
|
|
|
|
for key in ('names'):
|
|
|
|
if key not in attrs:
|
|
|
|
continue
|
|
|
|
attrs[key] = classmethod(key)
|
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
# Do not apply these mutations to the base class
|
|
|
|
if parents[0] != QtCore.QObject:
|
|
|
|
attrs['command'] = commandWrapper(attrs['command'])
|
|
|
|
|
2017-07-20 20:31:38 -04:00
|
|
|
# Turn version string into a number
|
|
|
|
try:
|
|
|
|
if 'version' not in attrs:
|
|
|
|
print(
|
|
|
|
'No version attribute in %s. Defaulting to 1' %
|
|
|
|
attrs['name'])
|
|
|
|
attrs['version'] = 1
|
|
|
|
else:
|
|
|
|
attrs['version'] = int(attrs['version'].split('.')[0])
|
|
|
|
except ValueError:
|
|
|
|
print('%s component has an invalid version string:\n%s' % (
|
|
|
|
attrs['name'], str(attrs['version'])))
|
|
|
|
except KeyError:
|
|
|
|
print('%s component has no version string.' % attrs['name'])
|
|
|
|
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-23 01:53:54 -04:00
|
|
|
# ui = 'nameOfNonDefaultUiFile'
|
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)
|
|
|
|
# ^ Signal used to tell core program that the component state changed,
|
|
|
|
# you shouldn't need to use this directly, it is used by self.update()
|
|
|
|
|
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-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):
|
|
|
|
return '%s\n%s\n%s' % (
|
|
|
|
self.__class__.name, str(self.__class__.version), self.savePreset()
|
|
|
|
)
|
|
|
|
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
# 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.
|
|
|
|
'''
|
|
|
|
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
|
|
|
def names():
|
2017-07-13 21:59:23 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
Alternative names for renaming a component between project files.
|
2017-07-13 21:59:23 -04:00
|
|
|
'''
|
2017-07-20 20:31:38 -04:00
|
|
|
return []
|
|
|
|
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
# Methods
|
|
|
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
|
|
|
|
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
|
|
|
|
(e.g., pushButtons to select a file/colour) and initialize
|
|
|
|
'''
|
|
|
|
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)
|
|
|
|
)
|
|
|
|
for widget in widgets['lineEdit']:
|
|
|
|
widget.textChanged.connect(self.update)
|
|
|
|
for widget in widgets['checkBox']:
|
|
|
|
widget.stateChanged.connect(self.update)
|
|
|
|
for widget in widgets['spinBox']:
|
|
|
|
widget.valueChanged.connect(self.update)
|
|
|
|
for widget in widgets['comboBox']:
|
|
|
|
widget.currentIndexChanged.connect(self.update)
|
|
|
|
|
|
|
|
def trackWidgets(self, trackDict, presetNames=None):
|
|
|
|
'''
|
|
|
|
Name widgets to track in update(), savePreset(), and loadPreset()
|
|
|
|
Accepts a dict with attribute names as keys and widgets as values.
|
|
|
|
Optional: a dict of attribute names to map to preset variable names
|
|
|
|
'''
|
|
|
|
self._trackedWidgets = trackDict
|
|
|
|
if type(presetNames) is dict:
|
|
|
|
self._presetNames = presetNames
|
2017-06-12 22:34:37 -04:00
|
|
|
|
2017-07-23 01:53:54 -04:00
|
|
|
def update(self):
|
2017-07-09 01:10:06 -04:00
|
|
|
'''
|
2017-07-23 01:53:54 -04:00
|
|
|
Reads all tracked widget values into instance attributes
|
|
|
|
and tells the MainWindow that the component was modified.
|
|
|
|
Call at the END of your method if you need to subclass this.
|
|
|
|
'''
|
|
|
|
for attr, widget in self._trackedWidgets.items():
|
|
|
|
if type(widget) == QtWidgets.QLineEdit:
|
|
|
|
setattr(self, attr, widget.text())
|
|
|
|
elif type(widget) == QtWidgets.QSpinBox \
|
|
|
|
or type(widget) == QtWidgets.QDoubleSpinBox:
|
|
|
|
setattr(self, attr, widget.value())
|
|
|
|
elif type(widget) == QtWidgets.QCheckBox:
|
|
|
|
setattr(self, attr, widget.isChecked())
|
|
|
|
elif type(widget) == QtWidgets.QComboBox:
|
|
|
|
setattr(self, attr, widget.currentIndex())
|
|
|
|
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():
|
|
|
|
val = presetDict[
|
|
|
|
attr if attr not in self._presetNames
|
|
|
|
else self._presetNames[attr]
|
|
|
|
]
|
|
|
|
if type(widget) == QtWidgets.QLineEdit:
|
|
|
|
widget.setText(val)
|
|
|
|
elif type(widget) == QtWidgets.QSpinBox \
|
|
|
|
or type(widget) == QtWidgets.QDoubleSpinBox:
|
|
|
|
widget.setValue(val)
|
|
|
|
elif type(widget) == QtWidgets.QCheckBox:
|
|
|
|
widget.setChecked(val)
|
|
|
|
elif type(widget) == QtWidgets.QComboBox:
|
|
|
|
widget.setCurrentIndex(val)
|
|
|
|
|
|
|
|
def savePreset(self):
|
|
|
|
saveValueStore = {}
|
|
|
|
for attr, widget in self._trackedWidgets.items():
|
|
|
|
saveValueStore[
|
|
|
|
attr if attr not in self._presetNames
|
|
|
|
else self._presetNames[attr]
|
|
|
|
] = getattr(self, attr)
|
|
|
|
return saveValueStore
|
2017-06-22 18:40:34 -04:00
|
|
|
|
2017-05-29 20:39:11 -04:00
|
|
|
def preFrameRender(self, **kwargs):
|
2017-07-09 14:31:19 -04:00
|
|
|
'''
|
|
|
|
Triggered only before a video is exported (video_thread.py)
|
2017-07-09 01:10:06 -04:00
|
|
|
self.worker = the video thread worker
|
|
|
|
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
|
2017-07-09 14:31:19 -04:00
|
|
|
Use the latter two signals to update the MainWindow if needed
|
2017-07-09 01:10:06 -04:00
|
|
|
for a long initialization procedure (i.e., for a visualizer)
|
2017-06-22 18:40:34 -04:00
|
|
|
'''
|
2017-07-13 21:59:23 -04:00
|
|
|
for key, value in kwargs.items():
|
|
|
|
setattr(self, key, value)
|
2017-05-29 20:39:11 -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-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
|
|
|
|
|
|
|
def cancel(self):
|
|
|
|
'''Stop any lengthy process in response to this variable.'''
|
|
|
|
self.canceled = True
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
self.canceled = False
|
2017-06-24 23:12:41 -04:00
|
|
|
|
2017-05-29 20:39:11 -04:00
|
|
|
'''
|
|
|
|
### Reference methods for creating a new component
|
|
|
|
### (Inherit from this class and define these)
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-05-29 20:39:11 -04:00
|
|
|
def previewRender(self, previewWorker):
|
2017-07-20 20:31:38 -04:00
|
|
|
width = int(self.settings.value('outputWidth'))
|
2017-07-23 01:53:54 -04:00
|
|
|
height = int(self.settings.value('outputHeight'))
|
2017-07-20 20:31:38 -04:00
|
|
|
from toolkit.frame import BlankFrame
|
2017-07-09 01:10:06 -04:00
|
|
|
image = BlankFrame(width, height)
|
2017-05-29 20:39:11 -04:00
|
|
|
return image
|
2017-06-06 11:14:39 -04:00
|
|
|
|
2017-07-09 01:10:06 -04:00
|
|
|
def frameRender(self, layerNo, frameNo):
|
|
|
|
audioArrayIndex = frameNo * self.sampleSize
|
2017-07-20 20:31:38 -04:00
|
|
|
width = int(self.settings.value('outputWidth'))
|
|
|
|
height = int(self.settings.value('outputHeight'))
|
|
|
|
from toolkit.frame import BlankFrame
|
2017-07-09 01:10:06 -04:00
|
|
|
image = BlankFrame(width, height)
|
2017-05-29 20:39:11 -04:00
|
|
|
return image
|
|
|
|
'''
|
2017-06-06 20:50:53 -04:00
|
|
|
|
2017-06-23 23:00:24 -04:00
|
|
|
|
2017-06-06 20:50:53 -04:00
|
|
|
class BadComponentInit(Exception):
|
2017-07-20 20:31:38 -04:00
|
|
|
'''
|
2017-07-23 01:53:54 -04:00
|
|
|
General purpose exception that components can raise to indicate
|
2017-07-20 20:31:38 -04:00
|
|
|
a Python issue with e.g., dynamic creation of instances or something.
|
|
|
|
Decorative for now, may have future use for logging.
|
|
|
|
'''
|
2017-06-06 20:50:53 -04:00
|
|
|
def __init__(self, arg, name):
|
2017-06-23 23:00:24 -04:00
|
|
|
string = '''################################
|
2017-06-06 20:50:53 -04:00
|
|
|
Mandatory argument "%s" not specified
|
|
|
|
in %s instance initialization
|
|
|
|
###################################'''
|
|
|
|
print(string % (arg, name))
|
|
|
|
quit()
|