components auto-connect & track widgets, less autosave spam

importing toolkit from live interpreter now works
This commit is contained in:
tassaron 2017-07-23 01:53:54 -04:00
parent 450b944b87
commit bf0890e7c8
21 changed files with 600 additions and 616 deletions

View File

@ -2,7 +2,7 @@ from setuptools import setup
import os import os
__version__ = '2.0.0.rc1' __version__ = '2.0.0.rc2'
def package_files(directory): def package_files(directory):

View File

@ -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)

View File

@ -10,7 +10,6 @@ import sys
import time import time
from core import Core from core import Core
from toolkit import loadDefaultSettings
class Command(QtCore.QObject): class Command(QtCore.QObject):
@ -55,7 +54,6 @@ class Command(QtCore.QObject):
self.args = self.parser.parse_args() self.args = self.parser.parse_args()
self.settings = Core.settings self.settings = Core.settings
loadDefaultSettings(self)
if self.args.projpath: if self.args.projpath:
projPath = self.args.projpath projPath = self.args.projpath

View File

@ -5,8 +5,28 @@
from PyQt5 import uic, QtCore, QtWidgets from PyQt5 import uic, QtCore, QtWidgets
import os import os
from core import Core from presetmanager import getPresetDir
from toolkit.common 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)): 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 E.g., takes only major version from version string & decorates methods
''' '''
def __new__(cls, name, parents, attrs): 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 # 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: if key not in attrs:
continue continue
attrs[key] = property(attrs[key]) attrs[key] = property(attrs[key])
@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
continue continue
attrs[key] = classmethod(key) 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 # Turn version string into a number
try: try:
if 'version' not in attrs: if 'version' not in attrs:
@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
''' '''
name = 'Component' name = 'Component'
# ui = 'nameOfNonDefaultUiFile'
version = '1.0.0' 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. # preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict) modified = QtCore.pyqtSignal(int, dict)
# ^ Signal used to tell core program that the component state changed, # ^ 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() # 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__() super().__init__()
self.currentPreset = None
self.moduleIndex = moduleIndex self.moduleIndex = moduleIndex
self.compPos = compPos self.compPos = compPos
self.core = core
self.currentPreset = None
self._trackedWidgets = {}
self._presetNames = {}
# Stop lengthy processes in response to this variable # Stop lengthy processes in response to this variable
self.canceled = False self.canceled = False
@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
''' '''
return [] return []
def commandHelp(self):
'''Help text as string for this component's commandline arguments'''
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Methods # Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
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
def update(self): def update(self):
'''Read widget values from self.page, then call super().update()''' '''
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() self.parent.drawPreview()
saveValueStore = self.savePreset() saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore) self.modified.emit(self.compPos, saveValueStore)
def loadPreset(self, presetDict, presetName): def loadPreset(self, presetDict, presetName=None):
''' '''
Subclasses take (presetDict, presetName=None) as args. Subclasses should take (presetDict, *args) as args.
Must use super().loadPreset(presetDict, presetName) first, Must use super().loadPreset(presetDict, *args) first,
then update self.page widgets using the preset dict. then update self.page widgets using the preset dict.
''' '''
self.currentPreset = presetName \ self.currentPreset = presetName \
if presetName is not None else presetDict['preset'] 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): def preFrameRender(self, **kwargs):
''' '''
@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(self, key, value) 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. Configure a component using an arg from the commandline. This is
Use super().command(arg) at the end of a subclass's method, never called if global args like 'preset=' are found in the arg.
if no arguments are found in that method first 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( print(
self.__doc__, 'Usage:\n' self.__class__.name, 'Usage:\n'
'Open a preset for this component:\n' 'Open a preset for this component:\n'
' "preset=Preset Name"') ' "preset=Preset Name"'
print(self.commandHelp) )
self.commandHelp()
quit(0) quit(0)
def loadUi(self, filename): def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget''' '''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): def cancel(self):
'''Stop any lengthy process in response to this variable.''' '''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 ### Reference methods for creating a new component
### (Inherit from this class and define these) ### (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): def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth')) width = int(self.settings.value('outputWidth'))
height = int(previewWorker.core.settings.value('outputHeight')) height = int(self.settings.value('outputHeight'))
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
image = BlankFrame(width, height) image = BlankFrame(width, height)
return image return image
@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
class BadComponentInit(Exception): 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. a Python issue with e.g., dynamic creation of instances or something.
Decorative for now, may have future use for logging. Decorative for now, may have future use for logging.
''' '''

View File

@ -13,18 +13,15 @@ class Component(Component):
name = 'Color' name = 'Color'
version = '1.0.0' version = '1.0.0'
def widget(self, parent): def widget(self, *args):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('color.ui')
self.color1 = (0, 0, 0) self.color1 = (0, 0, 0)
self.color2 = (133, 133, 133) self.color2 = (133, 133, 133)
self.x = 0 self.x = 0
self.y = 0 self.y = 0
super().widget(*args)
page.lineEdit_color1.setText('%s,%s,%s' % self.color1) self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1)
page.lineEdit_color2.setText('%s,%s,%s' % self.color2) self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2)
btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color1).name() % QColor(*self.color1).name()
@ -32,68 +29,55 @@ class Component(Component):
btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \ btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color2).name() % QColor(*self.color2).name()
page.pushButton_color1.setStyleSheet(btnStyle1) self.page.pushButton_color1.setStyleSheet(btnStyle1)
page.pushButton_color2.setStyleSheet(btnStyle2) self.page.pushButton_color2.setStyleSheet(btnStyle2)
page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1))
page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2))
# disable color #2 until non-default 'fill' option gets changed # disable color #2 until non-default 'fill' option gets changed
page.lineEdit_color2.setDisabled(True) self.page.lineEdit_color2.setDisabled(True)
page.pushButton_color2.setDisabled(True) self.page.pushButton_color2.setDisabled(True)
page.spinBox_x.valueChanged.connect(self.update) self.page.spinBox_width.setValue(
page.spinBox_y.valueChanged.connect(self.update)
page.spinBox_width.setValue(
int(self.settings.value("outputWidth"))) int(self.settings.value("outputWidth")))
page.spinBox_height.setValue( self.page.spinBox_height.setValue(
int(self.settings.value("outputHeight"))) 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 = [ self.fillLabels = [
'Solid', 'Solid',
'Linear Gradient', 'Linear Gradient',
'Radial Gradient', 'Radial Gradient',
] ]
for label in self.fillLabels: for label in self.fillLabels:
page.comboBox_fill.addItem(label) self.page.comboBox_fill.addItem(label)
page.comboBox_fill.setCurrentIndex(0) self.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 = page self.trackWidgets(
return page {
'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): def update(self):
self.color1 = rgbFromString(self.page.lineEdit_color1.text()) self.color1 = rgbFromString(self.page.lineEdit_color1.text())
self.color2 = rgbFromString(self.page.lineEdit_color2.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() fillType = self.page.comboBox_fill.currentIndex()
self.RG_end = self.page.spinBox_radialGradient_end.value() if fillType == 0:
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:
self.page.lineEdit_color2.setEnabled(False) self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False)
self.page.checkBox_trans.setEnabled(False) self.page.checkBox_trans.setEnabled(False)
@ -105,10 +89,10 @@ class Component(Component):
self.page.checkBox_trans.setEnabled(True) self.page.checkBox_trans.setEnabled(True)
self.page.checkBox_stretch.setEnabled(True) self.page.checkBox_stretch.setEnabled(True)
self.page.comboBox_spread.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.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(self.fillType) self.page.fillWidget.setCurrentIndex(fillType)
super().update() super().update()
@ -181,25 +165,11 @@ class Component(Component):
return image.finalize() return image.finalize()
def loadPreset(self, pr, presetName=None): def loadPreset(self, pr, *args):
super().loadPreset(pr, presetName) super().loadPreset(pr, *args)
self.page.comboBox_fill.setCurrentIndex(pr['fillType'])
self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1'])
self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) 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; }" \ btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['color1']).name() % QColor(*pr['color1']).name()
@ -209,23 +179,10 @@ class Component(Component):
self.page.pushButton_color2.setStyleSheet(btnStyle2) self.page.pushButton_color2.setStyleSheet(btnStyle2)
def savePreset(self): def savePreset(self):
return { saveValueStore = super().savePreset()
'color1': self.color1, saveValueStore['color1'] = self.color1
'color2': self.color2, saveValueStore['color2'] = self.color2
'x': self.x, return saveValueStore
'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,
}
def pickColor(self, num): def pickColor(self, num):
RGBstring, btnStyle = pickColor() RGBstring, btnStyle = pickColor()
@ -242,7 +199,7 @@ class Component(Component):
print('Specify a color:\n color=255,255,255') print('Specify a color:\n color=255,255,255')
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
if key == 'color': if key == 'color':
self.page.lineEdit_color1.setText(arg) self.page.lineEdit_color1.setText(arg)

View File

@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5 import QtGui, QtCore, QtWidgets
import os import os
from core import Core
from component import Component from component import Component
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
@ -11,35 +10,26 @@ class Component(Component):
name = 'Image' name = 'Image'
version = '1.0.0' version = '1.0.0'
def widget(self, parent): def widget(self, *args):
self.parent = parent super().widget(*args)
self.settings = parent.settings self.page.pushButton_image.clicked.connect(self.pickImage)
page = self.loadUi('image.ui') self.trackWidgets(
{
page.lineEdit_image.textChanged.connect(self.update) 'imagePath': self.page.lineEdit_image,
page.pushButton_image.clicked.connect(self.pickImage) 'scale': self.page.spinBox_scale,
page.spinBox_scale.valueChanged.connect(self.update) 'rotate': self.page.spinBox_rotate,
page.spinBox_rotate.valueChanged.connect(self.update) 'color': self.page.spinBox_color,
page.spinBox_color.valueChanged.connect(self.update) 'xPosition': self.page.spinBox_x,
page.checkBox_stretch.stateChanged.connect(self.update) 'yPosition': self.page.spinBox_y,
page.checkBox_mirror.stateChanged.connect(self.update) 'stretched': self.page.checkBox_stretch,
page.spinBox_x.valueChanged.connect(self.update) 'mirror': self.page.checkBox_mirror,
page.spinBox_y.valueChanged.connect(self.update) },
presetNames={
self.page = page 'imagePath': 'image',
return page 'xPosition': 'x',
'yPosition': 'y',
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 previewRender(self, previewWorker): def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth')) width = int(self.settings.value('outputWidth'))
@ -89,41 +79,18 @@ class Component(Component):
return frame 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): def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~")) imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName( filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir, self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(Core.imageFormats)) "Image Files (%s)" % " ".join(self.core.imageFormats))
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename) self.page.lineEdit_image.setText(filename)
self.update() self.update()
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg): if key == 'path' and os.path.exists(arg):
try: try:

View File

@ -18,59 +18,46 @@ class Component(Component):
def names(): def names():
return ['Original Audio Visualization'] return ['Original Audio Visualization']
def widget(self, parent): def widget(self, *args):
self.parent = parent
self.settings = parent.settings
self.visColor = (255, 255, 255) self.visColor = (255, 255, 255)
self.scale = 20 self.scale = 20
self.y = 0 self.y = 0
self.canceled = False super().widget(*args)
page = self.loadUi('original.ui') self.page.comboBox_visLayout.addItem("Classic")
page.comboBox_visLayout.addItem("Classic") self.page.comboBox_visLayout.addItem("Split")
page.comboBox_visLayout.addItem("Split") self.page.comboBox_visLayout.addItem("Bottom")
page.comboBox_visLayout.addItem("Bottom") self.page.comboBox_visLayout.addItem("Top")
page.comboBox_visLayout.addItem("Top") self.page.comboBox_visLayout.setCurrentIndex(0)
page.comboBox_visLayout.setCurrentIndex(0)
page.comboBox_visLayout.currentIndexChanged.connect(self.update) self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
page.pushButton_visColor.clicked.connect(lambda: self.pickColor())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.visColor).name() % QColor(*self.visColor).name()
page.pushButton_visColor.setStyleSheet(btnStyle) self.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 = page self.trackWidgets({
return page 'layout': self.page.comboBox_visLayout,
'scale': self.page.spinBox_scale,
'y': self.page.spinBox_y,
})
def update(self): def update(self):
self.layout = self.page.comboBox_visLayout.currentIndex()
self.visColor = rgbFromString(self.page.lineEdit_visColor.text()) self.visColor = rgbFromString(self.page.lineEdit_visColor.text())
self.scale = self.page.spinBox_scale.value()
self.y = self.page.spinBox_y.value()
super().update() super().update()
def loadPreset(self, pr, presetName=None): def loadPreset(self, pr, *args):
super().loadPreset(pr, presetName) super().loadPreset(pr, *args)
self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['visColor']).name() % QColor(*pr['visColor']).name()
self.page.pushButton_visColor.setStyleSheet(btnStyle) 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): def savePreset(self):
return { saveValueStore = super().savePreset()
'layout': self.layout, saveValueStore['visColor'] = self.visColor
'visColor': self.visColor, return saveValueStore
'scale': self.scale,
'y': self.y,
}
def previewRender(self, previewWorker): def previewRender(self, previewWorker):
spectrum = numpy.fromfunction( spectrum = numpy.fromfunction(
@ -206,7 +193,7 @@ class Component(Component):
return im return im
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
try: try:
if key == 'color': if key == 'color':

View File

@ -10,26 +10,15 @@ class Component(Component):
name = 'Sound' name = 'Sound'
version = '1.0.0' version = '1.0.0'
def widget(self, parent): def widget(self, *args):
self.parent = parent super().widget(*args)
self.settings = parent.settings self.page.pushButton_sound.clicked.connect(self.pickSound)
page = self.loadUi('sound.ui') self.trackWidgets({
'sound': self.page.lineEdit_sound,
page.lineEdit_sound.textChanged.connect(self.update) 'chorus': self.page.checkBox_chorus,
page.pushButton_sound.clicked.connect(self.pickSound) 'delay': self.page.spinBox_delay,
page.checkBox_chorus.stateChanged.connect(self.update) 'volume': self.page.spinBox_volume,
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 previewRender(self, previewWorker): def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth')) width = int(self.settings.value('outputWidth'))
@ -67,7 +56,7 @@ class Component(Component):
sndDir = self.settings.value("componentDir", os.path.expanduser("~")) sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName( filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Sound", sndDir, self.page, "Choose Sound", sndDir,
"Audio Files (%s)" % " ".join(Core.audioFormats)) "Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename) self.page.lineEdit_sound.setText(filename)
@ -78,30 +67,15 @@ class Component(Component):
height = int(self.settings.value('outputHeight')) height = int(self.settings.value('outputHeight'))
return BlankFrame(width, height) 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): def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg') print('Path to audio file:\n path=/filepath/to/sound.ogg')
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
if key == 'path': if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \ if '*%s' % os.path.splitext(arg)[1] \
not in Core.audioFormats: not in self.core.audioFormats:
print("Not a supported audio format") print("Not a supported audio format")
quit(1) quit(1)
self.page.lineEdit_sound.setText(arg) self.page.lineEdit_sound.setText(arg)

View File

@ -16,12 +16,10 @@ class Component(Component):
super().__init__(*args) super().__init__(*args)
self.titleFont = QFont() self.titleFont = QFont()
def widget(self, parent): def widget(self, *args):
self.parent = parent super().widget(*args)
self.settings = parent.settings
height = int(self.settings.value('outputHeight')) height = int(self.settings.value('outputHeight'))
width = int(self.settings.value('outputWidth')) width = int(self.settings.value('outputWidth'))
self.textColor = (255, 255, 255) self.textColor = (255, 255, 255)
self.title = 'Text' self.title = 'Text'
self.alignment = 1 self.alignment = 1
@ -30,40 +28,35 @@ class Component(Component):
self.xPosition = width / 2 - fm.width(self.title)/2 self.xPosition = width / 2 - fm.width(self.title)/2
self.yPosition = height / 2 * 1.036 self.yPosition = height / 2 * 1.036
page = self.loadUi('text.ui') self.page.comboBox_textAlign.addItem("Left")
page.comboBox_textAlign.addItem("Left") self.page.comboBox_textAlign.addItem("Middle")
page.comboBox_textAlign.addItem("Middle") self.page.comboBox_textAlign.addItem("Right")
page.comboBox_textAlign.addItem("Right")
page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
page.pushButton_textColor.clicked.connect(self.pickColor) self.page.pushButton_textColor.clicked.connect(self.pickColor)
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.textColor).name() % QColor(*self.textColor).name()
page.pushButton_textColor.setStyleSheet(btnStyle) self.page.pushButton_textColor.setStyleSheet(btnStyle)
page.lineEdit_title.setText(self.title) self.page.lineEdit_title.setText(self.title)
page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
page.spinBox_fontSize.setValue(int(self.fontSize)) self.page.spinBox_fontSize.setValue(int(self.fontSize))
page.spinBox_xTextAlign.setValue(int(self.xPosition)) self.page.spinBox_xTextAlign.setValue(int(self.xPosition))
page.spinBox_yTextAlign.setValue(int(self.yPosition)) self.page.spinBox_yTextAlign.setValue(int(self.yPosition))
page.fontComboBox_titleFont.currentFontChanged.connect(self.update) self.page.fontComboBox_titleFont.currentFontChanged.connect(
page.lineEdit_title.textChanged.connect(self.update) self.update
page.comboBox_textAlign.currentIndexChanged.connect(self.update) )
page.spinBox_xTextAlign.valueChanged.connect(self.update) self.trackWidgets({
page.spinBox_yTextAlign.valueChanged.connect(self.update) 'title': self.page.lineEdit_title,
page.spinBox_fontSize.valueChanged.connect(self.update) 'alignment': self.page.comboBox_textAlign,
page.lineEdit_textColor.textChanged.connect(self.update) 'fontSize': self.page.spinBox_fontSize,
self.page = page 'xPosition': self.page.spinBox_xTextAlign,
return page 'yPosition': self.page.spinBox_yTextAlign,
})
def update(self): 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.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.textColor = rgbFromString(
self.page.lineEdit_textColor.text()) self.page.lineEdit_textColor.text())
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
@ -87,32 +80,22 @@ class Component(Component):
x = self.xPosition - offset x = self.xPosition - offset
return x, self.yPosition return x, self.yPosition
def loadPreset(self, pr, presetName=None): def loadPreset(self, pr, *args):
super().loadPreset(pr, presetName) super().loadPreset(pr, *args)
self.page.lineEdit_title.setText(pr['title'])
font = QFont() font = QFont()
font.fromString(pr['titleFont']) font.fromString(pr['titleFont'])
self.page.fontComboBox_titleFont.setCurrentFont(font) 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']) self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor'])
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*pr['textColor']).name() % QColor(*pr['textColor']).name()
self.page.pushButton_textColor.setStyleSheet(btnStyle) self.page.pushButton_textColor.setStyleSheet(btnStyle)
def savePreset(self): def savePreset(self):
return { saveValueStore = super().savePreset()
'title': self.title, saveValueStore['titleFont'] = self.titleFont.toString()
'titleFont': self.titleFont.toString(), saveValueStore['textColor'] = self.textColor
'alignment': self.alignment, return saveValueStore
'fontSize': self.fontSize,
'xPosition': self.xPosition,
'yPosition': self.yPosition,
'textColor': self.textColor
}
def previewRender(self, previewWorker): def previewRender(self, previewWorker):
width = int(self.settings.value('outputWidth')) width = int(self.settings.value('outputWidth'))
@ -158,7 +141,7 @@ class Component(Component):
print('Set custom x, y position:\n x=500 y=500') print('Set custom x, y position:\n x=500 y=500')
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
if key == 'color': if key == 'color':
self.page.lineEdit_textColor.setText(arg) self.page.lineEdit_textColor.setText(arg)

View File

@ -9,6 +9,7 @@ from queue import PriorityQueue
from core import Core from core import Core
from component import Component, BadComponentInit from component import Component, BadComponentInit
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
from toolkit.ffmpeg import testAudioStream
from toolkit import openPipe, checkOutput from toolkit import openPipe, checkOutput
@ -16,7 +17,7 @@ class Video:
'''Video Component Frame-Fetcher''' '''Video Component Frame-Fetcher'''
def __init__(self, **kwargs): def __init__(self, **kwargs):
mandatoryArgs = [ mandatoryArgs = [
'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN 'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN
'videoPath', 'videoPath',
'width', 'width',
'height', 'height',
@ -110,47 +111,40 @@ class Component(Component):
name = 'Video' name = 'Video'
version = '1.0.0' version = '1.0.0'
def widget(self, parent): def widget(self, *args):
self.parent = parent
self.settings = parent.settings
page = self.loadUi('video.ui')
self.videoPath = '' self.videoPath = ''
self.badVideo = False self.badVideo = False
self.badAudio = False self.badAudio = False
self.x = 0 self.x = 0
self.y = 0 self.y = 0
self.loopVideo = False self.loopVideo = False
super().widget(*args)
page.lineEdit_video.textChanged.connect(self.update) self.page.pushButton_video.clicked.connect(self.pickVideo)
page.pushButton_video.clicked.connect(self.pickVideo) self.trackWidgets(
page.checkBox_loop.stateChanged.connect(self.update) {
page.checkBox_distort.stateChanged.connect(self.update) 'videoPath': self.page.lineEdit_video,
page.checkBox_useAudio.stateChanged.connect(self.update) 'loopVideo': self.page.checkBox_loop,
page.spinBox_scale.valueChanged.connect(self.update) 'useAudio': self.page.checkBox_useAudio,
page.spinBox_volume.valueChanged.connect(self.update) 'distort': self.page.checkBox_distort,
page.spinBox_x.valueChanged.connect(self.update) 'scale': self.page.spinBox_scale,
page.spinBox_y.valueChanged.connect(self.update) 'volume': self.page.spinBox_volume,
'xPosition': self.page.spinBox_x,
self.page = page 'yPosition': self.page.spinBox_y,
return page }, presetNames={
'videoPath': 'video',
'loopVideo': 'loop',
'xPosition': 'x',
'yPosition': 'y',
}
)
def update(self): def update(self):
self.videoPath = self.page.lineEdit_video.text() if self.page.checkBox_useAudio.isChecked():
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:
self.page.label_volume.setEnabled(True) self.page.label_volume.setEnabled(True)
self.page.spinBox_volume.setEnabled(True) self.page.spinBox_volume.setEnabled(True)
else: else:
self.page.label_volume.setEnabled(False) self.page.label_volume.setEnabled(False)
self.page.spinBox_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False)
super().update() super().update()
def previewRender(self, previewWorker): def previewRender(self, previewWorker):
@ -188,18 +182,7 @@ class Component(Component):
return "The video selected is corrupt!" return "The video selected is corrupt!"
def testAudioStream(self): def testAudioStream(self):
# test if an audio stream really exists self.badAudio = testAudioStream(self.videoPath)
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
def audio(self): def audio(self):
params = {} params = {}
@ -214,7 +197,7 @@ class Component(Component):
self.blankFrame_ = BlankFrame(width, height) self.blankFrame_ = BlankFrame(width, height)
self.updateChunksize(width, height) self.updateChunksize(width, height)
self.video = Video( 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, width=width, height=height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")), frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo, parent=self.parent, loopVideo=self.loopVideo,
@ -227,34 +210,11 @@ class Component(Component):
else: else:
return self.blankFrame_ 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): def pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~")) imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName( filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video", self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(Core.videoFormats) imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
) )
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
@ -266,7 +226,7 @@ class Component(Component):
return return
command = [ command = [
self.parent.core.FFMPEG_BIN, self.core.FFMPEG_BIN,
'-thread_queue_size', '512', '-thread_queue_size', '512',
'-i', self.videoPath, '-i', self.videoPath,
'-f', 'image2pipe', '-f', 'image2pipe',
@ -294,10 +254,10 @@ class Component(Component):
self.chunkSize = 4*width*height self.chunkSize = 4*width*height
def command(self, arg): def command(self, arg):
if not arg.startswith('preset=') and '=' in arg: if '=' in arg:
key, arg = arg.split('=', 1) key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg): 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.lineEdit_video.setText(arg)
self.page.spinBox_scale.setValue(100) self.page.spinBox_scale.setValue(100)
self.page.checkBox_loop.setChecked(True) self.page.checkBox_loop.setChecked(True)

View File

@ -1,5 +1,6 @@
''' '''
Home to the Core class which tracks program state. Used by GUI & commandline 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 from PyQt5 import QtCore, QtGui, uic
import sys import sys
@ -8,7 +9,6 @@ import json
from importlib import import_module from importlib import import_module
import toolkit import toolkit
from toolkit.ffmpeg import findFfmpeg
import video_thread import video_thread
@ -16,82 +16,21 @@ class Core:
''' '''
MainWindow and Command module both use an instance of this class MainWindow and Command module both use an instance of this class
to store the core program state. This object tracks the components, to store the core program state. This object tracks the components,
talks to the components and handles opening/creating project files talks to the components, handles opening/creating project files
and presets. The class also stores constants as class variables. 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): def __init__(self):
Core.storeSettings()
self.findComponents() self.findComponents()
self.selectedComponents = [] self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
def findComponents(self): def findComponents(self):
'''Imports all the component modules'''
def findComponents(): def findComponents():
for f in sorted(os.listdir(Core.componentsPath)): for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f) name, ext = os.path.splitext(f)
if name.startswith("__"): if name.startswith("__"):
continue continue
@ -104,8 +43,13 @@ class Core:
# store canonical module names and indexes # store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))] self.moduleIndexes = [i for i in range(len(self.modules))]
self.compNames = [mod.Component.name for mod in 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 # store alternative names for modules
self.altCompNames = []
for i, mod in enumerate(self.modules): for i, mod in enumerate(self.modules):
if hasattr(mod.Component, 'names'): if hasattr(mod.Component, 'names'):
for name in mod.Component.names(): for name in mod.Component.names():
@ -116,14 +60,17 @@ class Core:
component.compPos = i component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader): 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): if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents) compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50: if len(self.selectedComponents) > 50:
return None return None
component = self.modules[moduleIndex].Component( component = self.modules[moduleIndex].Component(
moduleIndex, compPos moduleIndex, compPos, self
) )
self.selectedComponents.insert( self.selectedComponents.insert(
compPos, compPos,
@ -206,6 +153,7 @@ class Core:
errcode, data = self.parseAvFile(filepath) errcode, data = self.parseAvFile(filepath)
if errcode == 0: if errcode == 0:
self.openingProject = True
try: try:
if hasattr(loader, 'window'): if hasattr(loader, 'window'):
for widget, value in data['WindowFields']: for widget, value in data['WindowFields']:
@ -239,7 +187,8 @@ class Core:
i = self.insertComponent( i = self.insertComponent(
-1, -1,
self.moduleIndexFor(name), self.moduleIndexFor(name),
loader) loader
)
if i is None: if i is None:
loader.showMessage(msg="Too many components!") loader.showMessage(msg="Too many components!")
break break
@ -284,6 +233,7 @@ class Core:
showCancel=False, showCancel=False,
icon='Warning', icon='Warning',
detail=msg) detail=msg)
self.openingProject = False
def parseAvFile(self, filepath): def parseAvFile(self, filepath):
'''Parses an avp (project) or avl (preset package) file. '''Parses an avp (project) or avl (preset package) file.
@ -467,8 +417,106 @@ class Core:
def cancel(self): def cancel(self):
Core.canceled = True Core.canceled = True
toolkit.cancel()
def reset(self): def reset(self):
Core.canceled = False 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()

View File

@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets
import sys import sys
import os import os
from __init__ import wd
def main(): def main():
if getattr(sys, 'frozen', False): app = QtWidgets.QApplication(sys.argv)
# frozen app.setApplicationName("audio-visualizer")
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)
# Determine mode
mode = 'GUI' mode = 'GUI'
if len(sys.argv) > 2: if len(sys.argv) > 2:
mode = 'commandline' mode = 'commandline'
elif len(sys.argv) == 2: elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'): if sys.argv[1].startswith('-'):
mode = 'commandline' mode = 'commandline'
@ -28,11 +23,7 @@ def main():
# normal gui launch # normal gui launch
proj = None proj = None
print('Starting Audio Visualizer in %s mode' % mode) # Launch program
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
# app.setOrganizationName("audio-visualizer")
if mode == 'commandline': if mode == 'commandline':
from command import Command from command import Command
@ -61,9 +52,7 @@ def main():
signal.signal(signal.SIGINT, main.cleanUp) signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp) atexit.register(main.cleanUp)
# applicable to both modes
sys.exit(app.exec_()) sys.exit(app.exec_())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -17,7 +17,7 @@ import time
from core import Core from core import Core
import preview_thread import preview_thread
from presetmanager import PresetManager from presetmanager import PresetManager
from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
class PreviewWindow(QtWidgets.QLabel): class PreviewWindow(QtWidgets.QLabel):
@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel):
Paints the preview QLabel and maintains the aspect ratio when the Paints the preview QLabel and maintains the aspect ratio when the
window is resized. window is resized.
''' '''
def __init__(self, parent, img): def __init__(self, parent, img):
super(PreviewWindow, self).__init__() super(PreviewWindow, self).__init__()
self.parent = parent self.parent = parent
@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel):
self.pixmap = QtGui.QPixmap(img) self.pixmap = QtGui.QPixmap(img)
self.repaint() self.repaint()
@QtCore.pyqtSlot(str)
def threadError(self, msg):
self.parent.showMessage(
msg=msg,
icon='Warning',
parent=self
)
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
''' '''
@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project): def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self) QtWidgets.QMainWindow.__init__(self)
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) # print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window self.window = window
self.core = Core() self.core = Core()
self.pages = [] # widgets of component settings # widgets of component settings
self.pages = []
self.lastAutosave = time.time() self.lastAutosave = time.time()
# list of previous five autosave times, used to reduce update spam
self.autosaveTimes = []
self.autosaveCooldown = 0.2
self.encoding = False self.encoding = False
# Create data directory, load/create settings # Create data directory, load/create settings
@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.presetDir = Core.presetDir self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings self.settings = Core.settings
loadDefaultSettings(self)
self.presetManager = PresetManager( self.presetManager = PresetManager(
uic.loadUi( uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self) os.path.join(Core.wd, 'presetmanager.ui')), self)
@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow):
if not os.path.exists(neededDirectory): if not os.path.exists(neededDirectory):
os.mkdir(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.previewQueue = Queue()
self.previewThread = QtCore.QThread(self) self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue) self.previewWorker = preview_thread.Worker(self, self.previewQueue)
self.previewWorker.error.connect(self.previewWindow.threadError)
self.previewWorker.moveToThread(self.previewThread) self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage) self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewWorker.error.connect(self.cleanUp)
self.previewThread.start() self.previewThread.start()
self.timer = QtCore.QTimer(self) self.timer = QtCore.QTimer(self)
@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.timer.start(500) self.timer.start(500)
# Begin decorating the window and connecting events # Begin decorating the window and connecting events
self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
if sys.platform == 'darwin': if sys.platform == 'darwin':
@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow):
window.spinBox_vBitrate.setValue(vBitrate) window.spinBox_vBitrate.setValue(vBitrate)
window.spinBox_aBitrate.setValue(aBitrate) window.spinBox_aBitrate.setValue(aBitrate)
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
window.spinBox_aBitrate.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 # Make component buttons
self.compMenu = QMenu() self.compMenu = QMenu()
for i, comp in enumerate(self.core.modules): for i, comp in enumerate(self.core.modules):
@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow):
currentRes = str(self.settings.value('outputWidth'))+'x' + \ currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight')) str(self.settings.value('outputHeight'))
for i, res in enumerate(self.resolutions): for i, res in enumerate(Core.resolutions):
window.comboBox_resolution.addItem(res) window.comboBox_resolution.addItem(res)
if res == currentRes: if res == currentRes:
currentRes = i currentRes = i
@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewThread.quit() self.previewThread.quit()
self.previewThread.wait() self.previewThread.wait()
@disableWhenOpeningProject
def updateWindowTitle(self): def updateWindowTitle(self):
appName = 'Audio Visualizer' appName = 'Audio Visualizer'
try: try:
@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue('outputVideoBitrate', currentVideoBitrate) self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
self.settings.setValue('outputAudioBitrate', currentAudioBitrate) self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
@disableWhenOpeningProject
def autosave(self, force=False): def autosave(self, force=False):
if not self.currentProject: if not self.currentProject:
if os.path.exists(self.autosavePath): if os.path.exists(self.autosavePath):
os.remove(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.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time() 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): def autosaveExists(self, identical=True):
'''Determines if creating the autosave should be blocked.''' '''Determines if creating the autosave should be blocked.'''
@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow):
def updateResolution(self): def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex()) 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('outputWidth', res[0])
self.settings.setValue('outputHeight', res[1]) self.settings.setValue('outputHeight', res[1])
self.drawPreview() 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.newTask.emit(self.core.selectedComponents)
# self.processTask.emit() # 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() self.updateWindowTitle()
@QtCore.pyqtSlot(QtGui.QImage) @QtCore.pyqtSlot(QtGui.QImage)
@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow):
stackedWidget.insertWidget(newRow, page) stackedWidget.insertWidget(newRow, page)
componentList.setCurrentRow(newRow) componentList.setCurrentRow(newRow)
stackedWidget.setCurrentIndex(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 componentList = self.window.listWidget_componentList
modelIndexes = [ modelIndexes = [
@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow):
componentList.visualRect(modelIndex) componentList.visualRect(modelIndex)
for modelIndex in modelIndexes 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 @disableWhenEncoding
def dragComponent(self, event): def dragComponent(self, event):
'''Used as Qt drop event for the component listwidget''' '''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
rects = self.getComponentListRects() mousePos = self.getComponentListMousePos(event.pos())
if mousePos > -1:
rowPos = [rect.contains(event.pos()) for rect in rects] change = (componentList.currentRow() - mousePos) * -1
if not any(rowPos): else:
return change = (componentList.count() - componentList.currentRow() -1)
i = rowPos.index(True)
change = (componentList.currentRow() - i) * -1
self.moveComponent(change) self.moveComponent(change)
def changeComponentWidget(self): def changeComponentWidget(self):
@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.settings.setValue("projectDir", os.path.dirname(filepath)) self.settings.setValue("projectDir", os.path.dirname(filepath))
# actually load the project using core method # actually load the project using core method
self.core.openProject(self, filepath) self.core.openProject(self, filepath)
if self.window.listWidget_componentList.count() == 0: self.drawPreview(autosave=False)
self.drawPreview()
self.autosave(True)
self.updateWindowTitle() self.updateWindowTitle()
def showMessage(self, **kwargs): def showMessage(self, **kwargs):
@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow):
def componentContextMenu(self, QPos): def componentContextMenu(self, QPos):
'''Appears when right-clicking the component list''' '''Appears when right-clicking the component list'''
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
index = componentList.currentRow()
self.menu = QMenu() self.menu = QMenu()
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
rects = self.getComponentListRects() index = self.getComponentListMousePos(QPos)
rowPos = [rect.contains(QPos) for rect in rects] if index > -1:
if not any(rowPos):
# Insert components at the top if clicking nothing
rowPos = 0
else:
rowPos = rowPos.index(True)
if index == rowPos:
# Show preset menu if clicking a component # Show preset menu if clicking a component
self.presetManager.findPresets() self.presetManager.findPresets()
menuItem = self.menu.addAction("Save Preset") menuItem = self.menu.addAction("Save Preset")
@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow):
# "Add Component" submenu # "Add Component" submenu
self.submenu = QMenu("Add") self.submenu = QMenu("Add")
self.menu.addMenu(self.submenu) self.menu.addMenu(self.submenu)
insertCompAtTop = self.settings.value("pref_insertCompAtTop")
for i, comp in enumerate(self.core.modules): for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name) menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect( menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent( lambda _, item=i: self.core.insertComponent(
rowPos, item, self 0 if insertCompAtTop else index, item, self
) )
) )
self.menu.move(parentPosition + QPos) self.menu.move(parentPosition + QPos)
self.menu.show() 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

View File

@ -22,6 +22,9 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>MainWindow</string> <string>MainWindow</string>
</property> </property>

View File

@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets
import string import string
import os import os
import toolkit from toolkit import badName
from core import Core
class PresetManager(QtWidgets.QDialog): class PresetManager(QtWidgets.QDialog):
@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog):
currentPreset currentPreset
) )
if OK: if OK:
if toolkit.badName(newName): if badName(newName):
self.warnMessage(self.parent.window) self.warnMessage(self.parent.window)
continue continue
if newName: if newName:
@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog):
os.remove(filepath) os.remove(filepath)
def warnMessage(self, window=None): def warnMessage(self, window=None):
print(window)
self.parent.showMessage( self.parent.showMessage(
msg='Preset names must contain only letters, ' msg='Preset names must contain only letters, '
'numbers, and spaces.', 'numbers, and spaces.',
@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog):
self.presetRows[index][2] self.presetRows[index][2]
) )
if OK: if OK:
if toolkit.badName(newName): if badName(newName):
self.warnMessage() self.warnMessage()
continue continue
if newName: if newName:
@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog):
self.findPresets() self.findPresets()
self.drawPresetList() self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents): for i, comp in enumerate(self.core.selectedComponents):
if toolkit.getPresetDir(comp) == path \ if getPresetDir(comp) == path \
and comp.currentPreset == oldName: and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName) self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False) self.parent.updateComponentTitle(i, False)
@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog):
def clearPresetListSelection(self): def clearPresetListSelection(self):
self.window.listWidget_presets.setCurrentRow(-1) 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))

View File

@ -10,12 +10,13 @@ from queue import Queue, Empty
import os import os
from toolkit.frame import Checkerboard from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
class Worker(QtCore.QObject): class Worker(QtCore.QObject):
imageCreated = pyqtSignal(QtGui.QImage) imageCreated = pyqtSignal(QtGui.QImage)
error = pyqtSignal() error = pyqtSignal(str)
def __init__(self, parent=None, queue=None): def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
@ -30,6 +31,7 @@ class Worker(QtCore.QObject):
height = int(self.settings.value('outputHeight')) height = int(self.settings.value('outputHeight'))
self.background = Checkerboard(width, height) self.background = Checkerboard(width, height)
@disableWhenOpeningProject
@pyqtSlot(list) @pyqtSlot(list)
def createPreviewImage(self, components): def createPreviewImage(self, components):
dic = { dic = {
@ -48,7 +50,6 @@ class Worker(QtCore.QObject):
self.queue.get(block=False) self.queue.get(block=False)
except Empty: except Empty:
continue continue
if self.background.width != width \ if self.background.width != width \
or self.background.height != height: or self.background.height != height:
self.background = Checkerboard(width, height) self.background = Checkerboard(width, height)
@ -65,20 +66,12 @@ class Worker(QtCore.QObject):
except ValueError as e: except ValueError as e:
errMsg = "Bad frame returned by %s's preview renderer. " \ errMsg = "Bad frame returned by %s's preview renderer. " \
"%s. New frame size was %s*%s; should be %s*%s. " \ "%s. New frame size was %s*%s; should be %s*%s." % (
"This is a fatal error." % (
str(component), str(e).capitalize(), str(component), str(e).capitalize(),
newFrame.width, newFrame.height, newFrame.width, newFrame.height,
width, height width, height
) )
print(errMsg) self.error.emit(errMsg)
self.parent.showMessage(
msg=errMsg,
detail=str(e),
icon='Warning',
parent=None # MainWindow is in a different thread
)
self.error.emit()
break break
except RuntimeError as e: except RuntimeError as e:
print(e) print(e)

View File

@ -8,13 +8,6 @@ import sys
import subprocess import subprocess
from collections import OrderedDict 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): def badName(name):
'''Returns whether a name contains non-alphanumeric chars''' '''Returns whether a name contains non-alphanumeric chars'''
@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs):
def disableWhenEncoding(func): def disableWhenEncoding(func):
''' Blocks calls to a function while the video is being exported def decorator(self, *args, **kwargs):
in MainWindow. if self.encoding:
'''
def decorator(*args, **kwargs):
if args[0].encoding:
return return
else: 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 return decorator
@ -108,34 +107,3 @@ def rgbFromString(string):
return tup return tup
except: except:
return (255, 255, 255) 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)

View File

@ -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

View File

@ -4,18 +4,19 @@
import numpy import numpy
import sys import sys
import os 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(): def findFfmpeg():
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# The application is frozen # The application is frozen
if sys.platform == "win32": if sys.platform == "win32":
return os.path.join(Core.wd, 'ffmpeg.exe') return os.path.join(core.Core.wd, 'ffmpeg.exe')
else: else:
return os.path.join(Core.wd, 'ffmpeg') return os.path.join(core.Core.wd, 'ffmpeg')
else: else:
if sys.platform == "win32": if sys.platform == "win32":
@ -27,7 +28,7 @@ def findFfmpeg():
['ffmpeg', '-version'], stderr=f ['ffmpeg', '-version'], stderr=f
) )
return "ffmpeg" return "ffmpeg"
except sp.CalledProcessError: except subprocess.CalledProcessError:
return "avconv" return "avconv"
@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
''' '''
if duration == -1: if duration == -1:
duration = getAudioDuration(inputFile) duration = getAudioDuration(inputFile)
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
duration = "{0:.3f}".format(duration + 0.1) # used by input sources duration = "{0:.3f}".format(duration + 0.1) # used by input sources
Core = core.Core
# Test if user has libfdk_aac # Test if user has libfdk_aac
encoders = checkOutput( encoders = checkOutput(
@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
return ffmpegCommand 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): 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: try:
fileInfo = checkOutput(command, stderr=sp.STDOUT) fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
except sp.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
fileInfo = ex.output fileInfo = ex.output
info = fileInfo.decode("utf-8").split('\n') info = fileInfo.decode("utf-8").split('\n')
@ -236,13 +253,17 @@ def getAudioDuration(filename):
def readAudioFile(filename, parent): def readAudioFile(filename, parent):
'''
Creates the completeAudioArray given to components
and used to draw the classic visualizer.
'''
duration = getAudioDuration(filename) duration = getAudioDuration(filename)
if not duration: if not duration:
print('Audio file doesn\'t exist or unreadable.') print('Audio file doesn\'t exist or unreadable.')
return return
command = [ command = [
Core.FFMPEG_BIN, core.Core.FFMPEG_BIN,
'-i', filename, '-i', filename,
'-f', 's16le', '-f', 's16le',
'-acodec', 'pcm_s16le', '-acodec', 'pcm_s16le',
@ -250,7 +271,8 @@ def readAudioFile(filename, parent):
'-ac', '1', # mono (set to '2' for stereo) '-ac', '1', # mono (set to '2' for stereo)
'-'] '-']
in_pipe = openPipe( 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") completeAudioArray = numpy.empty(0, dtype="int16")
@ -258,7 +280,7 @@ def readAudioFile(filename, parent):
progress = 0 progress = 0
lastPercent = None lastPercent = None
while True: while True:
if Core.canceled: if core.Core.canceled:
return return
# read 2 seconds of audio # read 2 seconds of audio
progress += 4 progress += 4

View File

@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt
import sys import sys
import os import os
from toolkit.common import Core import core
class FramePainter(QtGui.QPainter): class FramePainter(QtGui.QPainter):
@ -57,7 +57,7 @@ def Checkerboard(width, height):
''' '''
image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open( image.paste(Image.open(
os.path.join(Core.wd, "background.png")), os.path.join(core.Core.wd, "background.png")),
(0, 0) (0, 0)
) )
image = image.resize((width, height)) image = image.resize((width, height))

View File

@ -18,6 +18,7 @@ from threading import Thread, Event
import time import time
import signal import signal
import core
from toolkit import openPipe from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard from toolkit.frame import Checkerboard
@ -104,7 +105,8 @@ class Worker(QtCore.QObject):
while not self.stopped: while not self.stopped:
audioI, frame = self.previewQueue.get() 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) image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image))) self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time() self.lastPreview = time.time()
@ -231,7 +233,8 @@ class Worker(QtCore.QObject):
self.lastPreview = 0.0 self.lastPreview = 0.0
self.previewDispatch = Thread( self.previewDispatch = Thread(
target=self.previewDispatch, name="Render Dispatch Thread") target=self.previewDispatch, name="Render Dispatch Thread"
)
self.previewDispatch.daemon = True self.previewDispatch.daemon = True
self.previewDispatch.start() self.previewDispatch.start()