components auto-connect & track widgets, less autosave spam
importing toolkit from live interpreter now works
This commit is contained in:
parent
450b944b87
commit
bf0890e7c8
2
setup.py
2
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup
|
|||
import os
|
||||
|
||||
|
||||
__version__ = '2.0.0.rc1'
|
||||
__version__ = '2.0.0.rc2'
|
||||
|
||||
|
||||
def package_files(directory):
|
||||
|
|
|
@ -1 +1,13 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# frozen
|
||||
wd = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# unfrozen
|
||||
wd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# make relative imports work when using /src as a package
|
||||
sys.path.insert(0, wd)
|
||||
|
|
|
@ -10,7 +10,6 @@ import sys
|
|||
import time
|
||||
|
||||
from core import Core
|
||||
from toolkit import loadDefaultSettings
|
||||
|
||||
|
||||
class Command(QtCore.QObject):
|
||||
|
@ -55,7 +54,6 @@ class Command(QtCore.QObject):
|
|||
|
||||
self.args = self.parser.parse_args()
|
||||
self.settings = Core.settings
|
||||
loadDefaultSettings(self)
|
||||
|
||||
if self.args.projpath:
|
||||
projPath = self.args.projpath
|
||||
|
|
198
src/component.py
198
src/component.py
|
@ -5,8 +5,28 @@
|
|||
from PyQt5 import uic, QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
from core import Core
|
||||
from toolkit.common import getPresetDir
|
||||
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
|
||||
|
||||
|
||||
class ComponentMetaclass(type(QtCore.QObject)):
|
||||
|
@ -16,10 +36,14 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
|||
E.g., takes only major version from version string & decorates methods
|
||||
'''
|
||||
def __new__(cls, name, parents, attrs):
|
||||
# print('Creating %s component' % attrs['name'])
|
||||
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]
|
||||
|
||||
# Turn certain class methods into properties and classmethods
|
||||
for key in ('error', 'properties', 'audio', 'commandHelp'):
|
||||
for key in ('error', 'properties', 'audio'):
|
||||
if key not in attrs:
|
||||
continue
|
||||
attrs[key] = property(attrs[key])
|
||||
|
@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
|||
continue
|
||||
attrs[key] = classmethod(key)
|
||||
|
||||
# Do not apply these mutations to the base class
|
||||
if parents[0] != QtCore.QObject:
|
||||
attrs['command'] = commandWrapper(attrs['command'])
|
||||
|
||||
# Turn version string into a number
|
||||
try:
|
||||
if 'version' not in attrs:
|
||||
|
@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
'''
|
||||
|
||||
name = 'Component'
|
||||
# ui = 'nameOfNonDefaultUiFile'
|
||||
version = '1.0.0'
|
||||
# The 1st number (before dot, aka the major version) is used to determine
|
||||
# The major version (before the first dot) is used to determine
|
||||
# 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()
|
||||
|
||||
def __init__(self, moduleIndex, compPos):
|
||||
def __init__(self, moduleIndex, compPos, core):
|
||||
super().__init__()
|
||||
self.currentPreset = None
|
||||
self.moduleIndex = moduleIndex
|
||||
self.compPos = compPos
|
||||
self.core = core
|
||||
self.currentPreset = None
|
||||
|
||||
self._trackedWidgets = {}
|
||||
self._presetNames = {}
|
||||
|
||||
# Stop lengthy processes in response to this variable
|
||||
self.canceled = False
|
||||
|
@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
'''
|
||||
return []
|
||||
|
||||
def commandHelp(self):
|
||||
'''Help text as string for this component's commandline arguments'''
|
||||
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
# Methods
|
||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||
|
||||
def update(self):
|
||||
'''Read widget values from self.page, then call super().update()'''
|
||||
self.parent.drawPreview()
|
||||
saveValueStore = self.savePreset()
|
||||
saveValueStore['preset'] = self.currentPreset
|
||||
self.modified.emit(self.compPos, saveValueStore)
|
||||
|
||||
def loadPreset(self, presetDict, presetName):
|
||||
def widget(self, parent):
|
||||
'''
|
||||
Subclasses take (presetDict, presetName=None) as args.
|
||||
Must use super().loadPreset(presetDict, presetName) first,
|
||||
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
|
||||
|
||||
def update(self):
|
||||
'''
|
||||
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,
|
||||
then update self.page widgets using the preset dict.
|
||||
'''
|
||||
self.currentPreset = presetName \
|
||||
if presetName is not None else presetDict['preset']
|
||||
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
|
||||
|
||||
def preFrameRender(self, **kwargs):
|
||||
'''
|
||||
|
@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def command(self, arg):
|
||||
def commandHelp(self):
|
||||
'''Help text as string for this component's commandline arguments'''
|
||||
|
||||
def command(self, arg=''):
|
||||
'''
|
||||
Configure a component using argument from the commandline.
|
||||
Use super().command(arg) at the end of a subclass's method,
|
||||
if no arguments are found in that method first
|
||||
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.
|
||||
'''
|
||||
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)
|
||||
else:
|
||||
print(
|
||||
self.__doc__, 'Usage:\n'
|
||||
'Open a preset for this component:\n'
|
||||
' "preset=Preset Name"')
|
||||
print(self.commandHelp)
|
||||
quit(0)
|
||||
print(
|
||||
self.__class__.name, 'Usage:\n'
|
||||
'Open a preset for this component:\n'
|
||||
' "preset=Preset Name"'
|
||||
)
|
||||
self.commandHelp()
|
||||
quit(0)
|
||||
|
||||
def loadUi(self, filename):
|
||||
'''Load a Qt Designer ui file to use for this component's widget'''
|
||||
return uic.loadUi(os.path.join(Core.componentsPath, filename))
|
||||
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
|
||||
|
||||
def cancel(self):
|
||||
'''Stop any lengthy process in response to this variable.'''
|
||||
|
@ -191,16 +292,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
### Reference methods for creating a new component
|
||||
### (Inherit from this class and define these)
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
self.page = self.loadUi('example.ui')
|
||||
# --- connect widget signals here ---
|
||||
return self.page
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
height = int(previewWorker.core.settings.value('outputHeight'))
|
||||
height = int(self.settings.value('outputHeight'))
|
||||
from toolkit.frame import BlankFrame
|
||||
image = BlankFrame(width, height)
|
||||
return image
|
||||
|
@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
|||
|
||||
class BadComponentInit(Exception):
|
||||
'''
|
||||
General purpose exception components can raise to indicate
|
||||
General purpose exception that components can raise to indicate
|
||||
a Python issue with e.g., dynamic creation of instances or something.
|
||||
Decorative for now, may have future use for logging.
|
||||
'''
|
||||
|
|
|
@ -13,18 +13,15 @@ class Component(Component):
|
|||
name = 'Color'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
page = self.loadUi('color.ui')
|
||||
|
||||
def widget(self, *args):
|
||||
self.color1 = (0, 0, 0)
|
||||
self.color2 = (133, 133, 133)
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
super().widget(*args)
|
||||
|
||||
page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
|
||||
page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
|
||||
self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
|
||||
self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
|
||||
|
||||
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.color1).name()
|
||||
|
@ -32,68 +29,55 @@ class Component(Component):
|
|||
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.color2).name()
|
||||
|
||||
page.pushButton_color1.setStyleSheet(btnStyle1)
|
||||
page.pushButton_color2.setStyleSheet(btnStyle2)
|
||||
page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
|
||||
page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
|
||||
self.page.pushButton_color1.setStyleSheet(btnStyle1)
|
||||
self.page.pushButton_color2.setStyleSheet(btnStyle2)
|
||||
self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
|
||||
self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
|
||||
|
||||
# disable color #2 until non-default 'fill' option gets changed
|
||||
page.lineEdit_color2.setDisabled(True)
|
||||
page.pushButton_color2.setDisabled(True)
|
||||
page.spinBox_x.valueChanged.connect(self.update)
|
||||
page.spinBox_y.valueChanged.connect(self.update)
|
||||
page.spinBox_width.setValue(
|
||||
self.page.lineEdit_color2.setDisabled(True)
|
||||
self.page.pushButton_color2.setDisabled(True)
|
||||
self.page.spinBox_width.setValue(
|
||||
int(self.settings.value("outputWidth")))
|
||||
page.spinBox_height.setValue(
|
||||
self.page.spinBox_height.setValue(
|
||||
int(self.settings.value("outputHeight")))
|
||||
|
||||
page.lineEdit_color1.textChanged.connect(self.update)
|
||||
page.lineEdit_color2.textChanged.connect(self.update)
|
||||
page.spinBox_x.valueChanged.connect(self.update)
|
||||
page.spinBox_y.valueChanged.connect(self.update)
|
||||
page.spinBox_width.valueChanged.connect(self.update)
|
||||
page.spinBox_height.valueChanged.connect(self.update)
|
||||
page.checkBox_trans.stateChanged.connect(self.update)
|
||||
|
||||
self.fillLabels = [
|
||||
'Solid',
|
||||
'Linear Gradient',
|
||||
'Radial Gradient',
|
||||
]
|
||||
for label in self.fillLabels:
|
||||
page.comboBox_fill.addItem(label)
|
||||
page.comboBox_fill.setCurrentIndex(0)
|
||||
page.comboBox_fill.currentIndexChanged.connect(self.update)
|
||||
page.comboBox_spread.currentIndexChanged.connect(self.update)
|
||||
page.spinBox_radialGradient_end.valueChanged.connect(self.update)
|
||||
page.spinBox_radialGradient_start.valueChanged.connect(self.update)
|
||||
page.spinBox_radialGradient_spread.valueChanged.connect(self.update)
|
||||
page.spinBox_linearGradient_end.valueChanged.connect(self.update)
|
||||
page.spinBox_linearGradient_start.valueChanged.connect(self.update)
|
||||
page.checkBox_stretch.stateChanged.connect(self.update)
|
||||
self.page.comboBox_fill.addItem(label)
|
||||
self.page.comboBox_fill.setCurrentIndex(0)
|
||||
|
||||
self.page = page
|
||||
return page
|
||||
self.trackWidgets(
|
||||
{
|
||||
'x': self.page.spinBox_x,
|
||||
'y': self.page.spinBox_y,
|
||||
'sizeWidth': self.page.spinBox_width,
|
||||
'sizeHeight': self.page.spinBox_height,
|
||||
'trans': self.page.checkBox_trans,
|
||||
'spread': self.page.comboBox_spread,
|
||||
'stretch': self.page.checkBox_stretch,
|
||||
'RG_start': self.page.spinBox_radialGradient_start,
|
||||
'LG_start': self.page.spinBox_linearGradient_start,
|
||||
'RG_end': self.page.spinBox_radialGradient_end,
|
||||
'LG_end': self.page.spinBox_linearGradient_end,
|
||||
'RG_centre': self.page.spinBox_radialGradient_spread,
|
||||
'fillType': self.page.comboBox_fill,
|
||||
}, presetNames={
|
||||
'sizeWidth': 'width',
|
||||
'sizeHeight': 'height',
|
||||
}
|
||||
)
|
||||
|
||||
def update(self):
|
||||
self.color1 = rgbFromString(self.page.lineEdit_color1.text())
|
||||
self.color2 = rgbFromString(self.page.lineEdit_color2.text())
|
||||
self.x = self.page.spinBox_x.value()
|
||||
self.y = self.page.spinBox_y.value()
|
||||
self.sizeWidth = self.page.spinBox_width.value()
|
||||
self.sizeHeight = self.page.spinBox_height.value()
|
||||
self.trans = self.page.checkBox_trans.isChecked()
|
||||
self.spread = self.page.comboBox_spread.currentIndex()
|
||||
|
||||
self.RG_start = self.page.spinBox_radialGradient_start.value()
|
||||
self.RG_end = self.page.spinBox_radialGradient_end.value()
|
||||
self.RG_centre = self.page.spinBox_radialGradient_spread.value()
|
||||
self.stretch = self.page.checkBox_stretch.isChecked()
|
||||
self.LG_start = self.page.spinBox_linearGradient_start.value()
|
||||
self.LG_end = self.page.spinBox_linearGradient_end.value()
|
||||
|
||||
self.fillType = self.page.comboBox_fill.currentIndex()
|
||||
if self.fillType == 0:
|
||||
fillType = self.page.comboBox_fill.currentIndex()
|
||||
if fillType == 0:
|
||||
self.page.lineEdit_color2.setEnabled(False)
|
||||
self.page.pushButton_color2.setEnabled(False)
|
||||
self.page.checkBox_trans.setEnabled(False)
|
||||
|
@ -105,10 +89,10 @@ class Component(Component):
|
|||
self.page.checkBox_trans.setEnabled(True)
|
||||
self.page.checkBox_stretch.setEnabled(True)
|
||||
self.page.comboBox_spread.setEnabled(True)
|
||||
if self.trans:
|
||||
if self.page.checkBox_trans.isChecked():
|
||||
self.page.lineEdit_color2.setEnabled(False)
|
||||
self.page.pushButton_color2.setEnabled(False)
|
||||
self.page.fillWidget.setCurrentIndex(self.fillType)
|
||||
self.page.fillWidget.setCurrentIndex(fillType)
|
||||
|
||||
super().update()
|
||||
|
||||
|
@ -181,25 +165,11 @@ class Component(Component):
|
|||
|
||||
return image.finalize()
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
def loadPreset(self, pr, *args):
|
||||
super().loadPreset(pr, *args)
|
||||
|
||||
self.page.comboBox_fill.setCurrentIndex(pr['fillType'])
|
||||
self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1'])
|
||||
self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2'])
|
||||
self.page.spinBox_x.setValue(pr['x'])
|
||||
self.page.spinBox_y.setValue(pr['y'])
|
||||
self.page.spinBox_width.setValue(pr['width'])
|
||||
self.page.spinBox_height.setValue(pr['height'])
|
||||
self.page.checkBox_trans.setChecked(pr['trans'])
|
||||
|
||||
self.page.spinBox_radialGradient_start.setValue(pr['RG_start'])
|
||||
self.page.spinBox_radialGradient_end.setValue(pr['RG_end'])
|
||||
self.page.spinBox_radialGradient_spread.setValue(pr['RG_centre'])
|
||||
self.page.spinBox_linearGradient_start.setValue(pr['LG_start'])
|
||||
self.page.spinBox_linearGradient_end.setValue(pr['LG_end'])
|
||||
self.page.checkBox_stretch.setChecked(pr['stretch'])
|
||||
self.page.comboBox_spread.setCurrentIndex(pr['spread'])
|
||||
|
||||
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*pr['color1']).name()
|
||||
|
@ -209,23 +179,10 @@ class Component(Component):
|
|||
self.page.pushButton_color2.setStyleSheet(btnStyle2)
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'color1': self.color1,
|
||||
'color2': self.color2,
|
||||
'x': self.x,
|
||||
'y': self.y,
|
||||
'fillType': self.fillType,
|
||||
'width': self.sizeWidth,
|
||||
'height': self.sizeHeight,
|
||||
'trans': self.trans,
|
||||
'stretch': self.stretch,
|
||||
'spread': self.spread,
|
||||
'RG_start': self.RG_start,
|
||||
'RG_end': self.RG_end,
|
||||
'RG_centre': self.RG_centre,
|
||||
'LG_start': self.LG_start,
|
||||
'LG_end': self.LG_end,
|
||||
}
|
||||
saveValueStore = super().savePreset()
|
||||
saveValueStore['color1'] = self.color1
|
||||
saveValueStore['color2'] = self.color2
|
||||
return saveValueStore
|
||||
|
||||
def pickColor(self, num):
|
||||
RGBstring, btnStyle = pickColor()
|
||||
|
@ -242,7 +199,7 @@ class Component(Component):
|
|||
print('Specify a color:\n color=255,255,255')
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'color':
|
||||
self.page.lineEdit_color1.setText(arg)
|
||||
|
|
|
@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance
|
|||
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||
import os
|
||||
|
||||
from core import Core
|
||||
from component import Component
|
||||
from toolkit.frame import BlankFrame
|
||||
|
||||
|
@ -11,35 +10,26 @@ class Component(Component):
|
|||
name = 'Image'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
page = self.loadUi('image.ui')
|
||||
|
||||
page.lineEdit_image.textChanged.connect(self.update)
|
||||
page.pushButton_image.clicked.connect(self.pickImage)
|
||||
page.spinBox_scale.valueChanged.connect(self.update)
|
||||
page.spinBox_rotate.valueChanged.connect(self.update)
|
||||
page.spinBox_color.valueChanged.connect(self.update)
|
||||
page.checkBox_stretch.stateChanged.connect(self.update)
|
||||
page.checkBox_mirror.stateChanged.connect(self.update)
|
||||
page.spinBox_x.valueChanged.connect(self.update)
|
||||
page.spinBox_y.valueChanged.connect(self.update)
|
||||
|
||||
self.page = page
|
||||
return page
|
||||
|
||||
def update(self):
|
||||
self.imagePath = self.page.lineEdit_image.text()
|
||||
self.scale = self.page.spinBox_scale.value()
|
||||
self.rotate = self.page.spinBox_rotate.value()
|
||||
self.color = self.page.spinBox_color.value()
|
||||
self.xPosition = self.page.spinBox_x.value()
|
||||
self.yPosition = self.page.spinBox_y.value()
|
||||
self.stretched = self.page.checkBox_stretch.isChecked()
|
||||
self.mirror = self.page.checkBox_mirror.isChecked()
|
||||
|
||||
super().update()
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.page.pushButton_image.clicked.connect(self.pickImage)
|
||||
self.trackWidgets(
|
||||
{
|
||||
'imagePath': self.page.lineEdit_image,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'rotate': self.page.spinBox_rotate,
|
||||
'color': self.page.spinBox_color,
|
||||
'xPosition': self.page.spinBox_x,
|
||||
'yPosition': self.page.spinBox_y,
|
||||
'stretched': self.page.checkBox_stretch,
|
||||
'mirror': self.page.checkBox_mirror,
|
||||
},
|
||||
presetNames={
|
||||
'imagePath': 'image',
|
||||
'xPosition': 'x',
|
||||
'yPosition': 'y',
|
||||
},
|
||||
)
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
|
@ -89,41 +79,18 @@ class Component(Component):
|
|||
|
||||
return frame
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
self.page.lineEdit_image.setText(pr['image'])
|
||||
self.page.spinBox_scale.setValue(pr['scale'])
|
||||
self.page.spinBox_color.setValue(pr['color'])
|
||||
self.page.spinBox_rotate.setValue(pr['rotate'])
|
||||
self.page.spinBox_x.setValue(pr['x'])
|
||||
self.page.spinBox_y.setValue(pr['y'])
|
||||
self.page.checkBox_stretch.setChecked(pr['stretched'])
|
||||
self.page.checkBox_mirror.setChecked(pr['mirror'])
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'image': self.imagePath,
|
||||
'scale': self.scale,
|
||||
'color': self.color,
|
||||
'rotate': self.rotate,
|
||||
'stretched': self.stretched,
|
||||
'mirror': self.mirror,
|
||||
'x': self.xPosition,
|
||||
'y': self.yPosition,
|
||||
}
|
||||
|
||||
def pickImage(self):
|
||||
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Image", imgDir,
|
||||
"Image Files (%s)" % " ".join(Core.imageFormats))
|
||||
"Image Files (%s)" % " ".join(self.core.imageFormats))
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
self.page.lineEdit_image.setText(filename)
|
||||
self.update()
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path' and os.path.exists(arg):
|
||||
try:
|
||||
|
|
|
@ -18,59 +18,46 @@ class Component(Component):
|
|||
def names():
|
||||
return ['Original Audio Visualization']
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
def widget(self, *args):
|
||||
self.visColor = (255, 255, 255)
|
||||
self.scale = 20
|
||||
self.y = 0
|
||||
self.canceled = False
|
||||
super().widget(*args)
|
||||
|
||||
page = self.loadUi('original.ui')
|
||||
page.comboBox_visLayout.addItem("Classic")
|
||||
page.comboBox_visLayout.addItem("Split")
|
||||
page.comboBox_visLayout.addItem("Bottom")
|
||||
page.comboBox_visLayout.addItem("Top")
|
||||
page.comboBox_visLayout.setCurrentIndex(0)
|
||||
page.comboBox_visLayout.currentIndexChanged.connect(self.update)
|
||||
page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
|
||||
page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
|
||||
self.page.comboBox_visLayout.addItem("Classic")
|
||||
self.page.comboBox_visLayout.addItem("Split")
|
||||
self.page.comboBox_visLayout.addItem("Bottom")
|
||||
self.page.comboBox_visLayout.addItem("Top")
|
||||
self.page.comboBox_visLayout.setCurrentIndex(0)
|
||||
|
||||
self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
|
||||
self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.visColor).name()
|
||||
page.pushButton_visColor.setStyleSheet(btnStyle)
|
||||
page.lineEdit_visColor.textChanged.connect(self.update)
|
||||
page.spinBox_scale.valueChanged.connect(self.update)
|
||||
page.spinBox_y.valueChanged.connect(self.update)
|
||||
self.page.pushButton_visColor.setStyleSheet(btnStyle)
|
||||
|
||||
self.page = page
|
||||
return page
|
||||
self.trackWidgets({
|
||||
'layout': self.page.comboBox_visLayout,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'y': self.page.spinBox_y,
|
||||
})
|
||||
|
||||
def update(self):
|
||||
self.layout = self.page.comboBox_visLayout.currentIndex()
|
||||
self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
|
||||
self.scale = self.page.spinBox_scale.value()
|
||||
self.y = self.page.spinBox_y.value()
|
||||
|
||||
super().update()
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
def loadPreset(self, pr, *args):
|
||||
super().loadPreset(pr, *args)
|
||||
|
||||
self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor'])
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*pr['visColor']).name()
|
||||
self.page.pushButton_visColor.setStyleSheet(btnStyle)
|
||||
self.page.comboBox_visLayout.setCurrentIndex(pr['layout'])
|
||||
self.page.spinBox_scale.setValue(pr['scale'])
|
||||
self.page.spinBox_y.setValue(pr['y'])
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'layout': self.layout,
|
||||
'visColor': self.visColor,
|
||||
'scale': self.scale,
|
||||
'y': self.y,
|
||||
}
|
||||
saveValueStore = super().savePreset()
|
||||
saveValueStore['visColor'] = self.visColor
|
||||
return saveValueStore
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
spectrum = numpy.fromfunction(
|
||||
|
@ -206,7 +193,7 @@ class Component(Component):
|
|||
return im
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
try:
|
||||
if key == 'color':
|
||||
|
|
|
@ -10,26 +10,15 @@ class Component(Component):
|
|||
name = 'Sound'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
page = self.loadUi('sound.ui')
|
||||
|
||||
page.lineEdit_sound.textChanged.connect(self.update)
|
||||
page.pushButton_sound.clicked.connect(self.pickSound)
|
||||
page.checkBox_chorus.stateChanged.connect(self.update)
|
||||
page.spinBox_delay.valueChanged.connect(self.update)
|
||||
page.spinBox_volume.valueChanged.connect(self.update)
|
||||
|
||||
self.page = page
|
||||
return page
|
||||
|
||||
def update(self):
|
||||
self.sound = self.page.lineEdit_sound.text()
|
||||
self.delay = self.page.spinBox_delay.value()
|
||||
self.volume = self.page.spinBox_volume.value()
|
||||
self.chorus = self.page.checkBox_chorus.isChecked()
|
||||
super().update()
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
self.page.pushButton_sound.clicked.connect(self.pickSound)
|
||||
self.trackWidgets({
|
||||
'sound': self.page.lineEdit_sound,
|
||||
'chorus': self.page.checkBox_chorus,
|
||||
'delay': self.page.spinBox_delay,
|
||||
'volume': self.page.spinBox_volume,
|
||||
})
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
|
@ -67,7 +56,7 @@ class Component(Component):
|
|||
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Sound", sndDir,
|
||||
"Audio Files (%s)" % " ".join(Core.audioFormats))
|
||||
"Audio Files (%s)" % " ".join(self.core.audioFormats))
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
self.page.lineEdit_sound.setText(filename)
|
||||
|
@ -78,30 +67,15 @@ class Component(Component):
|
|||
height = int(self.settings.value('outputHeight'))
|
||||
return BlankFrame(width, height)
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
self.page.lineEdit_sound.setText(pr['sound'])
|
||||
self.page.checkBox_chorus.setChecked(pr['chorus'])
|
||||
self.page.spinBox_delay.setValue(pr['delay'])
|
||||
self.page.spinBox_volume.setValue(pr['volume'])
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'sound': self.sound,
|
||||
'chorus': self.chorus,
|
||||
'delay': self.delay,
|
||||
'volume': self.volume,
|
||||
}
|
||||
|
||||
def commandHelp(self):
|
||||
print('Path to audio file:\n path=/filepath/to/sound.ogg')
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path':
|
||||
if '*%s' % os.path.splitext(arg)[1] \
|
||||
not in Core.audioFormats:
|
||||
not in self.core.audioFormats:
|
||||
print("Not a supported audio format")
|
||||
quit(1)
|
||||
self.page.lineEdit_sound.setText(arg)
|
||||
|
|
|
@ -16,12 +16,10 @@ class Component(Component):
|
|||
super().__init__(*args)
|
||||
self.titleFont = QFont()
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
def widget(self, *args):
|
||||
super().widget(*args)
|
||||
height = int(self.settings.value('outputHeight'))
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
|
||||
self.textColor = (255, 255, 255)
|
||||
self.title = 'Text'
|
||||
self.alignment = 1
|
||||
|
@ -30,40 +28,35 @@ class Component(Component):
|
|||
self.xPosition = width / 2 - fm.width(self.title)/2
|
||||
self.yPosition = height / 2 * 1.036
|
||||
|
||||
page = self.loadUi('text.ui')
|
||||
page.comboBox_textAlign.addItem("Left")
|
||||
page.comboBox_textAlign.addItem("Middle")
|
||||
page.comboBox_textAlign.addItem("Right")
|
||||
self.page.comboBox_textAlign.addItem("Left")
|
||||
self.page.comboBox_textAlign.addItem("Middle")
|
||||
self.page.comboBox_textAlign.addItem("Right")
|
||||
|
||||
page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
|
||||
page.pushButton_textColor.clicked.connect(self.pickColor)
|
||||
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
|
||||
self.page.pushButton_textColor.clicked.connect(self.pickColor)
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*self.textColor).name()
|
||||
page.pushButton_textColor.setStyleSheet(btnStyle)
|
||||
self.page.pushButton_textColor.setStyleSheet(btnStyle)
|
||||
|
||||
page.lineEdit_title.setText(self.title)
|
||||
page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
|
||||
page.spinBox_fontSize.setValue(int(self.fontSize))
|
||||
page.spinBox_xTextAlign.setValue(int(self.xPosition))
|
||||
page.spinBox_yTextAlign.setValue(int(self.yPosition))
|
||||
self.page.lineEdit_title.setText(self.title)
|
||||
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
|
||||
self.page.spinBox_fontSize.setValue(int(self.fontSize))
|
||||
self.page.spinBox_xTextAlign.setValue(int(self.xPosition))
|
||||
self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
|
||||
|
||||
page.fontComboBox_titleFont.currentFontChanged.connect(self.update)
|
||||
page.lineEdit_title.textChanged.connect(self.update)
|
||||
page.comboBox_textAlign.currentIndexChanged.connect(self.update)
|
||||
page.spinBox_xTextAlign.valueChanged.connect(self.update)
|
||||
page.spinBox_yTextAlign.valueChanged.connect(self.update)
|
||||
page.spinBox_fontSize.valueChanged.connect(self.update)
|
||||
page.lineEdit_textColor.textChanged.connect(self.update)
|
||||
self.page = page
|
||||
return page
|
||||
self.page.fontComboBox_titleFont.currentFontChanged.connect(
|
||||
self.update
|
||||
)
|
||||
self.trackWidgets({
|
||||
'title': self.page.lineEdit_title,
|
||||
'alignment': self.page.comboBox_textAlign,
|
||||
'fontSize': self.page.spinBox_fontSize,
|
||||
'xPosition': self.page.spinBox_xTextAlign,
|
||||
'yPosition': self.page.spinBox_yTextAlign,
|
||||
})
|
||||
|
||||
def update(self):
|
||||
self.title = self.page.lineEdit_title.text()
|
||||
self.alignment = self.page.comboBox_textAlign.currentIndex()
|
||||
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
|
||||
self.fontSize = self.page.spinBox_fontSize.value()
|
||||
self.xPosition = self.page.spinBox_xTextAlign.value()
|
||||
self.yPosition = self.page.spinBox_yTextAlign.value()
|
||||
self.textColor = rgbFromString(
|
||||
self.page.lineEdit_textColor.text())
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
|
@ -87,32 +80,22 @@ class Component(Component):
|
|||
x = self.xPosition - offset
|
||||
return x, self.yPosition
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
def loadPreset(self, pr, *args):
|
||||
super().loadPreset(pr, *args)
|
||||
|
||||
self.page.lineEdit_title.setText(pr['title'])
|
||||
font = QFont()
|
||||
font.fromString(pr['titleFont'])
|
||||
self.page.fontComboBox_titleFont.setCurrentFont(font)
|
||||
self.page.spinBox_fontSize.setValue(pr['fontSize'])
|
||||
self.page.comboBox_textAlign.setCurrentIndex(pr['alignment'])
|
||||
self.page.spinBox_xTextAlign.setValue(pr['xPosition'])
|
||||
self.page.spinBox_yTextAlign.setValue(pr['yPosition'])
|
||||
self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor'])
|
||||
btnStyle = "QPushButton { background-color : %s; outline: none; }" \
|
||||
% QColor(*pr['textColor']).name()
|
||||
self.page.pushButton_textColor.setStyleSheet(btnStyle)
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'title': self.title,
|
||||
'titleFont': self.titleFont.toString(),
|
||||
'alignment': self.alignment,
|
||||
'fontSize': self.fontSize,
|
||||
'xPosition': self.xPosition,
|
||||
'yPosition': self.yPosition,
|
||||
'textColor': self.textColor
|
||||
}
|
||||
saveValueStore = super().savePreset()
|
||||
saveValueStore['titleFont'] = self.titleFont.toString()
|
||||
saveValueStore['textColor'] = self.textColor
|
||||
return saveValueStore
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
width = int(self.settings.value('outputWidth'))
|
||||
|
@ -158,7 +141,7 @@ class Component(Component):
|
|||
print('Set custom x, y position:\n x=500 y=500')
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'color':
|
||||
self.page.lineEdit_textColor.setText(arg)
|
||||
|
|
|
@ -9,6 +9,7 @@ from queue import PriorityQueue
|
|||
from core import Core
|
||||
from component import Component, BadComponentInit
|
||||
from toolkit.frame import BlankFrame
|
||||
from toolkit.ffmpeg import testAudioStream
|
||||
from toolkit import openPipe, checkOutput
|
||||
|
||||
|
||||
|
@ -16,7 +17,7 @@ class Video:
|
|||
'''Video Component Frame-Fetcher'''
|
||||
def __init__(self, **kwargs):
|
||||
mandatoryArgs = [
|
||||
'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN
|
||||
'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
|
||||
'videoPath',
|
||||
'width',
|
||||
'height',
|
||||
|
@ -110,47 +111,40 @@ class Component(Component):
|
|||
name = 'Video'
|
||||
version = '1.0.0'
|
||||
|
||||
def widget(self, parent):
|
||||
self.parent = parent
|
||||
self.settings = parent.settings
|
||||
page = self.loadUi('video.ui')
|
||||
def widget(self, *args):
|
||||
self.videoPath = ''
|
||||
self.badVideo = False
|
||||
self.badAudio = False
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.loopVideo = False
|
||||
|
||||
page.lineEdit_video.textChanged.connect(self.update)
|
||||
page.pushButton_video.clicked.connect(self.pickVideo)
|
||||
page.checkBox_loop.stateChanged.connect(self.update)
|
||||
page.checkBox_distort.stateChanged.connect(self.update)
|
||||
page.checkBox_useAudio.stateChanged.connect(self.update)
|
||||
page.spinBox_scale.valueChanged.connect(self.update)
|
||||
page.spinBox_volume.valueChanged.connect(self.update)
|
||||
page.spinBox_x.valueChanged.connect(self.update)
|
||||
page.spinBox_y.valueChanged.connect(self.update)
|
||||
|
||||
self.page = page
|
||||
return page
|
||||
super().widget(*args)
|
||||
self.page.pushButton_video.clicked.connect(self.pickVideo)
|
||||
self.trackWidgets(
|
||||
{
|
||||
'videoPath': self.page.lineEdit_video,
|
||||
'loopVideo': self.page.checkBox_loop,
|
||||
'useAudio': self.page.checkBox_useAudio,
|
||||
'distort': self.page.checkBox_distort,
|
||||
'scale': self.page.spinBox_scale,
|
||||
'volume': self.page.spinBox_volume,
|
||||
'xPosition': self.page.spinBox_x,
|
||||
'yPosition': self.page.spinBox_y,
|
||||
}, presetNames={
|
||||
'videoPath': 'video',
|
||||
'loopVideo': 'loop',
|
||||
'xPosition': 'x',
|
||||
'yPosition': 'y',
|
||||
}
|
||||
)
|
||||
|
||||
def update(self):
|
||||
self.videoPath = self.page.lineEdit_video.text()
|
||||
self.loopVideo = self.page.checkBox_loop.isChecked()
|
||||
self.useAudio = self.page.checkBox_useAudio.isChecked()
|
||||
self.distort = self.page.checkBox_distort.isChecked()
|
||||
self.scale = self.page.spinBox_scale.value()
|
||||
self.volume = self.page.spinBox_volume.value()
|
||||
self.xPosition = self.page.spinBox_x.value()
|
||||
self.yPosition = self.page.spinBox_y.value()
|
||||
|
||||
if self.useAudio:
|
||||
if self.page.checkBox_useAudio.isChecked():
|
||||
self.page.label_volume.setEnabled(True)
|
||||
self.page.spinBox_volume.setEnabled(True)
|
||||
else:
|
||||
self.page.label_volume.setEnabled(False)
|
||||
self.page.spinBox_volume.setEnabled(False)
|
||||
|
||||
super().update()
|
||||
|
||||
def previewRender(self, previewWorker):
|
||||
|
@ -188,18 +182,7 @@ class Component(Component):
|
|||
return "The video selected is corrupt!"
|
||||
|
||||
def testAudioStream(self):
|
||||
# test if an audio stream really exists
|
||||
audioTestCommand = [
|
||||
Core.FFMPEG_BIN,
|
||||
'-i', self.videoPath,
|
||||
'-vn', '-f', 'null', '-'
|
||||
]
|
||||
try:
|
||||
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
self.badAudio = True
|
||||
else:
|
||||
self.badAudio = False
|
||||
self.badAudio = testAudioStream(self.videoPath)
|
||||
|
||||
def audio(self):
|
||||
params = {}
|
||||
|
@ -214,7 +197,7 @@ class Component(Component):
|
|||
self.blankFrame_ = BlankFrame(width, height)
|
||||
self.updateChunksize(width, height)
|
||||
self.video = Video(
|
||||
ffmpeg=Core.FFMPEG_BIN, videoPath=self.videoPath,
|
||||
ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath,
|
||||
width=width, height=height, chunkSize=self.chunkSize,
|
||||
frameRate=int(self.settings.value("outputFrameRate")),
|
||||
parent=self.parent, loopVideo=self.loopVideo,
|
||||
|
@ -227,34 +210,11 @@ class Component(Component):
|
|||
else:
|
||||
return self.blankFrame_
|
||||
|
||||
def loadPreset(self, pr, presetName=None):
|
||||
super().loadPreset(pr, presetName)
|
||||
self.page.lineEdit_video.setText(pr['video'])
|
||||
self.page.checkBox_loop.setChecked(pr['loop'])
|
||||
self.page.checkBox_useAudio.setChecked(pr['useAudio'])
|
||||
self.page.checkBox_distort.setChecked(pr['distort'])
|
||||
self.page.spinBox_scale.setValue(pr['scale'])
|
||||
self.page.spinBox_volume.setValue(pr['volume'])
|
||||
self.page.spinBox_x.setValue(pr['x'])
|
||||
self.page.spinBox_y.setValue(pr['y'])
|
||||
|
||||
def savePreset(self):
|
||||
return {
|
||||
'video': self.videoPath,
|
||||
'loop': self.loopVideo,
|
||||
'useAudio': self.useAudio,
|
||||
'distort': self.distort,
|
||||
'scale': self.scale,
|
||||
'volume': self.volume,
|
||||
'x': self.xPosition,
|
||||
'y': self.yPosition,
|
||||
}
|
||||
|
||||
def pickVideo(self):
|
||||
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
self.page, "Choose Video",
|
||||
imgDir, "Video Files (%s)" % " ".join(Core.videoFormats)
|
||||
imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
|
||||
)
|
||||
if filename:
|
||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||
|
@ -266,7 +226,7 @@ class Component(Component):
|
|||
return
|
||||
|
||||
command = [
|
||||
self.parent.core.FFMPEG_BIN,
|
||||
self.core.FFMPEG_BIN,
|
||||
'-thread_queue_size', '512',
|
||||
'-i', self.videoPath,
|
||||
'-f', 'image2pipe',
|
||||
|
@ -294,10 +254,10 @@ class Component(Component):
|
|||
self.chunkSize = 4*width*height
|
||||
|
||||
def command(self, arg):
|
||||
if not arg.startswith('preset=') and '=' in arg:
|
||||
if '=' in arg:
|
||||
key, arg = arg.split('=', 1)
|
||||
if key == 'path' and os.path.exists(arg):
|
||||
if '*%s' % os.path.splitext(arg)[1] in Core.videoFormats:
|
||||
if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
|
||||
self.page.lineEdit_video.setText(arg)
|
||||
self.page.spinBox_scale.setValue(100)
|
||||
self.page.checkBox_loop.setChecked(True)
|
||||
|
|
196
src/core.py
196
src/core.py
|
@ -1,5 +1,6 @@
|
|||
'''
|
||||
Home to the Core class which tracks program state. Used by GUI & commandline
|
||||
to create a list of components and create a video thread to export.
|
||||
'''
|
||||
from PyQt5 import QtCore, QtGui, uic
|
||||
import sys
|
||||
|
@ -8,7 +9,6 @@ import json
|
|||
from importlib import import_module
|
||||
|
||||
import toolkit
|
||||
from toolkit.ffmpeg import findFfmpeg
|
||||
import video_thread
|
||||
|
||||
|
||||
|
@ -16,82 +16,21 @@ class Core:
|
|||
'''
|
||||
MainWindow and Command module both use an instance of this class
|
||||
to store the core program state. This object tracks the components,
|
||||
talks to the components and handles opening/creating project files
|
||||
and presets. The class also stores constants as class variables.
|
||||
talks to the components, handles opening/creating project files
|
||||
and presets, and creates the video thread to export.
|
||||
This class also stores constants as class variables.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def storeSettings(cls):
|
||||
'''Store settings/paths to directories as class variables.'''
|
||||
if getattr(sys, 'frozen', False):
|
||||
# frozen
|
||||
wd = os.path.dirname(sys.executable)
|
||||
else:
|
||||
wd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
dataDir = QtCore.QStandardPaths.writableLocation(
|
||||
QtCore.QStandardPaths.AppConfigLocation
|
||||
)
|
||||
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
||||
encoderOptions = json.load(json_file)
|
||||
|
||||
settings = {
|
||||
'wd': wd,
|
||||
'dataDir': dataDir,
|
||||
'settings': QtCore.QSettings(
|
||||
os.path.join(dataDir, 'settings.ini'),
|
||||
QtCore.QSettings.IniFormat),
|
||||
'presetDir': os.path.join(dataDir, 'presets'),
|
||||
'componentsPath': os.path.join(wd, 'components'),
|
||||
'encoderOptions': encoderOptions,
|
||||
'FFMPEG_BIN': findFfmpeg(),
|
||||
'canceled': False,
|
||||
}
|
||||
|
||||
settings['videoFormats'] = toolkit.appendUppercase([
|
||||
'*.mp4',
|
||||
'*.mov',
|
||||
'*.mkv',
|
||||
'*.avi',
|
||||
'*.webm',
|
||||
'*.flv',
|
||||
])
|
||||
settings['audioFormats'] = toolkit.appendUppercase([
|
||||
'*.mp3',
|
||||
'*.wav',
|
||||
'*.ogg',
|
||||
'*.fla',
|
||||
'*.flac',
|
||||
'*.aac',
|
||||
])
|
||||
settings['imageFormats'] = toolkit.appendUppercase([
|
||||
'*.png',
|
||||
'*.jpg',
|
||||
'*.tif',
|
||||
'*.tiff',
|
||||
'*.gif',
|
||||
'*.bmp',
|
||||
'*.ico',
|
||||
'*.xbm',
|
||||
'*.xpm',
|
||||
])
|
||||
|
||||
# Register all settings as class variables
|
||||
for classvar, val in settings.items():
|
||||
setattr(cls, classvar, val)
|
||||
# Make settings accessible to the toolkit package
|
||||
toolkit.init(settings)
|
||||
|
||||
def __init__(self):
|
||||
Core.storeSettings()
|
||||
|
||||
self.findComponents()
|
||||
self.selectedComponents = []
|
||||
self.savedPresets = {} # copies of presets to detect modification
|
||||
self.openingProject = False
|
||||
|
||||
def findComponents(self):
|
||||
'''Imports all the component modules'''
|
||||
def findComponents():
|
||||
for f in sorted(os.listdir(Core.componentsPath)):
|
||||
for f in os.listdir(Core.componentsPath):
|
||||
name, ext = os.path.splitext(f)
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
|
@ -104,8 +43,13 @@ class Core:
|
|||
# store canonical module names and indexes
|
||||
self.moduleIndexes = [i for i in range(len(self.modules))]
|
||||
self.compNames = [mod.Component.name for mod in self.modules]
|
||||
self.altCompNames = []
|
||||
# alphabetize modules by Component name
|
||||
sortedModules = sorted(zip(self.compNames, self.modules))
|
||||
self.compNames = [y[0] for y in sortedModules]
|
||||
self.modules = [y[1] for y in sortedModules]
|
||||
|
||||
# store alternative names for modules
|
||||
self.altCompNames = []
|
||||
for i, mod in enumerate(self.modules):
|
||||
if hasattr(mod.Component, 'names'):
|
||||
for name in mod.Component.names():
|
||||
|
@ -116,14 +60,17 @@ class Core:
|
|||
component.compPos = i
|
||||
|
||||
def insertComponent(self, compPos, moduleIndex, loader):
|
||||
'''Creates a new component'''
|
||||
'''
|
||||
Creates a new component using these args:
|
||||
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
|
||||
'''
|
||||
if compPos < 0 or compPos > len(self.selectedComponents):
|
||||
compPos = len(self.selectedComponents)
|
||||
if len(self.selectedComponents) > 50:
|
||||
return None
|
||||
|
||||
component = self.modules[moduleIndex].Component(
|
||||
moduleIndex, compPos
|
||||
moduleIndex, compPos, self
|
||||
)
|
||||
self.selectedComponents.insert(
|
||||
compPos,
|
||||
|
@ -206,6 +153,7 @@ class Core:
|
|||
|
||||
errcode, data = self.parseAvFile(filepath)
|
||||
if errcode == 0:
|
||||
self.openingProject = True
|
||||
try:
|
||||
if hasattr(loader, 'window'):
|
||||
for widget, value in data['WindowFields']:
|
||||
|
@ -239,7 +187,8 @@ class Core:
|
|||
i = self.insertComponent(
|
||||
-1,
|
||||
self.moduleIndexFor(name),
|
||||
loader)
|
||||
loader
|
||||
)
|
||||
if i is None:
|
||||
loader.showMessage(msg="Too many components!")
|
||||
break
|
||||
|
@ -284,6 +233,7 @@ class Core:
|
|||
showCancel=False,
|
||||
icon='Warning',
|
||||
detail=msg)
|
||||
self.openingProject = False
|
||||
|
||||
def parseAvFile(self, filepath):
|
||||
'''Parses an avp (project) or avl (preset package) file.
|
||||
|
@ -467,8 +417,106 @@ class Core:
|
|||
|
||||
def cancel(self):
|
||||
Core.canceled = True
|
||||
toolkit.cancel()
|
||||
|
||||
def reset(self):
|
||||
Core.canceled = False
|
||||
toolkit.reset()
|
||||
|
||||
@classmethod
|
||||
def storeSettings(cls):
|
||||
'''Store settings/paths to directories as class variables'''
|
||||
from __init__ import wd
|
||||
from toolkit.ffmpeg import findFfmpeg
|
||||
|
||||
cls.wd = wd
|
||||
dataDir = QtCore.QStandardPaths.writableLocation(
|
||||
QtCore.QStandardPaths.AppConfigLocation
|
||||
)
|
||||
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
||||
encoderOptions = json.load(json_file)
|
||||
|
||||
settings = {
|
||||
'dataDir': dataDir,
|
||||
'settings': QtCore.QSettings(
|
||||
os.path.join(dataDir, 'settings.ini'),
|
||||
QtCore.QSettings.IniFormat),
|
||||
'presetDir': os.path.join(dataDir, 'presets'),
|
||||
'componentsPath': os.path.join(wd, 'components'),
|
||||
'encoderOptions': encoderOptions,
|
||||
'resolutions': [
|
||||
'1920x1080',
|
||||
'1280x720',
|
||||
'854x480',
|
||||
],
|
||||
'windowHasFocus': False,
|
||||
'FFMPEG_BIN': findFfmpeg(),
|
||||
'canceled': False,
|
||||
}
|
||||
|
||||
settings['videoFormats'] = toolkit.appendUppercase([
|
||||
'*.mp4',
|
||||
'*.mov',
|
||||
'*.mkv',
|
||||
'*.avi',
|
||||
'*.webm',
|
||||
'*.flv',
|
||||
])
|
||||
settings['audioFormats'] = toolkit.appendUppercase([
|
||||
'*.mp3',
|
||||
'*.wav',
|
||||
'*.ogg',
|
||||
'*.fla',
|
||||
'*.flac',
|
||||
'*.aac',
|
||||
])
|
||||
settings['imageFormats'] = toolkit.appendUppercase([
|
||||
'*.png',
|
||||
'*.jpg',
|
||||
'*.tif',
|
||||
'*.tiff',
|
||||
'*.gif',
|
||||
'*.bmp',
|
||||
'*.ico',
|
||||
'*.xbm',
|
||||
'*.xpm',
|
||||
])
|
||||
|
||||
# Register all settings as class variables
|
||||
for classvar, val in settings.items():
|
||||
setattr(cls, classvar, val)
|
||||
|
||||
cls.loadDefaultSettings()
|
||||
|
||||
@classmethod
|
||||
def loadDefaultSettings(cls):
|
||||
defaultSettings = {
|
||||
"outputWidth": 1280,
|
||||
"outputHeight": 720,
|
||||
"outputFrameRate": 30,
|
||||
"outputAudioCodec": "AAC",
|
||||
"outputAudioBitrate": "192",
|
||||
"outputVideoCodec": "H264",
|
||||
"outputVideoBitrate": "2500",
|
||||
"outputVideoFormat": "yuv420p",
|
||||
"outputPreset": "medium",
|
||||
"outputFormat": "mp4",
|
||||
"outputContainer": "MP4",
|
||||
"projectDir": os.path.join(cls.dataDir, 'projects'),
|
||||
"pref_insertCompAtTop": True,
|
||||
}
|
||||
|
||||
for parm, value in defaultSettings.items():
|
||||
if cls.settings.value(parm) is None:
|
||||
cls.settings.setValue(parm, value)
|
||||
|
||||
# Allow manual editing of prefs. (Surprisingly necessary as Qt seems to
|
||||
# store True as 'true' but interprets a manually-added 'true' as str.)
|
||||
for key in cls.settings.allKeys():
|
||||
if not key.startswith('pref_'):
|
||||
continue
|
||||
val = cls.settings.value(key)
|
||||
if val in ('true', 'false'):
|
||||
cls.settings.setValue(key, True if val == 'true' else False)
|
||||
|
||||
|
||||
# always store settings in class variables even if a Core object is not created
|
||||
Core.storeSettings()
|
||||
|
|
23
src/main.py
23
src/main.py
|
@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets
|
|||
import sys
|
||||
import os
|
||||
|
||||
from __init__ import wd
|
||||
|
||||
|
||||
def main():
|
||||
if getattr(sys, 'frozen', False):
|
||||
# frozen
|
||||
wd = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# unfrozen
|
||||
wd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# make local imports work everywhere
|
||||
sys.path.insert(0, wd)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName("audio-visualizer")
|
||||
|
||||
# Determine mode
|
||||
mode = 'GUI'
|
||||
if len(sys.argv) > 2:
|
||||
mode = 'commandline'
|
||||
|
||||
elif len(sys.argv) == 2:
|
||||
if sys.argv[1].startswith('-'):
|
||||
mode = 'commandline'
|
||||
|
@ -28,11 +23,7 @@ def main():
|
|||
# normal gui launch
|
||||
proj = None
|
||||
|
||||
print('Starting Audio Visualizer in %s mode' % mode)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setApplicationName("audio-visualizer")
|
||||
# app.setOrganizationName("audio-visualizer")
|
||||
|
||||
# Launch program
|
||||
if mode == 'commandline':
|
||||
from command import Command
|
||||
|
||||
|
@ -61,9 +52,7 @@ def main():
|
|||
signal.signal(signal.SIGINT, main.cleanUp)
|
||||
atexit.register(main.cleanUp)
|
||||
|
||||
# applicable to both modes
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -17,7 +17,7 @@ import time
|
|||
from core import Core
|
||||
import preview_thread
|
||||
from presetmanager import PresetManager
|
||||
from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput
|
||||
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
|
||||
|
||||
|
||||
class PreviewWindow(QtWidgets.QLabel):
|
||||
|
@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel):
|
|||
Paints the preview QLabel and maintains the aspect ratio when the
|
||||
window is resized.
|
||||
'''
|
||||
|
||||
def __init__(self, parent, img):
|
||||
super(PreviewWindow, self).__init__()
|
||||
self.parent = parent
|
||||
|
@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel):
|
|||
self.pixmap = QtGui.QPixmap(img)
|
||||
self.repaint()
|
||||
|
||||
@QtCore.pyqtSlot(str)
|
||||
def threadError(self, msg):
|
||||
self.parent.showMessage(
|
||||
msg=msg,
|
||||
icon='Warning',
|
||||
parent=self
|
||||
)
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
'''
|
||||
|
@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
def __init__(self, window, project):
|
||||
QtWidgets.QMainWindow.__init__(self)
|
||||
|
||||
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
|
||||
self.window = window
|
||||
self.core = Core()
|
||||
|
||||
self.pages = [] # widgets of component settings
|
||||
# widgets of component settings
|
||||
self.pages = []
|
||||
self.lastAutosave = time.time()
|
||||
# list of previous five autosave times, used to reduce update spam
|
||||
self.autosaveTimes = []
|
||||
self.autosaveCooldown = 0.2
|
||||
self.encoding = False
|
||||
|
||||
# Create data directory, load/create settings
|
||||
|
@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.presetDir = Core.presetDir
|
||||
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
|
||||
self.settings = Core.settings
|
||||
loadDefaultSettings(self)
|
||||
self.presetManager = PresetManager(
|
||||
uic.loadUi(
|
||||
os.path.join(Core.wd, 'presetmanager.ui')), self)
|
||||
|
@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
if not os.path.exists(neededDirectory):
|
||||
os.mkdir(neededDirectory)
|
||||
|
||||
# Make queues/timers for the preview thread
|
||||
# Create the preview window and its thread, queues, and timers
|
||||
self.previewWindow = PreviewWindow(self, os.path.join(
|
||||
Core.wd, "background.png"))
|
||||
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
|
||||
|
||||
self.previewQueue = Queue()
|
||||
self.previewThread = QtCore.QThread(self)
|
||||
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
|
||||
self.previewWorker.error.connect(self.previewWindow.threadError)
|
||||
self.previewWorker.moveToThread(self.previewThread)
|
||||
self.previewWorker.imageCreated.connect(self.showPreviewImage)
|
||||
self.previewWorker.error.connect(self.cleanUp)
|
||||
self.previewThread.start()
|
||||
|
||||
self.timer = QtCore.QTimer(self)
|
||||
|
@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.timer.start(500)
|
||||
|
||||
# Begin decorating the window and connecting events
|
||||
self.window.installEventFilter(self)
|
||||
componentList = self.window.listWidget_componentList
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
|
@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
window.spinBox_vBitrate.setValue(vBitrate)
|
||||
window.spinBox_aBitrate.setValue(aBitrate)
|
||||
|
||||
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
|
||||
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
|
||||
|
||||
self.previewWindow = PreviewWindow(self, os.path.join(
|
||||
Core.wd, "background.png"))
|
||||
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
|
||||
|
||||
# Make component buttons
|
||||
self.compMenu = QMenu()
|
||||
for i, comp in enumerate(self.core.modules):
|
||||
|
@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
currentRes = str(self.settings.value('outputWidth'))+'x' + \
|
||||
str(self.settings.value('outputHeight'))
|
||||
for i, res in enumerate(self.resolutions):
|
||||
for i, res in enumerate(Core.resolutions):
|
||||
window.comboBox_resolution.addItem(res)
|
||||
if res == currentRes:
|
||||
currentRes = i
|
||||
|
@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.previewThread.quit()
|
||||
self.previewThread.wait()
|
||||
|
||||
@disableWhenOpeningProject
|
||||
def updateWindowTitle(self):
|
||||
appName = 'Audio Visualizer'
|
||||
try:
|
||||
|
@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
|
||||
self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
|
||||
|
||||
@disableWhenOpeningProject
|
||||
def autosave(self, force=False):
|
||||
if not self.currentProject:
|
||||
if os.path.exists(self.autosavePath):
|
||||
os.remove(self.autosavePath)
|
||||
elif force or time.time() - self.lastAutosave >= 0.2:
|
||||
elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
|
||||
self.core.createProjectFile(self.autosavePath, self.window)
|
||||
self.lastAutosave = time.time()
|
||||
if len(self.autosaveTimes) >= 5:
|
||||
# Do some math to reduce autosave spam. This gives a smooth
|
||||
# curve up to 5 seconds cooldown and maintains that for 30 secs
|
||||
# if a component is continuously updated
|
||||
timeDiff = self.lastAutosave - self.autosaveTimes.pop()
|
||||
if not force and timeDiff >= 1.0 \
|
||||
and timeDiff <= 10.0:
|
||||
if self.autosaveCooldown / 4.0 < 0.5:
|
||||
self.autosaveCooldown += 1.0
|
||||
self.autosaveCooldown = (
|
||||
5.0 * (self.autosaveCooldown / 5.0)
|
||||
) + (self.autosaveCooldown / 5.0) * 2
|
||||
elif force or timeDiff >= self.autosaveCooldown * 5:
|
||||
self.autosaveCooldown = 0.2
|
||||
self.autosaveTimes.insert(0, self.lastAutosave)
|
||||
|
||||
def autosaveExists(self, identical=True):
|
||||
'''Determines if creating the autosave should be blocked.'''
|
||||
|
@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
def updateResolution(self):
|
||||
resIndex = int(self.window.comboBox_resolution.currentIndex())
|
||||
res = self.resolutions[resIndex].split('x')
|
||||
res = Core.resolutions[resIndex].split('x')
|
||||
self.settings.setValue('outputWidth', res[0])
|
||||
self.settings.setValue('outputHeight', res[1])
|
||||
self.drawPreview()
|
||||
|
||||
def drawPreview(self, force=False):
|
||||
def drawPreview(self, force=False, **kwargs):
|
||||
'''Use autosave keyword arg to force saving or not saving if needed'''
|
||||
self.newTask.emit(self.core.selectedComponents)
|
||||
# self.processTask.emit()
|
||||
self.autosave(force)
|
||||
if force or 'autosave' in kwargs:
|
||||
if force or kwargs['autosave']:
|
||||
self.autosave(True)
|
||||
else:
|
||||
self.autosave()
|
||||
self.updateWindowTitle()
|
||||
|
||||
@QtCore.pyqtSlot(QtGui.QImage)
|
||||
|
@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
stackedWidget.insertWidget(newRow, page)
|
||||
componentList.setCurrentRow(newRow)
|
||||
stackedWidget.setCurrentIndex(newRow)
|
||||
self.drawPreview()
|
||||
self.drawPreview(True)
|
||||
|
||||
def getComponentListRects(self):
|
||||
def getComponentListMousePos(self, position):
|
||||
'''
|
||||
Given a QPos, returns the component index under the mouse cursor
|
||||
or -1 if no component is there.
|
||||
'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
|
||||
modelIndexes = [
|
||||
|
@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
componentList.visualRect(modelIndex)
|
||||
for modelIndex in modelIndexes
|
||||
]
|
||||
return rects
|
||||
mousePos = [rect.contains(position) for rect in rects]
|
||||
if not any(mousePos):
|
||||
# Not clicking a component
|
||||
mousePos = -1
|
||||
else:
|
||||
mousePos = mousePos.index(True)
|
||||
return mousePos
|
||||
|
||||
@disableWhenEncoding
|
||||
def dragComponent(self, event):
|
||||
'''Used as Qt drop event for the component listwidget'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
rects = self.getComponentListRects()
|
||||
|
||||
rowPos = [rect.contains(event.pos()) for rect in rects]
|
||||
if not any(rowPos):
|
||||
return
|
||||
|
||||
i = rowPos.index(True)
|
||||
change = (componentList.currentRow() - i) * -1
|
||||
mousePos = self.getComponentListMousePos(event.pos())
|
||||
if mousePos > -1:
|
||||
change = (componentList.currentRow() - mousePos) * -1
|
||||
else:
|
||||
change = (componentList.count() - componentList.currentRow() -1)
|
||||
self.moveComponent(change)
|
||||
|
||||
def changeComponentWidget(self):
|
||||
|
@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.settings.setValue("projectDir", os.path.dirname(filepath))
|
||||
# actually load the project using core method
|
||||
self.core.openProject(self, filepath)
|
||||
if self.window.listWidget_componentList.count() == 0:
|
||||
self.drawPreview()
|
||||
self.autosave(True)
|
||||
self.drawPreview(autosave=False)
|
||||
self.updateWindowTitle()
|
||||
|
||||
def showMessage(self, **kwargs):
|
||||
|
@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
def componentContextMenu(self, QPos):
|
||||
'''Appears when right-clicking the component list'''
|
||||
componentList = self.window.listWidget_componentList
|
||||
index = componentList.currentRow()
|
||||
|
||||
self.menu = QMenu()
|
||||
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
|
||||
|
||||
rects = self.getComponentListRects()
|
||||
rowPos = [rect.contains(QPos) for rect in rects]
|
||||
if not any(rowPos):
|
||||
# Insert components at the top if clicking nothing
|
||||
rowPos = 0
|
||||
else:
|
||||
rowPos = rowPos.index(True)
|
||||
|
||||
if index == rowPos:
|
||||
index = self.getComponentListMousePos(QPos)
|
||||
if index > -1:
|
||||
# Show preset menu if clicking a component
|
||||
self.presetManager.findPresets()
|
||||
menuItem = self.menu.addAction("Save Preset")
|
||||
|
@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
# "Add Component" submenu
|
||||
self.submenu = QMenu("Add")
|
||||
self.menu.addMenu(self.submenu)
|
||||
insertCompAtTop = self.settings.value("pref_insertCompAtTop")
|
||||
for i, comp in enumerate(self.core.modules):
|
||||
menuItem = self.submenu.addAction(comp.Component.name)
|
||||
menuItem.triggered.connect(
|
||||
lambda _, item=i: self.core.insertComponent(
|
||||
rowPos, item, self
|
||||
0 if insertCompAtTop else index, item, self
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.menu.move(parentPosition + QPos)
|
||||
self.menu.show()
|
||||
|
||||
def eventFilter(self, object, event):
|
||||
if event.type() == QtCore.QEvent.WindowActivate \
|
||||
or event.type() == QtCore.QEvent.FocusIn:
|
||||
Core.windowHasFocus = True
|
||||
elif event.type()== QtCore.QEvent.WindowDeactivate \
|
||||
or event.type() == QtCore.QEvent.FocusOut:
|
||||
Core.windowHasFocus = False
|
||||
return False
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
|
|
|
@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets
|
|||
import string
|
||||
import os
|
||||
|
||||
import toolkit
|
||||
from toolkit import badName
|
||||
from core import Core
|
||||
|
||||
|
||||
class PresetManager(QtWidgets.QDialog):
|
||||
|
@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
|
|||
currentPreset
|
||||
)
|
||||
if OK:
|
||||
if toolkit.badName(newName):
|
||||
if badName(newName):
|
||||
self.warnMessage(self.parent.window)
|
||||
continue
|
||||
if newName:
|
||||
|
@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog):
|
|||
os.remove(filepath)
|
||||
|
||||
def warnMessage(self, window=None):
|
||||
print(window)
|
||||
self.parent.showMessage(
|
||||
msg='Preset names must contain only letters, '
|
||||
'numbers, and spaces.',
|
||||
|
@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog):
|
|||
self.presetRows[index][2]
|
||||
)
|
||||
if OK:
|
||||
if toolkit.badName(newName):
|
||||
if badName(newName):
|
||||
self.warnMessage()
|
||||
continue
|
||||
if newName:
|
||||
|
@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog):
|
|||
self.findPresets()
|
||||
self.drawPresetList()
|
||||
for i, comp in enumerate(self.core.selectedComponents):
|
||||
if toolkit.getPresetDir(comp) == path \
|
||||
if getPresetDir(comp) == path \
|
||||
and comp.currentPreset == oldName:
|
||||
self.core.openPreset(newPath, i, newName)
|
||||
self.parent.updateComponentTitle(i, False)
|
||||
|
@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog):
|
|||
|
||||
def clearPresetListSelection(self):
|
||||
self.window.listWidget_presets.setCurrentRow(-1)
|
||||
|
||||
|
||||
def getPresetDir(comp):
|
||||
'''Get the preset subdir for a particular version of a component'''
|
||||
return os.path.join(Core.presetDir, str(comp), str(comp.version))
|
||||
|
|
|
@ -10,12 +10,13 @@ from queue import Queue, Empty
|
|||
import os
|
||||
|
||||
from toolkit.frame import Checkerboard
|
||||
from toolkit import disableWhenOpeningProject
|
||||
|
||||
|
||||
class Worker(QtCore.QObject):
|
||||
|
||||
imageCreated = pyqtSignal(QtGui.QImage)
|
||||
error = pyqtSignal()
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None, queue=None):
|
||||
QtCore.QObject.__init__(self)
|
||||
|
@ -30,6 +31,7 @@ class Worker(QtCore.QObject):
|
|||
height = int(self.settings.value('outputHeight'))
|
||||
self.background = Checkerboard(width, height)
|
||||
|
||||
@disableWhenOpeningProject
|
||||
@pyqtSlot(list)
|
||||
def createPreviewImage(self, components):
|
||||
dic = {
|
||||
|
@ -48,7 +50,6 @@ class Worker(QtCore.QObject):
|
|||
self.queue.get(block=False)
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
if self.background.width != width \
|
||||
or self.background.height != height:
|
||||
self.background = Checkerboard(width, height)
|
||||
|
@ -65,20 +66,12 @@ class Worker(QtCore.QObject):
|
|||
|
||||
except ValueError as e:
|
||||
errMsg = "Bad frame returned by %s's preview renderer. " \
|
||||
"%s. New frame size was %s*%s; should be %s*%s. " \
|
||||
"This is a fatal error." % (
|
||||
"%s. New frame size was %s*%s; should be %s*%s." % (
|
||||
str(component), str(e).capitalize(),
|
||||
newFrame.width, newFrame.height,
|
||||
width, height
|
||||
)
|
||||
print(errMsg)
|
||||
self.parent.showMessage(
|
||||
msg=errMsg,
|
||||
detail=str(e),
|
||||
icon='Warning',
|
||||
parent=None # MainWindow is in a different thread
|
||||
)
|
||||
self.error.emit()
|
||||
self.error.emit(errMsg)
|
||||
break
|
||||
except RuntimeError as e:
|
||||
print(e)
|
||||
|
|
|
@ -8,13 +8,6 @@ import sys
|
|||
import subprocess
|
||||
from collections import OrderedDict
|
||||
|
||||
from toolkit.core import *
|
||||
|
||||
|
||||
def getPresetDir(comp):
|
||||
'''Get the preset subdirectory for a particular version of a component'''
|
||||
return os.path.join(Core.presetDir, str(comp), str(comp.version))
|
||||
|
||||
|
||||
def badName(name):
|
||||
'''Returns whether a name contains non-alphanumeric chars'''
|
||||
|
@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs):
|
|||
|
||||
|
||||
def disableWhenEncoding(func):
|
||||
''' Blocks calls to a function while the video is being exported
|
||||
in MainWindow.
|
||||
'''
|
||||
def decorator(*args, **kwargs):
|
||||
if args[0].encoding:
|
||||
def decorator(self, *args, **kwargs):
|
||||
if self.encoding:
|
||||
return
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
def disableWhenOpeningProject(func):
|
||||
def decorator(self, *args, **kwargs):
|
||||
if self.core.openingProject:
|
||||
return
|
||||
else:
|
||||
return func(self, *args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
||||
|
@ -108,34 +107,3 @@ def rgbFromString(string):
|
|||
return tup
|
||||
except:
|
||||
return (255, 255, 255)
|
||||
|
||||
|
||||
def loadDefaultSettings(self):
|
||||
'''
|
||||
Runs once at each program start-up. Fills in default settings
|
||||
for any settings not found in settings.ini
|
||||
'''
|
||||
self.resolutions = [
|
||||
'1920x1080',
|
||||
'1280x720',
|
||||
'854x480'
|
||||
]
|
||||
|
||||
default = {
|
||||
"outputWidth": 1280,
|
||||
"outputHeight": 720,
|
||||
"outputFrameRate": 30,
|
||||
"outputAudioCodec": "AAC",
|
||||
"outputAudioBitrate": "192",
|
||||
"outputVideoCodec": "H264",
|
||||
"outputVideoBitrate": "2500",
|
||||
"outputVideoFormat": "yuv420p",
|
||||
"outputPreset": "medium",
|
||||
"outputFormat": "mp4",
|
||||
"outputContainer": "MP4",
|
||||
"projectDir": os.path.join(self.dataDir, 'projects'),
|
||||
}
|
||||
|
||||
for parm, value in default.items():
|
||||
if self.settings.value(parm) is None:
|
||||
self.settings.setValue(parm, value)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
class Core:
|
||||
'''A very complicated class for tracking settings'''
|
||||
|
||||
|
||||
def init(settings):
|
||||
global Core
|
||||
for classvar, val in settings.items():
|
||||
setattr(Core, classvar, val)
|
||||
|
||||
|
||||
def cancel():
|
||||
global Core
|
||||
Core.canceled = True
|
||||
|
||||
|
||||
def reset():
|
||||
global Core
|
||||
Core.canceled = False
|
|
@ -4,18 +4,19 @@
|
|||
import numpy
|
||||
import sys
|
||||
import os
|
||||
import subprocess as sp
|
||||
import subprocess
|
||||
|
||||
from toolkit.common import Core, checkOutput, openPipe
|
||||
import core
|
||||
from toolkit.common import checkOutput, openPipe
|
||||
|
||||
|
||||
def findFfmpeg():
|
||||
if getattr(sys, 'frozen', False):
|
||||
# The application is frozen
|
||||
if sys.platform == "win32":
|
||||
return os.path.join(Core.wd, 'ffmpeg.exe')
|
||||
return os.path.join(core.Core.wd, 'ffmpeg.exe')
|
||||
else:
|
||||
return os.path.join(Core.wd, 'ffmpeg')
|
||||
return os.path.join(core.Core.wd, 'ffmpeg')
|
||||
|
||||
else:
|
||||
if sys.platform == "win32":
|
||||
|
@ -27,7 +28,7 @@ def findFfmpeg():
|
|||
['ffmpeg', '-version'], stderr=f
|
||||
)
|
||||
return "ffmpeg"
|
||||
except sp.CalledProcessError:
|
||||
except subprocess.CalledProcessError:
|
||||
return "avconv"
|
||||
|
||||
|
||||
|
@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
|
|||
'''
|
||||
if duration == -1:
|
||||
duration = getAudioDuration(inputFile)
|
||||
|
||||
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
|
||||
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
|
||||
Core = core.Core
|
||||
|
||||
# Test if user has libfdk_aac
|
||||
encoders = checkOutput(
|
||||
|
@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
|
|||
return ffmpegCommand
|
||||
|
||||
|
||||
def testAudioStream(filename):
|
||||
'''Test if an audio stream definitely exists'''
|
||||
audioTestCommand = [
|
||||
core.Core.FFMPEG_BIN,
|
||||
'-i', filename,
|
||||
'-vn', '-f', 'null', '-'
|
||||
]
|
||||
try:
|
||||
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def getAudioDuration(filename):
|
||||
command = [Core.FFMPEG_BIN, '-i', filename]
|
||||
'''Try to get duration of audio file as float, or False if not possible'''
|
||||
command = [core.Core.FFMPEG_BIN, '-i', filename]
|
||||
|
||||
try:
|
||||
fileInfo = checkOutput(command, stderr=sp.STDOUT)
|
||||
except sp.CalledProcessError as ex:
|
||||
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
|
||||
except subprocess.CalledProcessError as ex:
|
||||
fileInfo = ex.output
|
||||
|
||||
info = fileInfo.decode("utf-8").split('\n')
|
||||
|
@ -236,13 +253,17 @@ def getAudioDuration(filename):
|
|||
|
||||
|
||||
def readAudioFile(filename, parent):
|
||||
'''
|
||||
Creates the completeAudioArray given to components
|
||||
and used to draw the classic visualizer.
|
||||
'''
|
||||
duration = getAudioDuration(filename)
|
||||
if not duration:
|
||||
print('Audio file doesn\'t exist or unreadable.')
|
||||
return
|
||||
|
||||
command = [
|
||||
Core.FFMPEG_BIN,
|
||||
core.Core.FFMPEG_BIN,
|
||||
'-i', filename,
|
||||
'-f', 's16le',
|
||||
'-acodec', 'pcm_s16le',
|
||||
|
@ -250,7 +271,8 @@ def readAudioFile(filename, parent):
|
|||
'-ac', '1', # mono (set to '2' for stereo)
|
||||
'-']
|
||||
in_pipe = openPipe(
|
||||
command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8
|
||||
command,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
|
||||
)
|
||||
|
||||
completeAudioArray = numpy.empty(0, dtype="int16")
|
||||
|
@ -258,7 +280,7 @@ def readAudioFile(filename, parent):
|
|||
progress = 0
|
||||
lastPercent = None
|
||||
while True:
|
||||
if Core.canceled:
|
||||
if core.Core.canceled:
|
||||
return
|
||||
# read 2 seconds of audio
|
||||
progress += 4
|
||||
|
|
|
@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt
|
|||
import sys
|
||||
import os
|
||||
|
||||
from toolkit.common import Core
|
||||
import core
|
||||
|
||||
|
||||
class FramePainter(QtGui.QPainter):
|
||||
|
@ -57,7 +57,7 @@ def Checkerboard(width, height):
|
|||
'''
|
||||
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
|
||||
image.paste(Image.open(
|
||||
os.path.join(Core.wd, "background.png")),
|
||||
os.path.join(core.Core.wd, "background.png")),
|
||||
(0, 0)
|
||||
)
|
||||
image = image.resize((width, height))
|
||||
|
|
|
@ -18,6 +18,7 @@ from threading import Thread, Event
|
|||
import time
|
||||
import signal
|
||||
|
||||
import core
|
||||
from toolkit import openPipe
|
||||
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
|
||||
from toolkit.frame import Checkerboard
|
||||
|
@ -104,7 +105,8 @@ class Worker(QtCore.QObject):
|
|||
|
||||
while not self.stopped:
|
||||
audioI, frame = self.previewQueue.get()
|
||||
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
|
||||
if core.Core.windowHasFocus \
|
||||
and time.time() - self.lastPreview >= 0.06 or audioI == 0:
|
||||
image = Image.alpha_composite(background.copy(), frame)
|
||||
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
|
||||
self.lastPreview = time.time()
|
||||
|
@ -231,7 +233,8 @@ class Worker(QtCore.QObject):
|
|||
|
||||
self.lastPreview = 0.0
|
||||
self.previewDispatch = Thread(
|
||||
target=self.previewDispatch, name="Render Dispatch Thread")
|
||||
target=self.previewDispatch, name="Render Dispatch Thread"
|
||||
)
|
||||
self.previewDispatch.daemon = True
|
||||
self.previewDispatch.start()
|
||||
|
||||
|
|
Reference in New Issue