Undo feature
This commit is contained in:
commit
22978a0635
2
setup.py
2
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
__version__ = '2.0.0.rc4'
|
__version__ = '2.0.0rc5'
|
||||||
|
|
||||||
|
|
||||||
def package_files(directory):
|
def package_files(directory):
|
||||||
|
|
|
@ -8,6 +8,7 @@ import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import signal
|
||||||
|
|
||||||
from core import Core
|
from core import Core
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ class Command(QtCore.QObject):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QtCore.QObject.__init__(self)
|
QtCore.QObject.__init__(self)
|
||||||
self.core = Core()
|
self.core = Core()
|
||||||
|
Core.mode = 'commandline'
|
||||||
self.dataDir = self.core.dataDir
|
self.dataDir = self.core.dataDir
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
|
|
||||||
|
@ -90,6 +92,9 @@ class Command(QtCore.QObject):
|
||||||
for arg in args:
|
for arg in args:
|
||||||
self.core.selectedComponents[i].command(arg)
|
self.core.selectedComponents[i].command(arg)
|
||||||
|
|
||||||
|
# ctrl-c stops the export thread
|
||||||
|
signal.signal(signal.SIGINT, self.stopVideo)
|
||||||
|
|
||||||
if self.args.export and self.args.projpath:
|
if self.args.export and self.args.projpath:
|
||||||
errcode, data = self.core.parseAvFile(projPath)
|
errcode, data = self.core.parseAvFile(projPath)
|
||||||
for key, value in data['WindowFields']:
|
for key, value in data['WindowFields']:
|
||||||
|
@ -123,6 +128,11 @@ class Command(QtCore.QObject):
|
||||||
self.worker.progressBarSetText.connect(self.progressBarSetText)
|
self.worker.progressBarSetText.connect(self.progressBarSetText)
|
||||||
self.createVideo.emit()
|
self.createVideo.emit()
|
||||||
|
|
||||||
|
def stopVideo(self, *args):
|
||||||
|
self.worker.error = True
|
||||||
|
self.worker.cancelExport()
|
||||||
|
self.worker.cancel()
|
||||||
|
|
||||||
@QtCore.pyqtSlot(str)
|
@QtCore.pyqtSlot(str)
|
||||||
def progressBarSetText(self, value):
|
def progressBarSetText(self, value):
|
||||||
if 'Export ' in value:
|
if 'Export ' in value:
|
||||||
|
|
461
src/component.py
461
src/component.py
|
@ -9,10 +9,11 @@ import sys
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
from toolkit.frame import BlankFrame
|
from toolkit.frame import BlankFrame
|
||||||
from toolkit import (
|
from toolkit import (
|
||||||
getWidgetValue, setWidgetValue, connectWidget, rgbFromString
|
getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,11 +40,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
||||||
def renderWrapper(func):
|
def renderWrapper(func):
|
||||||
def renderWrapper(self, *args, **kwargs):
|
def renderWrapper(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
log.verbose('### %s #%s renders%s frame %s###' % (
|
log.verbose(
|
||||||
|
'### %s #%s renders a preview frame ###',
|
||||||
self.__class__.name, str(self.compPos),
|
self.__class__.name, str(self.compPos),
|
||||||
'' if args else ' a preview',
|
)
|
||||||
'' if not args else '%s ' % args[0],
|
|
||||||
))
|
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
|
@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
||||||
'''Intercepts the command() method to check for global args'''
|
'''Intercepts the command() method to check for global args'''
|
||||||
def commandWrapper(self, arg):
|
def commandWrapper(self, arg):
|
||||||
if arg.startswith('preset='):
|
if arg.startswith('preset='):
|
||||||
from presetmanager import getPresetDir
|
|
||||||
_, preset = arg.split('=', 1)
|
_, preset = arg.split('=', 1)
|
||||||
path = os.path.join(getPresetDir(self), preset)
|
path = os.path.join(self.core.getPresetDir(self), preset)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
print('Couldn\'t locate preset "%s"' % preset)
|
print('Couldn\'t locate preset "%s"' % preset)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
@ -100,6 +99,92 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
||||||
return func(self)
|
return func(self)
|
||||||
return errorWrapper
|
return errorWrapper
|
||||||
|
|
||||||
|
def loadPresetWrapper(func):
|
||||||
|
'''Wraps loadPreset to handle the self.openingPreset boolean'''
|
||||||
|
class openingPreset:
|
||||||
|
def __init__(self, comp):
|
||||||
|
self.comp = comp
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.comp.openingPreset = True
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.comp.openingPreset = False
|
||||||
|
|
||||||
|
def presetWrapper(self, *args):
|
||||||
|
with openingPreset(self):
|
||||||
|
try:
|
||||||
|
return func(self, *args)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
raise ComponentError(self, 'preset loader')
|
||||||
|
except ComponentError:
|
||||||
|
return
|
||||||
|
return presetWrapper
|
||||||
|
|
||||||
|
def updateWrapper(func):
|
||||||
|
'''
|
||||||
|
Calls _preUpdate before every subclass update().
|
||||||
|
Afterwards, for non-user updates, calls _autoUpdate().
|
||||||
|
For undoable updates triggered by the user, calls _userUpdate()
|
||||||
|
'''
|
||||||
|
class wrap:
|
||||||
|
def __init__(self, comp, auto):
|
||||||
|
self.comp = comp
|
||||||
|
self.auto = auto
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.comp._preUpdate()
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
if self.auto or self.comp.openingPreset \
|
||||||
|
or not hasattr(self.comp.parent, 'undoStack'):
|
||||||
|
log.verbose('Automatic update')
|
||||||
|
self.comp._autoUpdate()
|
||||||
|
else:
|
||||||
|
log.verbose('User update')
|
||||||
|
self.comp._userUpdate()
|
||||||
|
|
||||||
|
def updateWrapper(self, **kwargs):
|
||||||
|
auto = kwargs['auto'] if 'auto' in kwargs else False
|
||||||
|
with wrap(self, auto):
|
||||||
|
try:
|
||||||
|
return func(self)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
raise ComponentError(self, 'update method')
|
||||||
|
except ComponentError:
|
||||||
|
return
|
||||||
|
return updateWrapper
|
||||||
|
|
||||||
|
def widgetWrapper(func):
|
||||||
|
'''Connects all widgets to update method after the subclass's method'''
|
||||||
|
class wrap:
|
||||||
|
def __init__(self, comp):
|
||||||
|
self.comp = comp
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
for widgetList in self.comp._allWidgets.values():
|
||||||
|
for widget in widgetList:
|
||||||
|
log.verbose('Connecting %s', str(
|
||||||
|
widget.__class__.__name__))
|
||||||
|
connectWidget(widget, self.comp.update)
|
||||||
|
|
||||||
|
def widgetWrapper(self, *args, **kwargs):
|
||||||
|
auto = kwargs['auto'] if 'auto' in kwargs else False
|
||||||
|
with wrap(self):
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
raise ComponentError(self, 'widget creation')
|
||||||
|
except ComponentError:
|
||||||
|
return
|
||||||
|
return widgetWrapper
|
||||||
|
|
||||||
def __new__(cls, name, parents, attrs):
|
def __new__(cls, name, parents, attrs):
|
||||||
if 'ui' not in attrs:
|
if 'ui' not in attrs:
|
||||||
# Use module name as ui filename by default
|
# Use module name as ui filename by default
|
||||||
|
@ -107,54 +192,55 @@ class ComponentMetaclass(type(QtCore.QObject)):
|
||||||
attrs['__module__'].split('.')[-1]
|
attrs['__module__'].split('.')[-1]
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
# if parents[0] == QtCore.QObject: else:
|
|
||||||
decorate = (
|
decorate = (
|
||||||
'names', # Class methods
|
'names', # Class methods
|
||||||
'error', 'audio', 'properties', # Properties
|
'error', 'audio', 'properties', # Properties
|
||||||
'preFrameRender', 'previewRender',
|
'preFrameRender', 'previewRender',
|
||||||
'frameRender', 'command',
|
'loadPreset', 'command',
|
||||||
|
'update', 'widget',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Auto-decorate methods
|
# Auto-decorate methods
|
||||||
for key in decorate:
|
for key in decorate:
|
||||||
if key not in attrs:
|
if key not in attrs:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if key in ('names'):
|
if key in ('names'):
|
||||||
attrs[key] = classmethod(attrs[key])
|
attrs[key] = classmethod(attrs[key])
|
||||||
|
elif key in ('audio'):
|
||||||
if key in ('audio'):
|
|
||||||
attrs[key] = property(attrs[key])
|
attrs[key] = property(attrs[key])
|
||||||
|
elif key == 'command':
|
||||||
if key == 'command':
|
|
||||||
attrs[key] = cls.commandWrapper(attrs[key])
|
attrs[key] = cls.commandWrapper(attrs[key])
|
||||||
|
elif key == 'previewRender':
|
||||||
if key in ('previewRender', 'frameRender'):
|
|
||||||
attrs[key] = cls.renderWrapper(attrs[key])
|
attrs[key] = cls.renderWrapper(attrs[key])
|
||||||
|
elif key == 'preFrameRender':
|
||||||
if key == 'preFrameRender':
|
|
||||||
attrs[key] = cls.initializationWrapper(attrs[key])
|
attrs[key] = cls.initializationWrapper(attrs[key])
|
||||||
|
elif key == 'properties':
|
||||||
if key == 'properties':
|
|
||||||
attrs[key] = cls.propertiesWrapper(attrs[key])
|
attrs[key] = cls.propertiesWrapper(attrs[key])
|
||||||
|
elif key == 'error':
|
||||||
if key == 'error':
|
|
||||||
attrs[key] = cls.errorWrapper(attrs[key])
|
attrs[key] = cls.errorWrapper(attrs[key])
|
||||||
|
elif key == 'loadPreset':
|
||||||
|
attrs[key] = cls.loadPresetWrapper(attrs[key])
|
||||||
|
elif key == 'update':
|
||||||
|
attrs[key] = cls.updateWrapper(attrs[key])
|
||||||
|
elif key == 'widget' and parents[0] != QtCore.QObject:
|
||||||
|
attrs[key] = cls.widgetWrapper(attrs[key])
|
||||||
|
|
||||||
# Turn version string into a number
|
# Turn version string into a number
|
||||||
try:
|
try:
|
||||||
if 'version' not in attrs:
|
if 'version' not in attrs:
|
||||||
log.error(
|
log.error(
|
||||||
'No version attribute in %s. Defaulting to 1' %
|
'No version attribute in %s. Defaulting to 1',
|
||||||
attrs['name'])
|
attrs['name'])
|
||||||
attrs['version'] = 1
|
attrs['version'] = 1
|
||||||
else:
|
else:
|
||||||
attrs['version'] = int(attrs['version'].split('.')[0])
|
attrs['version'] = int(attrs['version'].split('.')[0])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.critical('%s component has an invalid version string:\n%s' % (
|
log.critical(
|
||||||
attrs['name'], str(attrs['version'])))
|
'%s component has an invalid version string:\n%s',
|
||||||
|
attrs['name'], str(attrs['version'])
|
||||||
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.critical('%s component has no version string.' % attrs['name'])
|
log.critical('%s component has no version string.', attrs['name'])
|
||||||
else:
|
else:
|
||||||
return super().__new__(cls, name, parents, attrs)
|
return super().__new__(cls, name, parents, attrs)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
@ -180,23 +266,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
self.moduleIndex = moduleIndex
|
self.moduleIndex = moduleIndex
|
||||||
self.compPos = compPos
|
self.compPos = compPos
|
||||||
self.core = core
|
self.core = core
|
||||||
self.currentPreset = None
|
|
||||||
|
|
||||||
|
# STATUS VARIABLES
|
||||||
|
self.currentPreset = None
|
||||||
|
self._allWidgets = {}
|
||||||
self._trackedWidgets = {}
|
self._trackedWidgets = {}
|
||||||
self._presetNames = {}
|
self._presetNames = {}
|
||||||
self._commandArgs = {}
|
self._commandArgs = {}
|
||||||
self._colorWidgets = {}
|
self._colorWidgets = {}
|
||||||
self._colorFuncs = {}
|
self._colorFuncs = {}
|
||||||
self._relativeWidgets = {}
|
self._relativeWidgets = {}
|
||||||
# pixel values stored as floats
|
# Pixel values stored as floats
|
||||||
self._relativeValues = {}
|
self._relativeValues = {}
|
||||||
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
|
# Maximum values of spinBoxes at 1080p (Core.resolutions[0])
|
||||||
self._relativeMaximums = {}
|
self._relativeMaximums = {}
|
||||||
|
|
||||||
|
# LOCKING VARIABLES
|
||||||
|
self.openingPreset = False
|
||||||
|
self.mergeUndo = True
|
||||||
self._lockedProperties = None
|
self._lockedProperties = None
|
||||||
self._lockedError = None
|
self._lockedError = None
|
||||||
self._lockedSize = None
|
self._lockedSize = None
|
||||||
|
# If set to a dict, values are used as basis to update relative widgets
|
||||||
|
self.oldAttrs = None
|
||||||
# Stop lengthy processes in response to this variable
|
# Stop lengthy processes in response to this variable
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
|
|
||||||
|
@ -204,12 +296,20 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
return self.__class__.name
|
return self.__class__.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
import pprint
|
||||||
try:
|
try:
|
||||||
preset = self.savePreset()
|
preset = self.savePreset()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
preset = '%s occurred while saving preset' % str(e)
|
preset = '%s occurred while saving preset' % str(e)
|
||||||
return '%s\n%s\n%s' % (
|
|
||||||
self.__class__.name, str(self.__class__.version), preset
|
return (
|
||||||
|
'Component(module %s, pos %s) (%s)\n'
|
||||||
|
'Name: %s v%s\nPreset: %s' % (
|
||||||
|
self.moduleIndex, self.compPos,
|
||||||
|
object.__repr__(self),
|
||||||
|
self.__class__.name, str(self.__class__.version),
|
||||||
|
pprint.pformat(preset)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
@ -288,54 +388,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
'''
|
'''
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.settings = parent.settings
|
self.settings = parent.settings
|
||||||
|
log.verbose(
|
||||||
|
'Creating UI for %s #%s\'s widget',
|
||||||
|
self.__class__.name, self.compPos
|
||||||
|
)
|
||||||
self.page = self.loadUi(self.__class__.ui)
|
self.page = self.loadUi(self.__class__.ui)
|
||||||
|
|
||||||
# Connect widget signals
|
# Find all normal widgets which will be connected after subclass method
|
||||||
widgets = {
|
self._allWidgets = {
|
||||||
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
|
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
|
||||||
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
|
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
|
||||||
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
|
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
|
||||||
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
|
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
|
||||||
}
|
}
|
||||||
widgets['spinBox'].extend(
|
self._allWidgets['spinBox'].extend(
|
||||||
self.page.findChildren(QtWidgets.QDoubleSpinBox)
|
self.page.findChildren(QtWidgets.QDoubleSpinBox)
|
||||||
)
|
)
|
||||||
for widgetList in widgets.values():
|
|
||||||
for widget in widgetList:
|
|
||||||
connectWidget(widget, self.update)
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
'''
|
'''
|
||||||
Reads all tracked widget values into instance attributes
|
Starting point for a component update. A subclass should override
|
||||||
and tells the MainWindow that the component was modified.
|
this method, and the base class will then magically insert a call
|
||||||
Call super() at the END if you need to subclass this.
|
to either _autoUpdate() or _userUpdate() at the end.
|
||||||
'''
|
'''
|
||||||
for attr, widget in self._trackedWidgets.items():
|
|
||||||
if attr in self._colorWidgets:
|
|
||||||
# Color Widgets: text stored as tuple & update the button color
|
|
||||||
rgbTuple = rgbFromString(widget.text())
|
|
||||||
btnStyle = (
|
|
||||||
"QPushButton { background-color : %s; outline: none; }"
|
|
||||||
% QColor(*rgbTuple).name())
|
|
||||||
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
|
||||||
setattr(self, attr, rgbTuple)
|
|
||||||
|
|
||||||
elif attr in self._relativeWidgets:
|
|
||||||
# Relative widgets: number scales to fit export resolution
|
|
||||||
self.updateRelativeWidget(attr)
|
|
||||||
setattr(self, attr, self._trackedWidgets[attr].value())
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Normal tracked widget
|
|
||||||
setattr(self, attr, getWidgetValue(widget))
|
|
||||||
self.sendUpdateSignal()
|
|
||||||
|
|
||||||
def sendUpdateSignal(self):
|
|
||||||
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):
|
def loadPreset(self, presetDict, presetName=None):
|
||||||
'''
|
'''
|
||||||
|
@ -348,7 +423,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
for attr, widget in self._trackedWidgets.items():
|
for attr, widget in self._trackedWidgets.items():
|
||||||
key = attr if attr not in self._presetNames \
|
key = attr if attr not in self._presetNames \
|
||||||
else self._presetNames[attr]
|
else self._presetNames[attr]
|
||||||
val = presetDict[key]
|
try:
|
||||||
|
val = presetDict[key]
|
||||||
|
except KeyError as e:
|
||||||
|
log.info(
|
||||||
|
'%s missing value %s. Outdated preset?',
|
||||||
|
self.currentPreset, str(e)
|
||||||
|
)
|
||||||
|
val = getattr(self, key)
|
||||||
|
|
||||||
if attr in self._colorWidgets:
|
if attr in self._colorWidgets:
|
||||||
widget.setText('%s,%s,%s' % val)
|
widget.setText('%s,%s,%s' % val)
|
||||||
|
@ -403,6 +485,85 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
# "Private" Methods
|
# "Private" Methods
|
||||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
def _preUpdate(self):
|
||||||
|
'''Happens before subclass update()'''
|
||||||
|
for attr in self._relativeWidgets:
|
||||||
|
self.updateRelativeWidget(attr)
|
||||||
|
|
||||||
|
def _userUpdate(self):
|
||||||
|
'''Happens after subclass update() for an undoable update by user.'''
|
||||||
|
oldWidgetVals = {
|
||||||
|
attr: copy(getattr(self, attr))
|
||||||
|
for attr in self._trackedWidgets
|
||||||
|
}
|
||||||
|
newWidgetVals = {
|
||||||
|
attr: getWidgetValue(widget)
|
||||||
|
if attr not in self._colorWidgets else rgbFromString(widget.text())
|
||||||
|
for attr, widget in self._trackedWidgets.items()
|
||||||
|
}
|
||||||
|
modifiedWidgets = {
|
||||||
|
attr: val
|
||||||
|
for attr, val in newWidgetVals.items()
|
||||||
|
if val != oldWidgetVals[attr]
|
||||||
|
}
|
||||||
|
if modifiedWidgets:
|
||||||
|
action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets)
|
||||||
|
self.parent.undoStack.push(action)
|
||||||
|
|
||||||
|
def _autoUpdate(self):
|
||||||
|
'''Happens after subclass update() for an internal component update.'''
|
||||||
|
newWidgetVals = {
|
||||||
|
attr: getWidgetValue(widget)
|
||||||
|
for attr, widget in self._trackedWidgets.items()
|
||||||
|
}
|
||||||
|
self.setAttrs(newWidgetVals)
|
||||||
|
self._sendUpdateSignal()
|
||||||
|
|
||||||
|
def setAttrs(self, attrDict):
|
||||||
|
'''
|
||||||
|
Sets attrs (linked to trackedWidgets) in this component to
|
||||||
|
the values in the attrDict. Mutates certain widget values if needed
|
||||||
|
'''
|
||||||
|
for attr, val in attrDict.items():
|
||||||
|
if attr in self._colorWidgets:
|
||||||
|
# Color Widgets must have a tuple & have a button to update
|
||||||
|
if type(val) is tuple:
|
||||||
|
rgbTuple = val
|
||||||
|
else:
|
||||||
|
rgbTuple = rgbFromString(val)
|
||||||
|
btnStyle = (
|
||||||
|
"QPushButton { background-color : %s; outline: none; }"
|
||||||
|
% QColor(*rgbTuple).name())
|
||||||
|
self._colorWidgets[attr].setStyleSheet(btnStyle)
|
||||||
|
setattr(self, attr, rgbTuple)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Normal tracked widget
|
||||||
|
setattr(self, attr, val)
|
||||||
|
log.verbose('Setting %s self.%s to %s' % (
|
||||||
|
self.__class__.name, attr, val))
|
||||||
|
|
||||||
|
def setWidgetValues(self, attrDict):
|
||||||
|
'''
|
||||||
|
Sets widgets defined by keys in trackedWidgets in this preset to
|
||||||
|
the values in the attrDict.
|
||||||
|
'''
|
||||||
|
affectedWidgets = [
|
||||||
|
self._trackedWidgets[attr] for attr in attrDict
|
||||||
|
]
|
||||||
|
with blockSignals(affectedWidgets):
|
||||||
|
for attr, val in attrDict.items():
|
||||||
|
widget = self._trackedWidgets[attr]
|
||||||
|
if attr in self._colorWidgets:
|
||||||
|
val = '%s,%s,%s' % val
|
||||||
|
setWidgetValue(widget, val)
|
||||||
|
|
||||||
|
def _sendUpdateSignal(self):
|
||||||
|
if not self.core.openingProject:
|
||||||
|
self.parent.drawPreview()
|
||||||
|
saveValueStore = self.savePreset()
|
||||||
|
saveValueStore['preset'] = self.currentPreset
|
||||||
|
self.modified.emit(self.compPos, saveValueStore)
|
||||||
|
|
||||||
def trackWidgets(self, trackDict, **kwargs):
|
def trackWidgets(self, trackDict, **kwargs):
|
||||||
'''
|
'''
|
||||||
|
@ -412,6 +573,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
Optional args:
|
Optional args:
|
||||||
'presetNames': preset variable names to replace attr names
|
'presetNames': preset variable names to replace attr names
|
||||||
'commandArgs': arg keywords that differ from attr names
|
'commandArgs': arg keywords that differ from attr names
|
||||||
|
'colorWidgets': identify attr as RGB tuple & update button CSS
|
||||||
|
'relativeWidgets': change value proportionally to resolution
|
||||||
|
|
||||||
NOTE: Any kwarg key set to None will selectively disable tracking.
|
NOTE: Any kwarg key set to None will selectively disable tracking.
|
||||||
'''
|
'''
|
||||||
|
@ -424,7 +587,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
'colorWidgets',
|
'colorWidgets',
|
||||||
'relativeWidgets',
|
'relativeWidgets',
|
||||||
):
|
):
|
||||||
setattr(self, '_%s' % kwarg, kwargs[kwarg])
|
setattr(self, '_{}'.format(kwarg), kwargs[kwarg])
|
||||||
else:
|
else:
|
||||||
raise ComponentError(
|
raise ComponentError(
|
||||||
self, 'Nonsensical keywords to trackWidgets.')
|
self, 'Nonsensical keywords to trackWidgets.')
|
||||||
|
@ -434,10 +597,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
if kwarg == 'colorWidgets':
|
if kwarg == 'colorWidgets':
|
||||||
def makeColorFunc(attr):
|
def makeColorFunc(attr):
|
||||||
def pickColor_():
|
def pickColor_():
|
||||||
|
self.mergeUndo = False
|
||||||
self.pickColor(
|
self.pickColor(
|
||||||
self._trackedWidgets[attr],
|
self._trackedWidgets[attr],
|
||||||
self._colorWidgets[attr]
|
self._colorWidgets[attr]
|
||||||
)
|
)
|
||||||
|
self.mergeUndo = True
|
||||||
return pickColor_
|
return pickColor_
|
||||||
self._colorFuncs = {
|
self._colorFuncs = {
|
||||||
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
|
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
|
||||||
|
@ -455,6 +620,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
self._relativeMaximums[attr] = \
|
self._relativeMaximums[attr] = \
|
||||||
self._trackedWidgets[attr].maximum()
|
self._trackedWidgets[attr].maximum()
|
||||||
self.updateRelativeWidgetMaximum(attr)
|
self.updateRelativeWidgetMaximum(attr)
|
||||||
|
setattr(
|
||||||
|
self, attr, getWidgetValue(self._trackedWidgets[attr])
|
||||||
|
)
|
||||||
|
|
||||||
|
self._preUpdate()
|
||||||
|
self._autoUpdate()
|
||||||
|
|
||||||
def pickColor(self, textWidget, button):
|
def pickColor(self, textWidget, button):
|
||||||
'''Use color picker to get color input from the user.'''
|
'''Use color picker to get color input from the user.'''
|
||||||
|
@ -516,12 +687,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
|
|
||||||
def relativeWidgetAxis(func):
|
def relativeWidgetAxis(func):
|
||||||
def relativeWidgetAxis(self, attr, *args, **kwargs):
|
def relativeWidgetAxis(self, attr, *args, **kwargs):
|
||||||
|
hasVerticalWords = (
|
||||||
|
lambda attr:
|
||||||
|
'height' in attr.lower() or
|
||||||
|
'ypos' in attr.lower() or
|
||||||
|
attr == 'y'
|
||||||
|
)
|
||||||
if 'axis' not in kwargs:
|
if 'axis' not in kwargs:
|
||||||
axis = self.width
|
axis = self.width
|
||||||
if 'height' in attr.lower() \
|
if hasVerticalWords(attr):
|
||||||
or 'ypos' in attr.lower() or attr == 'y':
|
|
||||||
axis = self.height
|
axis = self.height
|
||||||
kwargs['axis'] = axis
|
kwargs['axis'] = axis
|
||||||
|
if 'axis' in kwargs and type(kwargs['axis']) is tuple:
|
||||||
|
axis = kwargs['axis'][0]
|
||||||
|
if hasVerticalWords(attr):
|
||||||
|
axis = kwargs['axis'][1]
|
||||||
|
kwargs['axis'] = axis
|
||||||
return func(self, attr, *args, **kwargs)
|
return func(self, attr, *args, **kwargs)
|
||||||
return relativeWidgetAxis
|
return relativeWidgetAxis
|
||||||
|
|
||||||
|
@ -529,7 +710,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
def pixelValForAttr(self, attr, val=None, **kwargs):
|
def pixelValForAttr(self, attr, val=None, **kwargs):
|
||||||
if val is None:
|
if val is None:
|
||||||
val = self._relativeValues[attr]
|
val = self._relativeValues[attr]
|
||||||
return math.ceil(kwargs['axis'] * val)
|
if val > 50.0:
|
||||||
|
log.warning(
|
||||||
|
'%s #%s attempted to set %s to dangerously high number %s',
|
||||||
|
self.__class__.name, self.compPos, attr, val
|
||||||
|
)
|
||||||
|
val = 50.0
|
||||||
|
result = math.ceil(kwargs['axis'] * val)
|
||||||
|
log.verbose(
|
||||||
|
'Converting %s: f%s to px%s using axis %s',
|
||||||
|
attr, val, result, kwargs['axis']
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
@relativeWidgetAxis
|
@relativeWidgetAxis
|
||||||
def floatValForAttr(self, attr, val=None, **kwargs):
|
def floatValForAttr(self, attr, val=None, **kwargs):
|
||||||
|
@ -540,14 +732,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
def setRelativeWidget(self, attr, floatVal):
|
def setRelativeWidget(self, attr, floatVal):
|
||||||
'''Set a relative widget using a float'''
|
'''Set a relative widget using a float'''
|
||||||
pixelVal = self.pixelValForAttr(attr, floatVal)
|
pixelVal = self.pixelValForAttr(attr, floatVal)
|
||||||
self._trackedWidgets[attr].setValue(pixelVal)
|
with blockSignals(self._trackedWidgets[attr]):
|
||||||
|
self._trackedWidgets[attr].setValue(pixelVal)
|
||||||
|
self.update(auto=True)
|
||||||
|
|
||||||
|
def getOldAttr(self, attr):
|
||||||
|
'''
|
||||||
|
Returns previous state of this attr. Used to determine whether
|
||||||
|
a relative widget must be updated. Required because undoing/redoing
|
||||||
|
can make determining the 'previous' value tricky.
|
||||||
|
'''
|
||||||
|
if self.oldAttrs is not None:
|
||||||
|
return self.oldAttrs[attr]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return getattr(self, attr)
|
||||||
|
except AttributeError:
|
||||||
|
log.error('Using visible values instead of oldAttrs')
|
||||||
|
return self._trackedWidgets[attr].value()
|
||||||
|
|
||||||
def updateRelativeWidget(self, attr):
|
def updateRelativeWidget(self, attr):
|
||||||
try:
|
'''Called by _preUpdate() for each relativeWidget before each update'''
|
||||||
oldUserValue = getattr(self, attr)
|
oldUserValue = self.getOldAttr(attr)
|
||||||
except AttributeError:
|
|
||||||
oldUserValue = self._trackedWidgets[attr].value()
|
|
||||||
newUserValue = self._trackedWidgets[attr].value()
|
newUserValue = self._trackedWidgets[attr].value()
|
||||||
newRelativeVal = self.floatValForAttr(attr, newUserValue)
|
newRelativeVal = self.floatValForAttr(attr, newUserValue)
|
||||||
|
|
||||||
|
@ -557,13 +763,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
|
||||||
and oldRelativeVal != newRelativeVal:
|
and oldRelativeVal != newRelativeVal:
|
||||||
# Float changed without pixel value changing, which
|
# Float changed without pixel value changing, which
|
||||||
# means the pixel value needs to be updated
|
# means the pixel value needs to be updated
|
||||||
log.debug('Updating %s #%s\'s relative widget: %s' % (
|
log.debug(
|
||||||
self.name, self.compPos, attr))
|
'Updating %s #%s\'s relative widget: %s',
|
||||||
self._trackedWidgets[attr].blockSignals(True)
|
self.__class__.name, self.compPos, attr)
|
||||||
self.updateRelativeWidgetMaximum(attr)
|
with blockSignals(self._trackedWidgets[attr]):
|
||||||
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
|
self.updateRelativeWidgetMaximum(attr)
|
||||||
self._trackedWidgets[attr].setValue(pixelVal)
|
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
|
||||||
self._trackedWidgets[attr].blockSignals(False)
|
self._trackedWidgets[attr].setValue(pixelVal)
|
||||||
|
|
||||||
if attr not in self._relativeValues \
|
if attr not in self._relativeValues \
|
||||||
or oldUserValue != newUserValue:
|
or oldUserValue != newUserValue:
|
||||||
|
@ -629,3 +835,90 @@ class ComponentError(RuntimeError):
|
||||||
super().__init__(string)
|
super().__init__(string)
|
||||||
caller.lockError(string)
|
caller.lockError(string)
|
||||||
caller._error.emit(string, detail)
|
caller._error.emit(string, detail)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentUpdate(QtWidgets.QUndoCommand):
|
||||||
|
'''Command object for making a component action undoable'''
|
||||||
|
def __init__(self, parent, oldWidgetVals, modifiedVals):
|
||||||
|
super().__init__(
|
||||||
|
'change %s component #%s' % (
|
||||||
|
parent.name, parent.compPos
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.undone = False
|
||||||
|
self.res = (int(parent.width), int(parent.height))
|
||||||
|
self.parent = parent
|
||||||
|
self.oldWidgetVals = {
|
||||||
|
attr: copy(val)
|
||||||
|
if attr not in self.parent._relativeWidgets
|
||||||
|
else self.parent.floatValForAttr(attr, val, axis=self.res)
|
||||||
|
for attr, val in oldWidgetVals.items()
|
||||||
|
if attr in modifiedVals
|
||||||
|
}
|
||||||
|
self.modifiedVals = {
|
||||||
|
attr: val
|
||||||
|
if attr not in self.parent._relativeWidgets
|
||||||
|
else self.parent.floatValForAttr(attr, val, axis=self.res)
|
||||||
|
for attr, val in modifiedVals.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Because relative widgets change themselves every update based on
|
||||||
|
# their previous value, we must store ALL their values in case of undo
|
||||||
|
self.relativeWidgetValsAfterUndo = {
|
||||||
|
attr: copy(getattr(self.parent, attr))
|
||||||
|
for attr in self.parent._relativeWidgets
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if this update is mergeable
|
||||||
|
self.id_ = -1
|
||||||
|
if len(self.modifiedVals) == 1 and self.parent.mergeUndo:
|
||||||
|
attr, val = self.modifiedVals.popitem()
|
||||||
|
self.id_ = sum([ord(letter) for letter in attr[-14:]])
|
||||||
|
self.modifiedVals[attr] = val
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
'%s component settings changed at once. (%s)',
|
||||||
|
len(self.modifiedVals), repr(self.modifiedVals)
|
||||||
|
)
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
|
||||||
|
return self.id_
|
||||||
|
|
||||||
|
def mergeWith(self, other):
|
||||||
|
self.modifiedVals.update(other.modifiedVals)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setWidgetValues(self, attrDict):
|
||||||
|
'''
|
||||||
|
Mask the component's usual method to handle our
|
||||||
|
relative widgets in case the resolution has changed.
|
||||||
|
'''
|
||||||
|
newAttrDict = {
|
||||||
|
attr: val if attr not in self.parent._relativeWidgets
|
||||||
|
else self.parent.pixelValForAttr(attr, val)
|
||||||
|
for attr, val in attrDict.items()
|
||||||
|
}
|
||||||
|
self.parent.setWidgetValues(newAttrDict)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.undone:
|
||||||
|
log.info('Redoing component update')
|
||||||
|
self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
|
||||||
|
self.setWidgetValues(self.modifiedVals)
|
||||||
|
self.parent.update(auto=True)
|
||||||
|
self.parent.oldAttrs = None
|
||||||
|
if not self.undone:
|
||||||
|
self.relativeWidgetValsAfterRedo = {
|
||||||
|
attr: copy(getattr(self.parent, attr))
|
||||||
|
for attr in self.parent._relativeWidgets
|
||||||
|
}
|
||||||
|
self.parent._sendUpdateSignal()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
log.info('Undoing component update')
|
||||||
|
self.undone = True
|
||||||
|
self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
|
||||||
|
self.setWidgetValues(self.oldWidgetVals)
|
||||||
|
self.parent.update(auto=True)
|
||||||
|
self.parent.oldAttrs = None
|
||||||
|
|
|
@ -17,9 +17,6 @@ class Component(Component):
|
||||||
self.y = 0
|
self.y = 0
|
||||||
super().widget(*args)
|
super().widget(*args)
|
||||||
|
|
||||||
self.page.lineEdit_color1.setText('0,0,0')
|
|
||||||
self.page.lineEdit_color2.setText('133,133,133')
|
|
||||||
|
|
||||||
# disable color #2 until non-default 'fill' option gets changed
|
# disable color #2 until non-default 'fill' option gets changed
|
||||||
self.page.lineEdit_color2.setDisabled(True)
|
self.page.lineEdit_color2.setDisabled(True)
|
||||||
self.page.pushButton_color2.setDisabled(True)
|
self.page.pushButton_color2.setDisabled(True)
|
||||||
|
@ -85,8 +82,6 @@ class Component(Component):
|
||||||
self.page.pushButton_color2.setEnabled(False)
|
self.page.pushButton_color2.setEnabled(False)
|
||||||
self.page.fillWidget.setCurrentIndex(fillType)
|
self.page.fillWidget.setCurrentIndex(fillType)
|
||||||
|
|
||||||
super().update()
|
|
||||||
|
|
||||||
def previewRender(self):
|
def previewRender(self):
|
||||||
return self.drawFrame(self.width, self.height)
|
return self.drawFrame(self.width, self.height)
|
||||||
|
|
||||||
|
@ -107,7 +102,7 @@ class Component(Component):
|
||||||
# Return a solid image at x, y
|
# Return a solid image at x, y
|
||||||
if self.fillType == 0:
|
if self.fillType == 0:
|
||||||
frame = BlankFrame(width, height)
|
frame = BlankFrame(width, height)
|
||||||
image = Image.new("RGBA", shapeSize, (r, g, b, 255))
|
image = FloodFrame(self.sizeWidth, self.sizeHeight, (r, g, b, 255))
|
||||||
frame.paste(image, box=(self.x, self.y))
|
frame.paste(image, box=(self.x, self.y))
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,9 @@
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>0,0,0</string>
|
||||||
|
</property>
|
||||||
<property name="maxLength">
|
<property name="maxLength">
|
||||||
<number>12</number>
|
<number>12</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -146,6 +149,9 @@
|
||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>133,133,133</string>
|
||||||
|
</property>
|
||||||
<property name="maxLength">
|
<property name="maxLength">
|
||||||
<number>12</number>
|
<number>12</number>
|
||||||
</property>
|
</property>
|
||||||
|
@ -198,7 +204,7 @@
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>999999999</number>
|
<number>19200</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="value">
|
<property name="value">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
|
@ -233,7 +239,7 @@
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>999999999</number>
|
<number>10800</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -83,8 +83,9 @@ class Component(Component):
|
||||||
"Image Files (%s)" % " ".join(self.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.mergeUndo = False
|
||||||
self.page.lineEdit_image.setText(filename)
|
self.page.lineEdit_image.setText(filename)
|
||||||
self.update()
|
self.mergeUndo = True
|
||||||
|
|
||||||
def command(self, arg):
|
def command(self, arg):
|
||||||
if '=' in arg:
|
if '=' in arg:
|
||||||
|
@ -123,4 +124,3 @@ class Component(Component):
|
||||||
else:
|
else:
|
||||||
scaleBox.setVisible(True)
|
scaleBox.setVisible(True)
|
||||||
stretchScaleBox.setVisible(False)
|
stretchScaleBox.setVisible(False)
|
||||||
super().update()
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
|
from PyQt5.QtWidgets import QUndoCommand
|
||||||
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
|
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
|
||||||
import os
|
import os
|
||||||
import math
|
import math
|
||||||
|
@ -35,6 +36,7 @@ class Component(Component):
|
||||||
self.page.toolButton_left,
|
self.page.toolButton_left,
|
||||||
self.page.toolButton_right,
|
self.page.toolButton_right,
|
||||||
)
|
)
|
||||||
|
|
||||||
def shiftFunc(i):
|
def shiftFunc(i):
|
||||||
def shift():
|
def shift():
|
||||||
self.shiftGrid(i)
|
self.shiftGrid(i)
|
||||||
|
@ -52,26 +54,13 @@ class Component(Component):
|
||||||
"Image Files (%s)" % " ".join(self.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.mergeUndo = False
|
||||||
self.page.lineEdit_image.setText(filename)
|
self.page.lineEdit_image.setText(filename)
|
||||||
self.update()
|
self.mergeUndo = True
|
||||||
|
|
||||||
def shiftGrid(self, d):
|
def shiftGrid(self, d):
|
||||||
def newGrid(Xchange, Ychange):
|
action = ShiftGrid(self, d)
|
||||||
return {
|
self.parent.undoStack.push(action)
|
||||||
(x + Xchange, y + Ychange)
|
|
||||||
for x, y in self.startingGrid
|
|
||||||
}
|
|
||||||
|
|
||||||
if d == 0:
|
|
||||||
newGrid = newGrid(0, -1)
|
|
||||||
elif d == 1:
|
|
||||||
newGrid = newGrid(0, 1)
|
|
||||||
elif d == 2:
|
|
||||||
newGrid = newGrid(-1, 0)
|
|
||||||
elif d == 3:
|
|
||||||
newGrid = newGrid(1, 0)
|
|
||||||
self.startingGrid = newGrid
|
|
||||||
self.sendUpdateSignal()
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.updateGridSize()
|
self.updateGridSize()
|
||||||
|
@ -96,17 +85,14 @@ class Component(Component):
|
||||||
enabled = (len(self.startingGrid) > 0)
|
enabled = (len(self.startingGrid) > 0)
|
||||||
for widget in self.shiftButtons:
|
for widget in self.shiftButtons:
|
||||||
widget.setEnabled(enabled)
|
widget.setEnabled(enabled)
|
||||||
super().update()
|
|
||||||
|
|
||||||
def previewClickEvent(self, pos, size, button):
|
def previewClickEvent(self, pos, size, button):
|
||||||
pos = (
|
pos = (
|
||||||
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
|
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
|
||||||
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
|
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
|
||||||
)
|
)
|
||||||
if button == 1:
|
action = ClickGrid(self, pos, button)
|
||||||
self.startingGrid.add(pos)
|
self.parent.undoStack.push(action)
|
||||||
elif button == 2:
|
|
||||||
self.startingGrid.discard(pos)
|
|
||||||
|
|
||||||
def updateGridSize(self):
|
def updateGridSize(self):
|
||||||
w, h = self.core.resolutions[-1].split('x')
|
w, h = self.core.resolutions[-1].split('x')
|
||||||
|
@ -198,7 +184,7 @@ class Component(Component):
|
||||||
# Circle
|
# Circle
|
||||||
if shape == 'circle':
|
if shape == 'circle':
|
||||||
drawer.ellipse(outlineShape, fill=self.color)
|
drawer.ellipse(outlineShape, fill=self.color)
|
||||||
drawer.ellipse(smallerShape, fill=(0,0,0,0))
|
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
|
||||||
|
|
||||||
# Lilypad
|
# Lilypad
|
||||||
elif shape == 'lilypad':
|
elif shape == 'lilypad':
|
||||||
|
@ -208,9 +194,9 @@ class Component(Component):
|
||||||
elif shape == 'pac-man':
|
elif shape == 'pac-man':
|
||||||
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
|
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
|
||||||
|
|
||||||
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
|
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
|
||||||
tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
|
tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
|
||||||
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
|
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
|
||||||
|
|
||||||
# Path
|
# Path
|
||||||
if shape == 'path':
|
if shape == 'path':
|
||||||
|
@ -221,7 +207,7 @@ class Component(Component):
|
||||||
'up', 'down', 'left', 'right',
|
'up', 'down', 'left', 'right',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for cell in nearbyCoords(x, y):
|
for cell in self.nearbyCoords(x, y):
|
||||||
if cell not in grid:
|
if cell not in grid:
|
||||||
continue
|
continue
|
||||||
if cell[0] == x:
|
if cell[0] == x:
|
||||||
|
@ -246,19 +232,19 @@ class Component(Component):
|
||||||
sect = (
|
sect = (
|
||||||
(drawPtX, drawPtY + hY),
|
(drawPtX, drawPtY + hY),
|
||||||
(drawPtX + self.pxWidth,
|
(drawPtX + self.pxWidth,
|
||||||
drawPtY + self.pxHeight)
|
drawPtY + self.pxHeight)
|
||||||
)
|
)
|
||||||
elif direction == 'left':
|
elif direction == 'left':
|
||||||
sect = (
|
sect = (
|
||||||
(drawPtX, drawPtY),
|
(drawPtX, drawPtY),
|
||||||
(drawPtX + hX,
|
(drawPtX + hX,
|
||||||
drawPtY + self.pxHeight)
|
drawPtY + self.pxHeight)
|
||||||
)
|
)
|
||||||
elif direction == 'right':
|
elif direction == 'right':
|
||||||
sect = (
|
sect = (
|
||||||
(drawPtX + hX, drawPtY),
|
(drawPtX + hX, drawPtY),
|
||||||
(drawPtX + self.pxWidth,
|
(drawPtX + self.pxWidth,
|
||||||
drawPtY + self.pxHeight)
|
drawPtY + self.pxHeight)
|
||||||
)
|
)
|
||||||
drawer.rectangle(sect, fill=self.color)
|
drawer.rectangle(sect, fill=self.color)
|
||||||
|
|
||||||
|
@ -288,20 +274,25 @@ class Component(Component):
|
||||||
|
|
||||||
# Peace
|
# Peace
|
||||||
elif shape == 'peace':
|
elif shape == 'peace':
|
||||||
line = (
|
line = ((
|
||||||
(drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
|
drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
|
||||||
(drawPtX + hX + int(tenthX / 2),
|
(drawPtX + hX + int(tenthX / 2),
|
||||||
drawPtY + self.pxHeight - int(tenthY / 2))
|
drawPtY + self.pxHeight - int(tenthY / 2))
|
||||||
)
|
)
|
||||||
drawer.ellipse(outlineShape, fill=self.color)
|
drawer.ellipse(outlineShape, fill=self.color)
|
||||||
drawer.ellipse(smallerShape, fill=(0,0,0,0))
|
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
|
||||||
drawer.rectangle(line, fill=self.color)
|
drawer.rectangle(line, fill=self.color)
|
||||||
slantLine = lambda difference: (
|
|
||||||
((drawPtX + difference),
|
def slantLine(difference):
|
||||||
(drawPtY + self.pxHeight - qY)),
|
return (
|
||||||
((drawPtX + hX),
|
(drawPtX + difference),
|
||||||
(drawPtY + hY)),
|
(drawPtY + self.pxHeight - qY)
|
||||||
)
|
),
|
||||||
|
(
|
||||||
|
(drawPtX + hX),
|
||||||
|
(drawPtY + hY)
|
||||||
|
)
|
||||||
|
|
||||||
drawer.line(
|
drawer.line(
|
||||||
slantLine(qX),
|
slantLine(qX),
|
||||||
fill=self.color,
|
fill=self.color,
|
||||||
|
@ -338,13 +329,13 @@ class Component(Component):
|
||||||
for x in range(self.pxWidth, self.width, self.pxWidth):
|
for x in range(self.pxWidth, self.width, self.pxWidth):
|
||||||
drawer.rectangle(
|
drawer.rectangle(
|
||||||
((x, 0),
|
((x, 0),
|
||||||
(x + w, self.height)),
|
(x + w, self.height)),
|
||||||
fill=self.color,
|
fill=self.color,
|
||||||
)
|
)
|
||||||
for y in range(self.pxHeight, self.height, self.pxHeight):
|
for y in range(self.pxHeight, self.height, self.pxHeight):
|
||||||
drawer.rectangle(
|
drawer.rectangle(
|
||||||
((0, y),
|
((0, y),
|
||||||
(self.width, y + h)),
|
(self.width, y + h)),
|
||||||
fill=self.color,
|
fill=self.color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -356,7 +347,7 @@ class Component(Component):
|
||||||
|
|
||||||
def neighbours(x, y):
|
def neighbours(x, y):
|
||||||
return {
|
return {
|
||||||
cell for cell in nearbyCoords(x, y)
|
cell for cell in self.nearbyCoords(x, y)
|
||||||
if cell in lastGrid
|
if cell in lastGrid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,7 +358,7 @@ class Component(Component):
|
||||||
newGrid.add((x, y))
|
newGrid.add((x, y))
|
||||||
potentialNewCells = {
|
potentialNewCells = {
|
||||||
coordTup for origin in lastGrid
|
coordTup for origin in lastGrid
|
||||||
for coordTup in list(nearbyCoords(*origin))
|
for coordTup in list(self.nearbyCoords(*origin))
|
||||||
}
|
}
|
||||||
for x, y in potentialNewCells:
|
for x, y in potentialNewCells:
|
||||||
if (x, y) in newGrid:
|
if (x, y) in newGrid:
|
||||||
|
@ -390,13 +381,95 @@ class Component(Component):
|
||||||
widget.setEnabled(True)
|
widget.setEnabled(True)
|
||||||
super().loadPreset(pr, *args)
|
super().loadPreset(pr, *args)
|
||||||
|
|
||||||
|
def nearbyCoords(self, x, y):
|
||||||
|
yield x + 1, y + 1
|
||||||
|
yield x + 1, y - 1
|
||||||
|
yield x - 1, y + 1
|
||||||
|
yield x - 1, y - 1
|
||||||
|
yield x, y + 1
|
||||||
|
yield x, y - 1
|
||||||
|
yield x + 1, y
|
||||||
|
yield x - 1, y
|
||||||
|
|
||||||
def nearbyCoords(x, y):
|
|
||||||
yield x + 1, y + 1
|
class ClickGrid(QUndoCommand):
|
||||||
yield x + 1, y - 1
|
def __init__(self, comp, pos, id_):
|
||||||
yield x - 1, y + 1
|
super().__init__(
|
||||||
yield x - 1, y - 1
|
"click %s component #%s" % (comp.name, comp.compPos))
|
||||||
yield x, y + 1
|
self.comp = comp
|
||||||
yield x, y - 1
|
self.pos = [pos]
|
||||||
yield x + 1, y
|
self.id_ = id_
|
||||||
yield x - 1, y
|
|
||||||
|
def id(self):
|
||||||
|
return self.id_
|
||||||
|
|
||||||
|
def mergeWith(self, other):
|
||||||
|
self.pos.extend(other.pos)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
for pos in self.pos[:]:
|
||||||
|
self.comp.startingGrid.add(pos)
|
||||||
|
self.comp.update(auto=True)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
for pos in self.pos[:]:
|
||||||
|
self.comp.startingGrid.discard(pos)
|
||||||
|
self.comp.update(auto=True)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.id_ == 1: # Left-click
|
||||||
|
self.add()
|
||||||
|
elif self.id_ == 2: # Right-click
|
||||||
|
self.remove()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.id_ == 1: # Left-click
|
||||||
|
self.remove()
|
||||||
|
elif self.id_ == 2: # Right-click
|
||||||
|
self.add()
|
||||||
|
|
||||||
|
class ShiftGrid(QUndoCommand):
|
||||||
|
def __init__(self, comp, direction):
|
||||||
|
super().__init__(
|
||||||
|
"change %s component #%s" % (comp.name, comp.compPos))
|
||||||
|
self.comp = comp
|
||||||
|
self.direction = direction
|
||||||
|
self.distance = 1
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
return self.direction
|
||||||
|
|
||||||
|
def mergeWith(self, other):
|
||||||
|
self.distance += other.distance
|
||||||
|
return True
|
||||||
|
|
||||||
|
def newGrid(self, Xchange, Ychange):
|
||||||
|
return {
|
||||||
|
(x + Xchange, y + Ychange)
|
||||||
|
for x, y in self.comp.startingGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.direction == 0:
|
||||||
|
newGrid = self.newGrid(0, -self.distance)
|
||||||
|
elif self.direction == 1:
|
||||||
|
newGrid = self.newGrid(0, self.distance)
|
||||||
|
elif self.direction == 2:
|
||||||
|
newGrid = self.newGrid(-self.distance, 0)
|
||||||
|
elif self.direction == 3:
|
||||||
|
newGrid = self.newGrid(self.distance, 0)
|
||||||
|
self.comp.startingGrid = newGrid
|
||||||
|
self.comp._sendUpdateSignal()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.direction == 0:
|
||||||
|
newGrid = self.newGrid(0, self.distance)
|
||||||
|
elif self.direction == 1:
|
||||||
|
newGrid = self.newGrid(0, -self.distance)
|
||||||
|
elif self.direction == 2:
|
||||||
|
newGrid = self.newGrid(self.distance, 0)
|
||||||
|
elif self.direction == 3:
|
||||||
|
newGrid = self.newGrid(-self.distance, 0)
|
||||||
|
self.comp.startingGrid = newGrid
|
||||||
|
self.comp._sendUpdateSignal()
|
||||||
|
|
|
@ -52,8 +52,9 @@ class Component(Component):
|
||||||
"Audio Files (%s)" % " ".join(self.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.mergeUndo = False
|
||||||
self.page.lineEdit_sound.setText(filename)
|
self.page.lineEdit_sound.setText(filename)
|
||||||
self.update()
|
self.mergeUndo = True
|
||||||
|
|
||||||
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')
|
||||||
|
|
|
@ -76,8 +76,6 @@ class Component(Component):
|
||||||
else:
|
else:
|
||||||
self.page.checkBox_mono.setEnabled(True)
|
self.page.checkBox_mono.setEnabled(True)
|
||||||
|
|
||||||
super().update()
|
|
||||||
|
|
||||||
def previewRender(self):
|
def previewRender(self):
|
||||||
changedSize = self.updateChunksize()
|
changedSize = self.updateChunksize()
|
||||||
if not changedSize \
|
if not changedSize \
|
||||||
|
@ -100,7 +98,8 @@ class Component(Component):
|
||||||
|
|
||||||
def preFrameRender(self, **kwargs):
|
def preFrameRender(self, **kwargs):
|
||||||
super().preFrameRender(**kwargs)
|
super().preFrameRender(**kwargs)
|
||||||
self.previewPipe.wait()
|
if self.previewPipe is not None:
|
||||||
|
self.previewPipe.wait()
|
||||||
self.updateChunksize()
|
self.updateChunksize()
|
||||||
w, h = scale(self.scale, self.width, self.height, str)
|
w, h = scale(self.scale, self.width, self.height, str)
|
||||||
self.video = FfmpegVideo(
|
self.video = FfmpegVideo(
|
||||||
|
@ -138,7 +137,7 @@ class Component(Component):
|
||||||
'-r', self.settings.value("outputFrameRate"),
|
'-r', self.settings.value("outputFrameRate"),
|
||||||
'-ss', "{0:.3f}".format(startPt),
|
'-ss', "{0:.3f}".format(startPt),
|
||||||
'-i',
|
'-i',
|
||||||
os.path.join(self.core.wd, 'background.png')
|
self.core.junkStream
|
||||||
if genericPreview else inputFile,
|
if genericPreview else inputFile,
|
||||||
'-f', 'image2pipe',
|
'-f', 'image2pipe',
|
||||||
'-pix_fmt', 'rgba',
|
'-pix_fmt', 'rgba',
|
||||||
|
@ -150,15 +149,22 @@ class Component(Component):
|
||||||
'-codec:v', 'rawvideo', '-',
|
'-codec:v', 'rawvideo', '-',
|
||||||
'-frames:v', '1',
|
'-frames:v', '1',
|
||||||
])
|
])
|
||||||
logFilename = os.path.join(
|
|
||||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
if self.core.logEnabled:
|
||||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
logFilename = os.path.join(
|
||||||
with open(logFilename, 'w') as logf:
|
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||||
logf.write(" ".join(command) + '\n\n')
|
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
||||||
with open(logFilename, 'a') as logf:
|
with open(logFilename, 'w') as logf:
|
||||||
|
logf.write(" ".join(command) + '\n\n')
|
||||||
|
with open(logFilename, 'a') as logf:
|
||||||
|
self.previewPipe = openPipe(
|
||||||
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
|
stderr=logf, bufsize=10**8
|
||||||
|
)
|
||||||
|
else:
|
||||||
self.previewPipe = openPipe(
|
self.previewPipe = openPipe(
|
||||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
stderr=logf, bufsize=10**8
|
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||||
)
|
)
|
||||||
byteFrame = self.previewPipe.stdout.read(self.chunkSize)
|
byteFrame = self.previewPipe.stdout.read(self.chunkSize)
|
||||||
closePipe(self.previewPipe)
|
closePipe(self.previewPipe)
|
||||||
|
|
|
@ -2,10 +2,13 @@ from PIL import ImageEnhance, ImageFilter, ImageChops
|
||||||
from PyQt5.QtGui import QColor, QFont
|
from PyQt5.QtGui import QColor, QFont
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from component import Component
|
from component import Component
|
||||||
from toolkit.frame import FramePainter, PaintColor
|
from toolkit.frame import FramePainter, PaintColor
|
||||||
|
|
||||||
|
log = logging.getLogger('AVP.Components.Text')
|
||||||
|
|
||||||
|
|
||||||
class Component(Component):
|
class Component(Component):
|
||||||
name = 'Title Text'
|
name = 'Title Text'
|
||||||
|
@ -13,8 +16,6 @@ class Component(Component):
|
||||||
|
|
||||||
def widget(self, *args):
|
def widget(self, *args):
|
||||||
super().widget(*args)
|
super().widget(*args)
|
||||||
self.textColor = (255, 255, 255)
|
|
||||||
self.strokeColor = (0, 0, 0)
|
|
||||||
self.title = 'Text'
|
self.title = 'Text'
|
||||||
self.alignment = 1
|
self.alignment = 1
|
||||||
self.titleFont = QFont()
|
self.titleFont = QFont()
|
||||||
|
@ -25,8 +26,6 @@ class Component(Component):
|
||||||
self.page.comboBox_textAlign.addItem("Right")
|
self.page.comboBox_textAlign.addItem("Right")
|
||||||
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
|
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
|
||||||
|
|
||||||
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
|
|
||||||
self.page.lineEdit_strokeColor.setText('%s,%s,%s' % self.strokeColor)
|
|
||||||
self.page.spinBox_fontSize.setValue(int(self.fontSize))
|
self.page.spinBox_fontSize.setValue(int(self.fontSize))
|
||||||
self.page.lineEdit_title.setText(self.title)
|
self.page.lineEdit_title.setText(self.title)
|
||||||
|
|
||||||
|
@ -72,7 +71,6 @@ class Component(Component):
|
||||||
self.page.spinBox_shadY.setHidden(True)
|
self.page.spinBox_shadY.setHidden(True)
|
||||||
self.page.label_shadBlur.setHidden(True)
|
self.page.label_shadBlur.setHidden(True)
|
||||||
self.page.spinBox_shadBlur.setHidden(True)
|
self.page.spinBox_shadBlur.setHidden(True)
|
||||||
super().update()
|
|
||||||
|
|
||||||
def centerXY(self):
|
def centerXY(self):
|
||||||
self.setRelativeWidget('xPosition', 0.5)
|
self.setRelativeWidget('xPosition', 0.5)
|
||||||
|
@ -81,16 +79,15 @@ class Component(Component):
|
||||||
def getXY(self):
|
def getXY(self):
|
||||||
'''Returns true x, y after considering alignment settings'''
|
'''Returns true x, y after considering alignment settings'''
|
||||||
fm = QtGui.QFontMetrics(self.titleFont)
|
fm = QtGui.QFontMetrics(self.titleFont)
|
||||||
if self.alignment == 0: # Left
|
x = self.pixelValForAttr('xPosition')
|
||||||
x = int(self.xPosition)
|
|
||||||
|
|
||||||
if self.alignment == 1: # Middle
|
if self.alignment == 1: # Middle
|
||||||
offset = int(fm.width(self.title)/2)
|
offset = int(fm.width(self.title)/2)
|
||||||
x = self.xPosition - offset
|
x -= offset
|
||||||
|
|
||||||
if self.alignment == 2: # Right
|
if self.alignment == 2: # Right
|
||||||
offset = fm.width(self.title)
|
offset = fm.width(self.title)
|
||||||
x = self.xPosition - offset
|
x -= offset
|
||||||
|
|
||||||
return x, self.yPosition
|
return x, self.yPosition
|
||||||
|
|
||||||
def loadPreset(self, pr, *args):
|
def loadPreset(self, pr, *args):
|
||||||
|
@ -142,6 +139,7 @@ class Component(Component):
|
||||||
|
|
||||||
image = FramePainter(width, height)
|
image = FramePainter(width, height)
|
||||||
x, y = self.getXY()
|
x, y = self.getXY()
|
||||||
|
log.debug('Text position translates to %s, %s', x, y)
|
||||||
if self.stroke > 0:
|
if self.stroke > 0:
|
||||||
outliner = QtGui.QPainterPathStroker()
|
outliner = QtGui.QPainterPathStroker()
|
||||||
outliner.setWidth(self.stroke)
|
outliner.setWidth(self.stroke)
|
||||||
|
|
|
@ -427,6 +427,9 @@
|
||||||
<property name="focusPolicy">
|
<property name="focusPolicy">
|
||||||
<enum>Qt::NoFocus</enum>
|
<enum>Qt::NoFocus</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>255,255,255</string>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -485,6 +488,9 @@
|
||||||
<property name="focusPolicy">
|
<property name="focusPolicy">
|
||||||
<enum>Qt::NoFocus</enum>
|
<enum>Qt::NoFocus</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>0,0,0</string>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|
|
@ -52,7 +52,6 @@ class Component(Component):
|
||||||
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()
|
|
||||||
|
|
||||||
def previewRender(self):
|
def previewRender(self):
|
||||||
self.updateChunksize()
|
self.updateChunksize()
|
||||||
|
@ -118,8 +117,9 @@ class Component(Component):
|
||||||
)
|
)
|
||||||
if filename:
|
if filename:
|
||||||
self.settings.setValue("componentDir", os.path.dirname(filename))
|
self.settings.setValue("componentDir", os.path.dirname(filename))
|
||||||
|
self.mergeUndo = False
|
||||||
self.page.lineEdit_video.setText(filename)
|
self.page.lineEdit_video.setText(filename)
|
||||||
self.update()
|
self.mergeUndo = True
|
||||||
|
|
||||||
def getPreviewFrame(self, width, height):
|
def getPreviewFrame(self, width, height):
|
||||||
if not self.videoPath or not os.path.exists(self.videoPath):
|
if not self.videoPath or not os.path.exists(self.videoPath):
|
||||||
|
@ -139,16 +139,23 @@ class Component(Component):
|
||||||
'-frames:v', '1',
|
'-frames:v', '1',
|
||||||
])
|
])
|
||||||
|
|
||||||
logFilename = os.path.join(
|
if self.core.logEnabled:
|
||||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
logFilename = os.path.join(
|
||||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||||
with open(logFilename, 'w') as logf:
|
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
||||||
logf.write(" ".join(command) + '\n\n')
|
with open(logFilename, 'w') as logf:
|
||||||
with open(logFilename, 'a') as logf:
|
logf.write(" ".join(command) + '\n\n')
|
||||||
|
with open(logFilename, 'a') as logf:
|
||||||
|
pipe = openPipe(
|
||||||
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
|
stderr=logf, bufsize=10**8
|
||||||
|
)
|
||||||
|
else:
|
||||||
pipe = openPipe(
|
pipe = openPipe(
|
||||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
stderr=logf, bufsize=10**8
|
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||||
)
|
)
|
||||||
|
|
||||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||||
closePipe(pipe)
|
closePipe(pipe)
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ class Component(Component):
|
||||||
'-r', self.settings.value("outputFrameRate"),
|
'-r', self.settings.value("outputFrameRate"),
|
||||||
'-ss', "{0:.3f}".format(startPt),
|
'-ss', "{0:.3f}".format(startPt),
|
||||||
'-i',
|
'-i',
|
||||||
os.path.join(self.core.wd, 'background.png')
|
self.core.junkStream
|
||||||
if genericPreview else inputFile,
|
if genericPreview else inputFile,
|
||||||
'-f', 'image2pipe',
|
'-f', 'image2pipe',
|
||||||
'-pix_fmt', 'rgba',
|
'-pix_fmt', 'rgba',
|
||||||
|
@ -110,15 +110,21 @@ class Component(Component):
|
||||||
'-codec:v', 'rawvideo', '-',
|
'-codec:v', 'rawvideo', '-',
|
||||||
'-frames:v', '1',
|
'-frames:v', '1',
|
||||||
])
|
])
|
||||||
logFilename = os.path.join(
|
if self.core.logEnabled:
|
||||||
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
logFilename = os.path.join(
|
||||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
self.core.logDir, 'preview_%s.log' % str(self.compPos))
|
||||||
with open(logFilename, 'w') as logf:
|
log.debug('Creating ffmpeg log at %s', logFilename)
|
||||||
logf.write(" ".join(command) + '\n\n')
|
with open(logFilename, 'w') as logf:
|
||||||
with open(logFilename, 'a') as logf:
|
logf.write(" ".join(command) + '\n\n')
|
||||||
|
with open(logFilename, 'a') as logf:
|
||||||
|
pipe = openPipe(
|
||||||
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
|
stderr=logf, bufsize=10**8
|
||||||
|
)
|
||||||
|
else:
|
||||||
pipe = openPipe(
|
pipe = openPipe(
|
||||||
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
stderr=logf, bufsize=10**8
|
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||||
)
|
)
|
||||||
byteFrame = pipe.stdout.read(self.chunkSize)
|
byteFrame = pipe.stdout.read(self.chunkSize)
|
||||||
closePipe(pipe)
|
closePipe(pipe)
|
||||||
|
|
164
src/core.py
164
src/core.py
|
@ -14,7 +14,7 @@ import toolkit
|
||||||
|
|
||||||
log = logging.getLogger('AVP.Core')
|
log = logging.getLogger('AVP.Core')
|
||||||
STDOUT_LOGLVL = logging.WARNING
|
STDOUT_LOGLVL = logging.WARNING
|
||||||
FILE_LOGLVL = logging.DEBUG
|
FILE_LOGLVL = None
|
||||||
|
|
||||||
|
|
||||||
class Core:
|
class Core:
|
||||||
|
@ -32,6 +32,11 @@ class Core:
|
||||||
self.savedPresets = {} # copies of presets to detect modification
|
self.savedPresets = {} # copies of presets to detect modification
|
||||||
self.openingProject = False
|
self.openingProject = False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "\n=~=~=~=\n".join(
|
||||||
|
[repr(comp) for comp in self.selectedComponents]
|
||||||
|
)
|
||||||
|
|
||||||
def importComponents(self):
|
def importComponents(self):
|
||||||
def findComponents():
|
def findComponents():
|
||||||
for f in os.listdir(Core.componentsPath):
|
for f in os.listdir(Core.componentsPath):
|
||||||
|
@ -64,34 +69,41 @@ class Core:
|
||||||
for i, component in enumerate(self.selectedComponents):
|
for i, component in enumerate(self.selectedComponents):
|
||||||
component.compPos = i
|
component.compPos = i
|
||||||
|
|
||||||
def insertComponent(self, compPos, moduleIndex, loader):
|
def insertComponent(self, compPos, component, loader):
|
||||||
'''
|
'''
|
||||||
Creates a new component using these args:
|
Creates a new component using these args:
|
||||||
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
|
(compPos, component obj or moduleIndex, 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 -1
|
||||||
log.debug('Inserting Component from module #%s' % moduleIndex)
|
if type(component) is int:
|
||||||
component = self.modules[moduleIndex].Component(
|
# create component using module index in self.modules
|
||||||
moduleIndex, compPos, self
|
moduleIndex = int(component)
|
||||||
|
log.debug(
|
||||||
|
'Creating new component from module #%s', str(moduleIndex))
|
||||||
|
component = self.modules[moduleIndex].Component(
|
||||||
|
moduleIndex, compPos, self
|
||||||
|
)
|
||||||
|
component.widget(loader)
|
||||||
|
else:
|
||||||
|
moduleIndex = -1
|
||||||
|
log.debug(
|
||||||
|
'Inserting previously-created %s component', component.name)
|
||||||
|
|
||||||
|
component._error.connect(
|
||||||
|
loader.videoThreadError
|
||||||
)
|
)
|
||||||
self.selectedComponents.insert(
|
self.selectedComponents.insert(
|
||||||
compPos,
|
compPos,
|
||||||
component
|
component
|
||||||
)
|
)
|
||||||
self.componentListChanged()
|
|
||||||
self.selectedComponents[compPos]._error.connect(
|
|
||||||
loader.videoThreadError
|
|
||||||
)
|
|
||||||
|
|
||||||
# init component's widget for loading/saving presets
|
|
||||||
self.selectedComponents[compPos].widget(loader)
|
|
||||||
self.updateComponent(compPos)
|
|
||||||
|
|
||||||
if hasattr(loader, 'insertComponent'):
|
if hasattr(loader, 'insertComponent'):
|
||||||
loader.insertComponent(compPos)
|
loader.insertComponent(compPos)
|
||||||
|
|
||||||
|
self.componentListChanged()
|
||||||
|
self.updateComponent(compPos)
|
||||||
return compPos
|
return compPos
|
||||||
|
|
||||||
def moveComponent(self, startI, endI):
|
def moveComponent(self, startI, endI):
|
||||||
|
@ -110,8 +122,10 @@ class Core:
|
||||||
self.componentListChanged()
|
self.componentListChanged()
|
||||||
|
|
||||||
def updateComponent(self, i):
|
def updateComponent(self, i):
|
||||||
log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
|
log.debug(
|
||||||
self.selectedComponents[i].update()
|
'Auto-updating %s #%s',
|
||||||
|
self.selectedComponents[i], str(i))
|
||||||
|
self.selectedComponents[i].update(auto=True)
|
||||||
|
|
||||||
def moduleIndexFor(self, compName):
|
def moduleIndexFor(self, compName):
|
||||||
try:
|
try:
|
||||||
|
@ -130,18 +144,11 @@ class Core:
|
||||||
saveValueStore = self.getPreset(filepath)
|
saveValueStore = self.getPreset(filepath)
|
||||||
if not saveValueStore:
|
if not saveValueStore:
|
||||||
return False
|
return False
|
||||||
try:
|
comp = self.selectedComponents[compIndex]
|
||||||
comp = self.selectedComponents[compIndex]
|
comp.loadPreset(
|
||||||
comp.loadPreset(
|
saveValueStore,
|
||||||
saveValueStore,
|
presetName
|
||||||
presetName
|
)
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
log.warning(
|
|
||||||
'%s #%s\'s preset is missing value: %s' % (
|
|
||||||
comp.name, str(compIndex), str(e)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.savedPresets[presetName] = dict(saveValueStore)
|
self.savedPresets[presetName] = dict(saveValueStore)
|
||||||
return True
|
return True
|
||||||
|
@ -156,6 +163,10 @@ class Core:
|
||||||
break
|
break
|
||||||
return saveValueStore
|
return saveValueStore
|
||||||
|
|
||||||
|
def getPresetDir(self, comp):
|
||||||
|
'''Get the preset subdir for a particular version of a component'''
|
||||||
|
return os.path.join(Core.presetDir, comp.name, str(comp.version))
|
||||||
|
|
||||||
def openProject(self, loader, filepath):
|
def openProject(self, loader, filepath):
|
||||||
''' loader is the object calling this method which must have
|
''' loader is the object calling this method which must have
|
||||||
its own showMessage(**kwargs) method for displaying errors.
|
its own showMessage(**kwargs) method for displaying errors.
|
||||||
|
@ -171,13 +182,11 @@ class Core:
|
||||||
if hasattr(loader, 'window'):
|
if hasattr(loader, 'window'):
|
||||||
for widget, value in data['WindowFields']:
|
for widget, value in data['WindowFields']:
|
||||||
widget = eval('loader.window.%s' % widget)
|
widget = eval('loader.window.%s' % widget)
|
||||||
widget.blockSignals(True)
|
with toolkit.blockSignals(widget):
|
||||||
toolkit.setWidgetValue(widget, value)
|
toolkit.setWidgetValue(widget, value)
|
||||||
widget.blockSignals(False)
|
|
||||||
|
|
||||||
for key, value in data['Settings']:
|
for key, value in data['Settings']:
|
||||||
Core.settings.setValue(key, value)
|
Core.settings.setValue(key, value)
|
||||||
|
|
||||||
for tup in data['Components']:
|
for tup in data['Components']:
|
||||||
name, vers, preset = tup
|
name, vers, preset = tup
|
||||||
clearThis = False
|
clearThis = False
|
||||||
|
@ -202,7 +211,7 @@ class Core:
|
||||||
self.moduleIndexFor(name),
|
self.moduleIndexFor(name),
|
||||||
loader
|
loader
|
||||||
)
|
)
|
||||||
if i is None:
|
if i == -1:
|
||||||
loader.showMessage(msg="Too many components!")
|
loader.showMessage(msg="Too many components!")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -255,7 +264,7 @@ class Core:
|
||||||
Returns dictionary with section names as the keys, each one
|
Returns dictionary with section names as the keys, each one
|
||||||
contains a list of tuples: (compName, version, compPresetDict)
|
contains a list of tuples: (compName, version, compPresetDict)
|
||||||
'''
|
'''
|
||||||
log.debug('Parsing av file: %s' % filepath)
|
log.debug('Parsing av file: %s', filepath)
|
||||||
validSections = (
|
validSections = (
|
||||||
'Components',
|
'Components',
|
||||||
'Settings',
|
'Settings',
|
||||||
|
@ -374,7 +383,7 @@ class Core:
|
||||||
|
|
||||||
def createProjectFile(self, filepath, window=None):
|
def createProjectFile(self, filepath, window=None):
|
||||||
'''Create a project file (.avp) using the current program state'''
|
'''Create a project file (.avp) using the current program state'''
|
||||||
log.info('Creating %s' % filepath)
|
log.info('Creating %s', filepath)
|
||||||
settingsKeys = [
|
settingsKeys = [
|
||||||
'componentDir',
|
'componentDir',
|
||||||
'inputDir',
|
'inputDir',
|
||||||
|
@ -448,26 +457,30 @@ class Core:
|
||||||
dataDir = QtCore.QStandardPaths.writableLocation(
|
dataDir = QtCore.QStandardPaths.writableLocation(
|
||||||
QtCore.QStandardPaths.AppConfigLocation
|
QtCore.QStandardPaths.AppConfigLocation
|
||||||
)
|
)
|
||||||
|
# Windows: C:/Users/<USER>/AppData/Local/audio-visualizer
|
||||||
|
# macOS: ~/Library/Preferences/audio-visualizer
|
||||||
|
# Linux: ~/.config/audio-visualizer
|
||||||
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
|
||||||
encoderOptions = json.load(json_file)
|
encoderOptions = json.load(json_file)
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
|
'canceled': False,
|
||||||
|
'FFMPEG_BIN': findFfmpeg(),
|
||||||
'dataDir': dataDir,
|
'dataDir': dataDir,
|
||||||
'settings': QtCore.QSettings(
|
'settings': QtCore.QSettings(
|
||||||
os.path.join(dataDir, 'settings.ini'),
|
os.path.join(dataDir, 'settings.ini'),
|
||||||
QtCore.QSettings.IniFormat),
|
QtCore.QSettings.IniFormat),
|
||||||
'logDir': os.path.join(dataDir, 'log'),
|
|
||||||
'presetDir': os.path.join(dataDir, 'presets'),
|
'presetDir': os.path.join(dataDir, 'presets'),
|
||||||
'componentsPath': os.path.join(wd, 'components'),
|
'componentsPath': os.path.join(wd, 'components'),
|
||||||
|
'junkStream': os.path.join(wd, 'gui', 'background.png'),
|
||||||
'encoderOptions': encoderOptions,
|
'encoderOptions': encoderOptions,
|
||||||
'resolutions': [
|
'resolutions': [
|
||||||
'1920x1080',
|
'1920x1080',
|
||||||
'1280x720',
|
'1280x720',
|
||||||
'854x480',
|
'854x480',
|
||||||
],
|
],
|
||||||
'FFMPEG_BIN': findFfmpeg(),
|
'logDir': os.path.join(dataDir, 'log'),
|
||||||
'windowHasFocus': False,
|
'logEnabled': False,
|
||||||
'canceled': False,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settings['videoFormats'] = toolkit.appendUppercase([
|
settings['videoFormats'] = toolkit.appendUppercase([
|
||||||
|
@ -528,6 +541,7 @@ class Core:
|
||||||
"projectDir": os.path.join(cls.dataDir, 'projects'),
|
"projectDir": os.path.join(cls.dataDir, 'projects'),
|
||||||
"pref_insertCompAtTop": True,
|
"pref_insertCompAtTop": True,
|
||||||
"pref_genericPreview": True,
|
"pref_genericPreview": True,
|
||||||
|
"pref_undoLimit": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
for parm, value in cls.defaultSettings.items():
|
for parm, value in cls.defaultSettings.items():
|
||||||
|
@ -540,47 +554,53 @@ class Core:
|
||||||
if not key.startswith('pref_'):
|
if not key.startswith('pref_'):
|
||||||
continue
|
continue
|
||||||
val = cls.settings.value(key)
|
val = cls.settings.value(key)
|
||||||
if val in ('true', 'false'):
|
try:
|
||||||
cls.settings.setValue(key, True if val == 'true' else False)
|
val = int(val)
|
||||||
|
except ValueError:
|
||||||
|
if val == 'true':
|
||||||
|
val = True
|
||||||
|
elif val == 'false':
|
||||||
|
val = False
|
||||||
|
cls.settings.setValue(key, val)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def makeLogger():
|
def makeLogger():
|
||||||
logFilename = os.path.join(Core.logDir, 'avp_debug.log')
|
# send critical log messages to stdout
|
||||||
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
|
|
||||||
# delete old logs
|
|
||||||
for log in (logFilename, libLogFilename):
|
|
||||||
if os.path.exists(log):
|
|
||||||
os.remove(log)
|
|
||||||
|
|
||||||
# create file handlers to capture every log message somewhere
|
|
||||||
logFile = logging.FileHandler(logFilename)
|
|
||||||
logFile.setLevel(FILE_LOGLVL)
|
|
||||||
libLogFile = logging.FileHandler(libLogFilename)
|
|
||||||
libLogFile.setLevel(FILE_LOGLVL)
|
|
||||||
|
|
||||||
# send some critical log messages to stdout as well
|
|
||||||
logStream = logging.StreamHandler()
|
logStream = logging.StreamHandler()
|
||||||
logStream.setLevel(STDOUT_LOGLVL)
|
logStream.setLevel(STDOUT_LOGLVL)
|
||||||
|
|
||||||
# create formatters for each stream
|
|
||||||
fileFormatter = logging.Formatter(
|
|
||||||
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
|
|
||||||
'%(message)s'
|
|
||||||
)
|
|
||||||
streamFormatter = logging.Formatter(
|
streamFormatter = logging.Formatter(
|
||||||
'<%(name)s> %(message)s'
|
'<%(name)s> %(levelname)s: %(message)s'
|
||||||
)
|
)
|
||||||
logFile.setFormatter(fileFormatter)
|
|
||||||
libLogFile.setFormatter(fileFormatter)
|
|
||||||
logStream.setFormatter(streamFormatter)
|
logStream.setFormatter(streamFormatter)
|
||||||
|
|
||||||
log = logging.getLogger('AVP')
|
log = logging.getLogger('AVP')
|
||||||
log.addHandler(logFile)
|
|
||||||
log.addHandler(logStream)
|
log.addHandler(logStream)
|
||||||
libLog = logging.getLogger()
|
|
||||||
libLog.addHandler(libLogFile)
|
if FILE_LOGLVL is not None:
|
||||||
# lowest level must be explicitly set on the root Logger
|
# write log files as well!
|
||||||
libLog.setLevel(0)
|
Core.logEnabled = True
|
||||||
|
logFilename = os.path.join(Core.logDir, 'avp_debug.log')
|
||||||
|
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
|
||||||
|
# delete old logs
|
||||||
|
for log_ in (logFilename, libLogFilename):
|
||||||
|
if os.path.exists(log_):
|
||||||
|
os.remove(log_)
|
||||||
|
|
||||||
|
logFile = logging.FileHandler(logFilename)
|
||||||
|
logFile.setLevel(FILE_LOGLVL)
|
||||||
|
libLogFile = logging.FileHandler(libLogFilename)
|
||||||
|
libLogFile.setLevel(FILE_LOGLVL)
|
||||||
|
fileFormatter = logging.Formatter(
|
||||||
|
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
|
||||||
|
'%(message)s'
|
||||||
|
)
|
||||||
|
logFile.setFormatter(fileFormatter)
|
||||||
|
libLogFile.setFormatter(fileFormatter)
|
||||||
|
|
||||||
|
libLog = logging.getLogger()
|
||||||
|
log.addHandler(logFile)
|
||||||
|
libLog.addHandler(libLogFile)
|
||||||
|
# lowest level must be explicitly set on the root Logger
|
||||||
|
libLog.setLevel(0)
|
||||||
|
|
||||||
# always store settings in class variables even if a Core object is not created
|
# always store settings in class variables even if a Core object is not created
|
||||||
Core.storeSettings()
|
Core.storeSettings()
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
'''
|
||||||
|
QCommand classes for every undoable user action performed in the MainWindow
|
||||||
|
'''
|
||||||
|
from PyQt5.QtWidgets import QUndoCommand
|
||||||
|
import os
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
from core import Core
|
||||||
|
|
||||||
|
|
||||||
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
# COMPONENT ACTIONS
|
||||||
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
|
||||||
|
class AddComponent(QUndoCommand):
|
||||||
|
def __init__(self, parent, compI, moduleI):
|
||||||
|
super().__init__(
|
||||||
|
"create new %s component" %
|
||||||
|
parent.core.modules[moduleI].Component.name
|
||||||
|
)
|
||||||
|
self.parent = parent
|
||||||
|
self.moduleI = moduleI
|
||||||
|
self.compI = compI
|
||||||
|
self.comp = None
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
if self.comp is None:
|
||||||
|
self.parent.core.insertComponent(
|
||||||
|
self.compI, self.moduleI, self.parent)
|
||||||
|
else:
|
||||||
|
# inserting previously-created component
|
||||||
|
self.parent.core.insertComponent(
|
||||||
|
self.compI, self.comp, self.parent)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.comp = self.parent.core.selectedComponents[self.compI]
|
||||||
|
self.parent._removeComponent(self.compI)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveComponent(QUndoCommand):
|
||||||
|
def __init__(self, parent, selectedRows):
|
||||||
|
super().__init__('remove component')
|
||||||
|
self.parent = parent
|
||||||
|
componentList = self.parent.window.listWidget_componentList
|
||||||
|
self.selectedRows = [
|
||||||
|
componentList.row(selected) for selected in selectedRows
|
||||||
|
]
|
||||||
|
self.components = [
|
||||||
|
parent.core.selectedComponents[i] for i in self.selectedRows
|
||||||
|
]
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
self.parent._removeComponent(self.selectedRows[0])
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
componentList = self.parent.window.listWidget_componentList
|
||||||
|
for index, comp in zip(self.selectedRows, self.components):
|
||||||
|
self.parent.core.insertComponent(
|
||||||
|
index, comp, self.parent
|
||||||
|
)
|
||||||
|
self.parent.drawPreview()
|
||||||
|
|
||||||
|
|
||||||
|
class MoveComponent(QUndoCommand):
|
||||||
|
def __init__(self, parent, row, newRow, tag):
|
||||||
|
super().__init__("move component %s" % tag)
|
||||||
|
self.parent = parent
|
||||||
|
self.row = row
|
||||||
|
self.newRow = newRow
|
||||||
|
self.id_ = ord(tag[0])
|
||||||
|
|
||||||
|
def id(self):
|
||||||
|
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
|
||||||
|
return self.id_
|
||||||
|
|
||||||
|
def mergeWith(self, other):
|
||||||
|
self.newRow = other.newRow
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do(self, rowa, rowb):
|
||||||
|
componentList = self.parent.window.listWidget_componentList
|
||||||
|
|
||||||
|
page = self.parent.pages.pop(rowa)
|
||||||
|
self.parent.pages.insert(rowb, page)
|
||||||
|
|
||||||
|
item = componentList.takeItem(rowa)
|
||||||
|
componentList.insertItem(rowb, item)
|
||||||
|
|
||||||
|
stackedWidget = self.parent.window.stackedWidget
|
||||||
|
widget = stackedWidget.removeWidget(page)
|
||||||
|
stackedWidget.insertWidget(rowb, page)
|
||||||
|
componentList.setCurrentRow(rowb)
|
||||||
|
stackedWidget.setCurrentIndex(rowb)
|
||||||
|
self.parent.core.moveComponent(rowa, rowb)
|
||||||
|
self.parent.drawPreview(True)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
self.do(self.row, self.newRow)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.do(self.newRow, self.row)
|
||||||
|
|
||||||
|
|
||||||
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
# PRESET ACTIONS
|
||||||
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
|
|
||||||
|
class ClearPreset(QUndoCommand):
|
||||||
|
def __init__(self, parent, compI):
|
||||||
|
super().__init__("clear preset")
|
||||||
|
self.parent = parent
|
||||||
|
self.compI = compI
|
||||||
|
self.component = self.parent.core.selectedComponents[compI]
|
||||||
|
self.store = self.component.savePreset()
|
||||||
|
self.store['preset'] = self.component.currentPreset
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
self.parent.core.clearPreset(self.compI)
|
||||||
|
self.parent.updateComponentTitle(self.compI, False)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.parent.core.selectedComponents[self.compI].loadPreset(self.store)
|
||||||
|
self.parent.updateComponentTitle(self.compI, self.store)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenPreset(QUndoCommand):
|
||||||
|
def __init__(self, parent, presetName, compI):
|
||||||
|
super().__init__("open %s preset" % presetName)
|
||||||
|
self.parent = parent
|
||||||
|
self.presetName = presetName
|
||||||
|
self.compI = compI
|
||||||
|
|
||||||
|
comp = self.parent.core.selectedComponents[compI]
|
||||||
|
self.store = comp.savePreset()
|
||||||
|
self.store['preset'] = copy(comp.currentPreset)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
self.parent._openPreset(self.presetName, self.compI)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.parent.core.selectedComponents[self.compI].loadPreset(
|
||||||
|
self.store)
|
||||||
|
self.parent.parent.updateComponentTitle(self.compI, self.store)
|
||||||
|
|
||||||
|
|
||||||
|
class RenamePreset(QUndoCommand):
|
||||||
|
def __init__(self, parent, path, oldName, newName):
|
||||||
|
super().__init__('rename preset')
|
||||||
|
self.parent = parent
|
||||||
|
self.path = path
|
||||||
|
self.oldName = oldName
|
||||||
|
self.newName = newName
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
self.parent.renamePreset(self.path, self.oldName, self.newName)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.parent.renamePreset(self.path, self.newName, self.oldName)
|
||||||
|
|
||||||
|
|
||||||
|
class DeletePreset(QUndoCommand):
|
||||||
|
def __init__(self, parent, compName, vers, presetFile):
|
||||||
|
self.parent = parent
|
||||||
|
self.preset = (compName, vers, presetFile)
|
||||||
|
self.path = os.path.join(
|
||||||
|
Core.presetDir, compName, str(vers), presetFile
|
||||||
|
)
|
||||||
|
self.store = self.parent.core.getPreset(self.path)
|
||||||
|
self.presetName = self.store['preset']
|
||||||
|
super().__init__('delete %s preset (%s)' % (self.presetName, compName))
|
||||||
|
self.loadedPresets = [
|
||||||
|
i for i, comp in enumerate(self.parent.core.selectedComponents)
|
||||||
|
if self.presetName == str(comp.currentPreset)
|
||||||
|
]
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
os.remove(self.path)
|
||||||
|
for i in self.loadedPresets:
|
||||||
|
self.parent.core.clearPreset(i)
|
||||||
|
self.parent.parent.updateComponentTitle(i, False)
|
||||||
|
self.parent.findPresets()
|
||||||
|
self.parent.drawPresetList()
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
self.parent.createNewPreset(*self.preset, self.store)
|
||||||
|
selectedComponents = self.parent.core.selectedComponents
|
||||||
|
for i in self.loadedPresets:
|
||||||
|
selectedComponents[i].currentPreset = self.presetName
|
||||||
|
self.parent.parent.updateComponentTitle(i)
|
||||||
|
self.parent.findPresets()
|
||||||
|
self.parent.drawPresetList()
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
@ -11,18 +11,22 @@ from queue import Queue
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import atexit
|
||||||
import filecmp
|
import filecmp
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from core import Core
|
from core import Core
|
||||||
import preview_thread
|
import gui.preview_thread as preview_thread
|
||||||
from preview_win import PreviewWindow
|
from gui.preview_win import PreviewWindow
|
||||||
from presetmanager import PresetManager
|
from gui.presetmanager import PresetManager
|
||||||
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
|
from gui.actions import *
|
||||||
|
from toolkit import (
|
||||||
|
disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('AVP.MainWindow')
|
log = logging.getLogger('AVP.Gui.MainWindow')
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow):
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
@ -41,11 +45,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
def __init__(self, window, project):
|
def __init__(self, window, project):
|
||||||
QtWidgets.QMainWindow.__init__(self)
|
QtWidgets.QMainWindow.__init__(self)
|
||||||
self.window = window
|
|
||||||
self.core = Core()
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
|
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
|
||||||
|
self.window = window
|
||||||
|
self.core = Core()
|
||||||
|
Core.mode = 'GUI'
|
||||||
# widgets of component settings
|
# widgets of component settings
|
||||||
self.pages = []
|
self.pages = []
|
||||||
self.lastAutosave = time.time()
|
self.lastAutosave = time.time()
|
||||||
|
@ -60,14 +64,24 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
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
|
||||||
|
|
||||||
|
# Register clean-up functions
|
||||||
|
signal.signal(signal.SIGINT, self.terminate)
|
||||||
|
atexit.register(self.cleanUp)
|
||||||
|
|
||||||
|
# Create stack of undoable user actions
|
||||||
|
self.undoStack = QtWidgets.QUndoStack(self)
|
||||||
|
undoLimit = self.settings.value("pref_undoLimit")
|
||||||
|
self.undoStack.setUndoLimit(undoLimit)
|
||||||
|
|
||||||
|
# Create Preset Manager
|
||||||
self.presetManager = PresetManager(
|
self.presetManager = PresetManager(
|
||||||
uic.loadUi(
|
uic.loadUi(
|
||||||
os.path.join(Core.wd, 'presetmanager.ui')), self)
|
os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
|
||||||
|
|
||||||
# Create the preview window and its thread, queues, and timers
|
# Create the preview window and its thread, queues, and timers
|
||||||
log.debug('Creating preview window')
|
log.debug('Creating preview window')
|
||||||
self.previewWindow = PreviewWindow(self, os.path.join(
|
self.previewWindow = PreviewWindow(self, os.path.join(
|
||||||
Core.wd, "background.png"))
|
Core.wd, 'gui', "background.png"))
|
||||||
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
|
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
|
||||||
|
|
||||||
log.debug('Starting preview thread')
|
log.debug('Starting preview thread')
|
||||||
|
@ -78,16 +92,58 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.previewWorker.moveToThread(self.previewThread)
|
self.previewWorker.moveToThread(self.previewThread)
|
||||||
self.previewWorker.imageCreated.connect(self.showPreviewImage)
|
self.previewWorker.imageCreated.connect(self.showPreviewImage)
|
||||||
self.previewThread.start()
|
self.previewThread.start()
|
||||||
|
self.previewThread.finished.connect(
|
||||||
|
lambda:
|
||||||
|
log.critical('PREVIEW THREAD DIED! This should never happen.')
|
||||||
|
)
|
||||||
|
|
||||||
log.debug('Starting preview timer')
|
timeout = 500
|
||||||
|
log.debug(
|
||||||
|
'Preview timer set to trigger when idle for %sms' % str(timeout)
|
||||||
|
)
|
||||||
self.timer = QtCore.QTimer(self)
|
self.timer = QtCore.QTimer(self)
|
||||||
self.timer.timeout.connect(self.processTask.emit)
|
self.timer.timeout.connect(self.processTask.emit)
|
||||||
self.timer.start(500)
|
self.timer.start(timeout)
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
style = window.pushButton_undo.style()
|
||||||
|
undoButton = window.pushButton_undo
|
||||||
|
undoButton.setIcon(
|
||||||
|
style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack)
|
||||||
|
)
|
||||||
|
undoButton.clicked.connect(self.undoStack.undo)
|
||||||
|
undoButton.setEnabled(False)
|
||||||
|
self.undoStack.cleanChanged.connect(
|
||||||
|
lambda change: undoButton.setEnabled(self.undoStack.count())
|
||||||
|
)
|
||||||
|
self.undoMenu = QMenu()
|
||||||
|
self.undoMenu.addAction(
|
||||||
|
self.undoStack.createUndoAction(self)
|
||||||
|
)
|
||||||
|
self.undoMenu.addAction(
|
||||||
|
self.undoStack.createRedoAction(self)
|
||||||
|
)
|
||||||
|
action = self.undoMenu.addAction('Show History...')
|
||||||
|
action.triggered.connect(
|
||||||
|
lambda _: self.showUndoStack()
|
||||||
|
)
|
||||||
|
undoButton.setMenu(self.undoMenu)
|
||||||
|
|
||||||
|
style = window.pushButton_listMoveUp.style()
|
||||||
|
window.pushButton_listMoveUp.setIcon(
|
||||||
|
style.standardIcon(QtWidgets.QStyle.SP_ArrowUp)
|
||||||
|
)
|
||||||
|
style = window.pushButton_listMoveDown.style()
|
||||||
|
window.pushButton_listMoveDown.setIcon(
|
||||||
|
style.standardIcon(QtWidgets.QStyle.SP_ArrowDown)
|
||||||
|
)
|
||||||
|
style = window.pushButton_removeComponent.style()
|
||||||
|
window.pushButton_removeComponent.setIcon(
|
||||||
|
style.standardIcon(QtWidgets.QStyle.SP_DialogDiscardButton)
|
||||||
|
)
|
||||||
|
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
log.debug(
|
log.debug(
|
||||||
'Darwin detected: showing progress label below progress bar')
|
'Darwin detected: showing progress label below progress bar')
|
||||||
|
@ -158,7 +214,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
for i, comp in enumerate(self.core.modules):
|
for i, comp in enumerate(self.core.modules):
|
||||||
action = self.compMenu.addAction(comp.Component.name)
|
action = self.compMenu.addAction(comp.Component.name)
|
||||||
action.triggered.connect(
|
action.triggered.connect(
|
||||||
lambda _, item=i: self.core.insertComponent(0, item, self)
|
lambda _, item=i: self.addComponent(0, item)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.window.pushButton_addComponent.setMenu(self.compMenu)
|
self.window.pushButton_addComponent.setMenu(self.compMenu)
|
||||||
|
@ -299,6 +355,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
|
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
|
||||||
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
|
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
|
||||||
|
|
||||||
|
QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo)
|
||||||
|
QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo)
|
||||||
|
QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo)
|
||||||
|
|
||||||
# Hotkeys for component list
|
# Hotkeys for component list
|
||||||
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
|
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
|
||||||
QtWidgets.QShortcut(
|
QtWidgets.QShortcut(
|
||||||
|
@ -339,21 +399,41 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
activated=lambda: self.moveComponent('bottom')
|
activated=lambda: self.moveComponent('bottom')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Debug Hotkeys
|
|
||||||
QtWidgets.QShortcut(
|
QtWidgets.QShortcut(
|
||||||
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
|
"Ctrl+Shift+F", self.window, self.showFfmpegCommand
|
||||||
)
|
)
|
||||||
QtWidgets.QShortcut(
|
QtWidgets.QShortcut(
|
||||||
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
|
"Ctrl+Shift+U", self.window, self.showUndoStack
|
||||||
|
)
|
||||||
|
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
QtWidgets.QShortcut(
|
||||||
|
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
|
||||||
|
)
|
||||||
|
QtWidgets.QShortcut(
|
||||||
|
"Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self))
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
'\n%s\n'
|
||||||
|
'#####\n'
|
||||||
|
'Preview thread is %s\n' % (
|
||||||
|
repr(self.core),
|
||||||
|
'live' if self.previewThread.isRunning() else 'dead',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@QtCore.pyqtSlot()
|
|
||||||
def cleanUp(self, *args):
|
def cleanUp(self, *args):
|
||||||
log.info('Ending the preview thread')
|
log.info('Ending the preview thread')
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
self.previewThread.quit()
|
self.previewThread.quit()
|
||||||
self.previewThread.wait()
|
self.previewThread.wait()
|
||||||
|
|
||||||
|
def terminate(self, *args):
|
||||||
|
self.cleanUp()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
@disableWhenOpeningProject
|
@disableWhenOpeningProject
|
||||||
def updateWindowTitle(self):
|
def updateWindowTitle(self):
|
||||||
appName = 'Audio Visualizer'
|
appName = 'Audio Visualizer'
|
||||||
|
@ -366,35 +446,43 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
appName += '*'
|
appName += '*'
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
log.debug('Setting window title to %s' % appName)
|
log.verbose('Setting window title to %s' % appName)
|
||||||
self.window.setWindowTitle(appName)
|
self.window.setWindowTitle(appName)
|
||||||
|
|
||||||
@QtCore.pyqtSlot(int, dict)
|
@QtCore.pyqtSlot(int, dict)
|
||||||
def updateComponentTitle(self, pos, presetStore=False):
|
def updateComponentTitle(self, pos, presetStore=False):
|
||||||
|
'''
|
||||||
|
Sets component title to modified or unmodified when given boolean.
|
||||||
|
If given a preset dict, compares it against the component to
|
||||||
|
determine if it is modified.
|
||||||
|
A component with no preset is always unmodified.
|
||||||
|
'''
|
||||||
if type(presetStore) is dict:
|
if type(presetStore) is dict:
|
||||||
name = presetStore['preset']
|
name = presetStore['preset']
|
||||||
if name is None or name not in self.core.savedPresets:
|
if name is None or name not in self.core.savedPresets:
|
||||||
modified = False
|
modified = False
|
||||||
else:
|
else:
|
||||||
modified = (presetStore != self.core.savedPresets[name])
|
modified = (presetStore != self.core.savedPresets[name])
|
||||||
else:
|
|
||||||
modified = bool(presetStore)
|
modified = bool(presetStore)
|
||||||
if pos < 0:
|
if pos < 0:
|
||||||
pos = len(self.core.selectedComponents)-1
|
pos = len(self.core.selectedComponents)-1
|
||||||
name = str(self.core.selectedComponents[pos])
|
name = self.core.selectedComponents[pos].name
|
||||||
title = str(name)
|
title = str(name)
|
||||||
if self.core.selectedComponents[pos].currentPreset:
|
if self.core.selectedComponents[pos].currentPreset:
|
||||||
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
|
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
|
||||||
if modified:
|
if modified:
|
||||||
title += '*'
|
title += '*'
|
||||||
if type(presetStore) is bool:
|
if type(presetStore) is bool:
|
||||||
log.debug('Forcing %s #%s\'s modified status to %s: %s' % (
|
log.debug(
|
||||||
|
'Forcing %s #%s\'s modified status to %s: %s',
|
||||||
name, pos, modified, title
|
name, pos, modified, title
|
||||||
))
|
)
|
||||||
else:
|
else:
|
||||||
log.debug('Setting %s #%s\'s title: %s' % (
|
log.debug(
|
||||||
|
'Setting %s #%s\'s title: %s',
|
||||||
name, pos, title
|
name, pos, title
|
||||||
))
|
)
|
||||||
self.window.listWidget_componentList.item(pos).setText(title)
|
self.window.listWidget_componentList.item(pos).setText(title)
|
||||||
|
|
||||||
def updateCodecs(self):
|
def updateCodecs(self):
|
||||||
|
@ -471,7 +559,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
return True
|
return True
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
log.error(
|
log.error(
|
||||||
'Project file couldn\'t be located:', self.currentProject)
|
'Project file couldn\'t be located: %s', self.currentProject)
|
||||||
return identical
|
return identical
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -568,6 +656,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
detail=detail,
|
detail=detail,
|
||||||
icon='Critical',
|
icon='Critical',
|
||||||
)
|
)
|
||||||
|
log.info('%s', repr(self))
|
||||||
|
|
||||||
def changeEncodingStatus(self, status):
|
def changeEncodingStatus(self, status):
|
||||||
self.encoding = status
|
self.encoding = status
|
||||||
|
@ -651,6 +740,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
def showPreviewImage(self, image):
|
def showPreviewImage(self, image):
|
||||||
self.previewWindow.changePixmap(image)
|
self.previewWindow.changePixmap(image)
|
||||||
|
|
||||||
|
def showUndoStack(self):
|
||||||
|
dialog = QtWidgets.QDialog(self.window)
|
||||||
|
undoView = QtWidgets.QUndoView(self.undoStack)
|
||||||
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
layout.addWidget(undoView)
|
||||||
|
dialog.setLayout(layout)
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
def showFfmpegCommand(self):
|
def showFfmpegCommand(self):
|
||||||
from textwrap import wrap
|
from textwrap import wrap
|
||||||
from toolkit.ffmpeg import createFfmpegCommand
|
from toolkit.ffmpeg import createFfmpegCommand
|
||||||
|
@ -664,7 +761,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
|
msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def addComponent(self, compPos, moduleIndex):
|
||||||
|
'''Creates an undoable action that adds a new component.'''
|
||||||
|
action = AddComponent(self, compPos, moduleIndex)
|
||||||
|
self.undoStack.push(action)
|
||||||
|
|
||||||
def insertComponent(self, index):
|
def insertComponent(self, index):
|
||||||
|
'''Triggered by Core to finish initializing a new component.'''
|
||||||
componentList = self.window.listWidget_componentList
|
componentList = self.window.listWidget_componentList
|
||||||
stackedWidget = self.window.stackedWidget
|
stackedWidget = self.window.stackedWidget
|
||||||
|
|
||||||
|
@ -685,41 +788,38 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
def removeComponent(self):
|
def removeComponent(self):
|
||||||
componentList = self.window.listWidget_componentList
|
componentList = self.window.listWidget_componentList
|
||||||
|
selected = componentList.selectedItems()
|
||||||
|
if selected:
|
||||||
|
action = RemoveComponent(self, selected)
|
||||||
|
self.undoStack.push(action)
|
||||||
|
|
||||||
for selected in componentList.selectedItems():
|
def _removeComponent(self, index):
|
||||||
index = componentList.row(selected)
|
stackedWidget = self.window.stackedWidget
|
||||||
self.window.stackedWidget.removeWidget(self.pages[index])
|
componentList = self.window.listWidget_componentList
|
||||||
componentList.takeItem(index)
|
stackedWidget.removeWidget(self.pages[index])
|
||||||
self.core.removeComponent(index)
|
componentList.takeItem(index)
|
||||||
self.pages.pop(index)
|
self.core.removeComponent(index)
|
||||||
self.changeComponentWidget()
|
self.pages.pop(index)
|
||||||
|
self.changeComponentWidget()
|
||||||
self.drawPreview()
|
self.drawPreview()
|
||||||
|
|
||||||
@disableWhenEncoding
|
@disableWhenEncoding
|
||||||
def moveComponent(self, change):
|
def moveComponent(self, change):
|
||||||
'''Moves a component relatively from its current position'''
|
'''Moves a component relatively from its current position'''
|
||||||
componentList = self.window.listWidget_componentList
|
componentList = self.window.listWidget_componentList
|
||||||
|
tag = change
|
||||||
if change == 'top':
|
if change == 'top':
|
||||||
change = -componentList.currentRow()
|
change = -componentList.currentRow()
|
||||||
elif change == 'bottom':
|
elif change == 'bottom':
|
||||||
change = len(componentList)-componentList.currentRow()-1
|
change = len(componentList)-componentList.currentRow()-1
|
||||||
stackedWidget = self.window.stackedWidget
|
else:
|
||||||
|
tag = 'down' if change == 1 else 'up'
|
||||||
|
|
||||||
row = componentList.currentRow()
|
row = componentList.currentRow()
|
||||||
newRow = row + change
|
newRow = row + change
|
||||||
if newRow > -1 and newRow < componentList.count():
|
if newRow > -1 and newRow < componentList.count():
|
||||||
self.core.moveComponent(row, newRow)
|
action = MoveComponent(self, row, newRow, tag)
|
||||||
|
self.undoStack.push(action)
|
||||||
# update widgets
|
|
||||||
page = self.pages.pop(row)
|
|
||||||
self.pages.insert(newRow, page)
|
|
||||||
item = componentList.takeItem(row)
|
|
||||||
newItem = componentList.insertItem(newRow, item)
|
|
||||||
widget = stackedWidget.removeWidget(page)
|
|
||||||
stackedWidget.insertWidget(newRow, page)
|
|
||||||
componentList.setCurrentRow(newRow)
|
|
||||||
stackedWidget.setCurrentIndex(newRow)
|
|
||||||
self.drawPreview(True)
|
|
||||||
|
|
||||||
def getComponentListMousePos(self, position):
|
def getComponentListMousePos(self, position):
|
||||||
'''
|
'''
|
||||||
|
@ -777,11 +877,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
self.window.lineEdit_audioFile,
|
self.window.lineEdit_audioFile,
|
||||||
self.window.lineEdit_outputFile
|
self.window.lineEdit_outputFile
|
||||||
):
|
):
|
||||||
field.blockSignals(True)
|
with blockSignals(field):
|
||||||
field.setText('')
|
field.setText('')
|
||||||
field.blockSignals(False)
|
|
||||||
self.progressBarUpdated(0)
|
self.progressBarUpdated(0)
|
||||||
self.progressBarSetText('')
|
self.progressBarSetText('')
|
||||||
|
self.undoStack.clear()
|
||||||
|
|
||||||
@disableWhenEncoding
|
@disableWhenEncoding
|
||||||
def createNewProject(self, prompt=True):
|
def createNewProject(self, prompt=True):
|
||||||
|
@ -845,7 +945,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
def openProject(self, filepath, prompt=True):
|
def openProject(self, filepath, prompt=True):
|
||||||
if not filepath or not os.path.exists(filepath) \
|
if not filepath or not os.path.exists(filepath) \
|
||||||
or not filepath.endswith('.avp'):
|
or not filepath.endswith('.avp'):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
|
@ -928,19 +1028,10 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
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.addComponent(
|
||||||
0 if insertCompAtTop else index, item, self
|
0 if insertCompAtTop else index, item
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
|
@ -110,6 +110,13 @@
|
||||||
<property name="sizeConstraint">
|
<property name="sizeConstraint">
|
||||||
<enum>QLayout::SetMinimumSize</enum>
|
<enum>QLayout::SetMinimumSize</enum>
|
||||||
</property>
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pushButton_undo">
|
||||||
|
<property name="text">
|
||||||
|
<string>Undo</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer_6">
|
<spacer name="horizontalSpacer_6">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
|
@ -5,9 +5,14 @@
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
import string
|
import string
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from toolkit import badName
|
from toolkit import badName
|
||||||
from core import Core
|
from core import Core
|
||||||
|
from gui.actions import *
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('AVP.Gui.PresetManager')
|
||||||
|
|
||||||
|
|
||||||
class PresetManager(QtWidgets.QDialog):
|
class PresetManager(QtWidgets.QDialog):
|
||||||
|
@ -130,8 +135,8 @@ class PresetManager(QtWidgets.QDialog):
|
||||||
def clearPreset(self, compI=None):
|
def clearPreset(self, compI=None):
|
||||||
'''Functions on mainwindow level from the context menu'''
|
'''Functions on mainwindow level from the context menu'''
|
||||||
compI = self.parent.window.listWidget_componentList.currentRow()
|
compI = self.parent.window.listWidget_componentList.currentRow()
|
||||||
self.core.clearPreset(compI)
|
action = ClearPreset(self.parent, compI)
|
||||||
self.parent.updateComponentTitle(compI, False)
|
self.parent.undoStack.push(action)
|
||||||
|
|
||||||
def openSavePresetDialog(self):
|
def openSavePresetDialog(self):
|
||||||
'''Functions on mainwindow level from the context menu'''
|
'''Functions on mainwindow level from the context menu'''
|
||||||
|
@ -196,12 +201,16 @@ class PresetManager(QtWidgets.QDialog):
|
||||||
|
|
||||||
def openPreset(self, presetName, compPos=None):
|
def openPreset(self, presetName, compPos=None):
|
||||||
componentList = self.parent.window.listWidget_componentList
|
componentList = self.parent.window.listWidget_componentList
|
||||||
selectedComponents = self.core.selectedComponents
|
|
||||||
|
|
||||||
index = compPos if compPos is not None else componentList.currentRow()
|
index = compPos if compPos is not None else componentList.currentRow()
|
||||||
if index == -1:
|
if index == -1:
|
||||||
return
|
return
|
||||||
componentName = str(selectedComponents[index]).strip()
|
action = OpenPreset(self, presetName, index)
|
||||||
|
self.parent.undoStack.push(action)
|
||||||
|
|
||||||
|
def _openPreset(self, presetName, index):
|
||||||
|
selectedComponents = self.core.selectedComponents
|
||||||
|
|
||||||
|
componentName = selectedComponents[index].name.strip()
|
||||||
version = selectedComponents[index].version
|
version = selectedComponents[index].version
|
||||||
dirname = os.path.join(self.presetDir, componentName, str(version))
|
dirname = os.path.join(self.presetDir, componentName, str(version))
|
||||||
filepath = os.path.join(dirname, presetName)
|
filepath = os.path.join(dirname, presetName)
|
||||||
|
@ -224,16 +233,10 @@ class PresetManager(QtWidgets.QDialog):
|
||||||
if not ch:
|
if not ch:
|
||||||
return
|
return
|
||||||
self.deletePreset(comp, vers, name)
|
self.deletePreset(comp, vers, name)
|
||||||
self.findPresets()
|
|
||||||
self.drawPresetList()
|
|
||||||
|
|
||||||
for i, comp in enumerate(self.core.selectedComponents):
|
|
||||||
if comp.currentPreset == name:
|
|
||||||
self.clearPreset(i)
|
|
||||||
|
|
||||||
def deletePreset(self, comp, vers, name):
|
def deletePreset(self, comp, vers, name):
|
||||||
filepath = os.path.join(self.presetDir, comp, str(vers), name)
|
action = DeletePreset(self, comp, vers, name)
|
||||||
os.remove(filepath)
|
self.parent.undoStack.push(action)
|
||||||
|
|
||||||
def warnMessage(self, window=None):
|
def warnMessage(self, window=None):
|
||||||
self.parent.showMessage(
|
self.parent.showMessage(
|
||||||
|
@ -270,7 +273,6 @@ class PresetManager(QtWidgets.QDialog):
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def openRenamePresetDialog(self):
|
def openRenamePresetDialog(self):
|
||||||
# TODO: maintain consistency by changing this to call createNewPreset()
|
|
||||||
presetList = self.window.listWidget_presets
|
presetList = self.window.listWidget_presets
|
||||||
index = self.getPresetRow()
|
index = self.getPresetRow()
|
||||||
if index == -1:
|
if index == -1:
|
||||||
|
@ -293,22 +295,28 @@ class PresetManager(QtWidgets.QDialog):
|
||||||
path = os.path.join(
|
path = os.path.join(
|
||||||
self.presetDir, comp, str(vers))
|
self.presetDir, comp, str(vers))
|
||||||
newPath = os.path.join(path, newName)
|
newPath = os.path.join(path, newName)
|
||||||
oldPath = os.path.join(path, oldName)
|
|
||||||
if self.presetExists(newPath):
|
if self.presetExists(newPath):
|
||||||
return
|
return
|
||||||
if os.path.exists(newPath):
|
action = RenamePreset(self, path, oldName, newName)
|
||||||
os.remove(newPath)
|
self.parent.undoStack.push(action)
|
||||||
os.rename(oldPath, newPath)
|
|
||||||
self.findPresets()
|
|
||||||
self.drawPresetList()
|
|
||||||
for i, comp in enumerate(self.core.selectedComponents):
|
|
||||||
if getPresetDir(comp) == path \
|
|
||||||
and comp.currentPreset == oldName:
|
|
||||||
self.core.openPreset(newPath, i, newName)
|
|
||||||
self.parent.updateComponentTitle(i, False)
|
|
||||||
self.parent.drawPreview()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def renamePreset(self, path, oldName, newName):
|
||||||
|
oldPath = os.path.join(path, oldName)
|
||||||
|
newPath = os.path.join(path, newName)
|
||||||
|
if os.path.exists(newPath):
|
||||||
|
os.remove(newPath)
|
||||||
|
os.rename(oldPath, newPath)
|
||||||
|
self.findPresets()
|
||||||
|
self.drawPresetList()
|
||||||
|
path = os.path.dirname(newPath)
|
||||||
|
for i, comp in enumerate(self.core.selectedComponents):
|
||||||
|
if self.core.getPresetDir(comp) == path \
|
||||||
|
and comp.currentPreset == oldName:
|
||||||
|
self.core.openPreset(newPath, i, newName)
|
||||||
|
self.parent.updateComponentTitle(i, False)
|
||||||
|
self.parent.drawPreview()
|
||||||
|
|
||||||
def openImportDialog(self):
|
def openImportDialog(self):
|
||||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||||
self.window, "Import Preset File",
|
self.window, "Import Preset File",
|
||||||
|
@ -351,8 +359,3 @@ 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))
|
|
|
@ -14,7 +14,7 @@ from toolkit.frame import Checkerboard
|
||||||
from toolkit import disableWhenOpeningProject
|
from toolkit import disableWhenOpeningProject
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger("AVP.PreviewThread")
|
log = logging.getLogger("AVP.Gui.PreviewThread")
|
||||||
|
|
||||||
|
|
||||||
class Worker(QtCore.QObject):
|
class Worker(QtCore.QObject):
|
||||||
|
@ -45,8 +45,6 @@ class Worker(QtCore.QObject):
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def process(self):
|
def process(self):
|
||||||
width = int(self.settings.value('outputWidth'))
|
|
||||||
height = int(self.settings.value('outputHeight'))
|
|
||||||
try:
|
try:
|
||||||
nextPreviewInformation = self.queue.get(block=False)
|
nextPreviewInformation = self.queue.get(block=False)
|
||||||
while self.queue.qsize() >= 2:
|
while self.queue.qsize() >= 2:
|
||||||
|
@ -54,12 +52,14 @@ class Worker(QtCore.QObject):
|
||||||
self.queue.get(block=False)
|
self.queue.get(block=False)
|
||||||
except Empty:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
|
width = int(self.settings.value('outputWidth'))
|
||||||
|
height = int(self.settings.value('outputHeight'))
|
||||||
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)
|
||||||
|
|
||||||
frame = self.background.copy()
|
frame = self.background.copy()
|
||||||
log.debug('Creating new preview frame')
|
log.info('Creating new preview frame')
|
||||||
components = nextPreviewInformation["components"]
|
components = nextPreviewInformation["components"]
|
||||||
for component in reversed(components):
|
for component in reversed(components):
|
||||||
try:
|
try:
|
|
@ -1,14 +1,14 @@
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger('AVP.Gui.PreviewWindow')
|
||||||
|
|
||||||
|
|
||||||
class PreviewWindow(QtWidgets.QLabel):
|
class PreviewWindow(QtWidgets.QLabel):
|
||||||
'''
|
'''
|
||||||
Paints the preview QLabel in MainWindow and maintains the aspect ratio
|
Paints the preview QLabel in MainWindow and maintains the aspect ratio
|
||||||
when the window is resized.
|
when the window is resized.
|
||||||
'''
|
'''
|
||||||
log = logging.getLogger('AVP.PreviewWindow')
|
|
||||||
|
|
||||||
def __init__(self, parent, img):
|
def __init__(self, parent, img):
|
||||||
super(PreviewWindow, self).__init__()
|
super(PreviewWindow, self).__init__()
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
@ -41,17 +41,15 @@ class PreviewWindow(QtWidgets.QLabel):
|
||||||
if i >= 0:
|
if i >= 0:
|
||||||
component = self.parent.core.selectedComponents[i]
|
component = self.parent.core.selectedComponents[i]
|
||||||
if not hasattr(component, 'previewClickEvent'):
|
if not hasattr(component, 'previewClickEvent'):
|
||||||
self.log.info('Ignored click event')
|
|
||||||
return
|
return
|
||||||
pos = (event.x(), event.y())
|
pos = (event.x(), event.y())
|
||||||
size = (self.width(), self.height())
|
size = (self.width(), self.height())
|
||||||
butt = event.button()
|
butt = event.button()
|
||||||
self.log.info('Click event for #%s: %s button %s' % (
|
log.info('Click event for #%s: %s button %s' % (
|
||||||
i, pos, butt))
|
i, pos, butt))
|
||||||
component.previewClickEvent(
|
component.previewClickEvent(
|
||||||
pos, size, butt
|
pos, size, butt
|
||||||
)
|
)
|
||||||
self.parent.core.updateComponent(i)
|
|
||||||
|
|
||||||
@QtCore.pyqtSlot(str)
|
@QtCore.pyqtSlot(str)
|
||||||
def threadError(self, msg):
|
def threadError(self, msg):
|
||||||
|
@ -60,3 +58,4 @@ class PreviewWindow(QtWidgets.QLabel):
|
||||||
icon='Critical',
|
icon='Critical',
|
||||||
parent=self
|
parent=self
|
||||||
)
|
)
|
||||||
|
log.info('%', repr(self.parent))
|
11
src/main.py
11
src/main.py
|
@ -6,7 +6,7 @@ import logging
|
||||||
from __init__ import wd
|
from __init__ import wd
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('AVP.Entrypoint')
|
log = logging.getLogger('AVP.Main')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -35,11 +35,9 @@ def main():
|
||||||
log.debug("Finished creating command object")
|
log.debug("Finished creating command object")
|
||||||
|
|
||||||
elif mode == 'GUI':
|
elif mode == 'GUI':
|
||||||
from mainwindow import MainWindow
|
from gui.mainwindow import MainWindow
|
||||||
import atexit
|
|
||||||
import signal
|
|
||||||
|
|
||||||
window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
|
window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
|
||||||
# window.adjustSize()
|
# window.adjustSize()
|
||||||
desc = QtWidgets.QDesktopWidget()
|
desc = QtWidgets.QDesktopWidget()
|
||||||
dpi = desc.physicalDpiX()
|
dpi = desc.physicalDpiX()
|
||||||
|
@ -56,9 +54,6 @@ def main():
|
||||||
log.debug("Finished creating main window")
|
log.debug("Finished creating main window")
|
||||||
window.raise_()
|
window.raise_()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, main.cleanUp)
|
|
||||||
atexit.register(main.cleanUp)
|
|
||||||
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -6,9 +6,59 @@ import string
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from copy import copy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('AVP.Toolkit.Common')
|
||||||
|
|
||||||
|
|
||||||
|
class blockSignals:
|
||||||
|
'''
|
||||||
|
Context manager to temporarily block list of QtWidgets from updating,
|
||||||
|
and guarantee restoring the previous state afterwards.
|
||||||
|
'''
|
||||||
|
def __init__(self, widgets):
|
||||||
|
if type(widgets) is dict:
|
||||||
|
self.widgets = concatDictVals(widgets)
|
||||||
|
else:
|
||||||
|
self.widgets = (
|
||||||
|
widgets if hasattr(widgets, '__iter__')
|
||||||
|
else [widgets]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
log.verbose(
|
||||||
|
'Blocking signals for %s',
|
||||||
|
", ".join([
|
||||||
|
str(w.__class__.__name__) for w in self.widgets
|
||||||
|
])
|
||||||
|
)
|
||||||
|
self.oldStates = [w.signalsBlocked() for w in self.widgets]
|
||||||
|
for w in self.widgets:
|
||||||
|
w.blockSignals(True)
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
log.verbose(
|
||||||
|
'Resetting blockSignals to %s', str(bool(sum(self.oldStates))))
|
||||||
|
for w, state in zip(self.widgets, self.oldStates):
|
||||||
|
w.blockSignals(state)
|
||||||
|
|
||||||
|
|
||||||
|
def concatDictVals(d):
|
||||||
|
'''Concatenates all values in given dict into one list.'''
|
||||||
|
key, value = d.popitem()
|
||||||
|
d[key] = value
|
||||||
|
final = copy(value)
|
||||||
|
if type(final) is not list:
|
||||||
|
final = [final]
|
||||||
|
final.extend([val for val in d.values()])
|
||||||
|
else:
|
||||||
|
value.extend([item for val in d.values() for item in val])
|
||||||
|
return final
|
||||||
|
|
||||||
|
|
||||||
def badName(name):
|
def badName(name):
|
||||||
'''Returns whether a name contains non-alphanumeric chars'''
|
'''Returns whether a name contains non-alphanumeric chars'''
|
||||||
return any([letter in string.punctuation for letter in name])
|
return any([letter in string.punctuation for letter in name])
|
||||||
|
@ -34,6 +84,7 @@ def appendUppercase(lst):
|
||||||
lst.append(form.upper())
|
lst.append(form.upper())
|
||||||
return lst
|
return lst
|
||||||
|
|
||||||
|
|
||||||
def pipeWrapper(func):
|
def pipeWrapper(func):
|
||||||
'''A decorator to insert proper kwargs into Popen objects.'''
|
'''A decorator to insert proper kwargs into Popen objects.'''
|
||||||
def pipeWrapper(commandList, **kwargs):
|
def pipeWrapper(commandList, **kwargs):
|
||||||
|
@ -107,12 +158,14 @@ def connectWidget(widget, func):
|
||||||
elif type(widget) == QtWidgets.QComboBox:
|
elif type(widget) == QtWidgets.QComboBox:
|
||||||
widget.currentIndexChanged.connect(func)
|
widget.currentIndexChanged.connect(func)
|
||||||
else:
|
else:
|
||||||
|
log.warning('Failed to connect %s ', str(widget.__class__.__name__))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def setWidgetValue(widget, val):
|
def setWidgetValue(widget, val):
|
||||||
'''Generic setValue method for use with any typical QtWidget'''
|
'''Generic setValue method for use with any typical QtWidget'''
|
||||||
|
log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val))
|
||||||
if type(widget) == QtWidgets.QLineEdit:
|
if type(widget) == QtWidgets.QLineEdit:
|
||||||
widget.setText(val)
|
widget.setText(val)
|
||||||
elif type(widget) == QtWidgets.QSpinBox \
|
elif type(widget) == QtWidgets.QSpinBox \
|
||||||
|
@ -123,6 +176,7 @@ def setWidgetValue(widget, val):
|
||||||
elif type(widget) == QtWidgets.QComboBox:
|
elif type(widget) == QtWidgets.QComboBox:
|
||||||
widget.setCurrentIndex(val)
|
widget.setCurrentIndex(val)
|
||||||
else:
|
else:
|
||||||
|
log.warning('Failed to set %s ', str(widget.__class__.__name__))
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -91,16 +91,24 @@ class FfmpegVideo:
|
||||||
|
|
||||||
def fillBuffer(self):
|
def fillBuffer(self):
|
||||||
from component import ComponentError
|
from component import ComponentError
|
||||||
logFilename = os.path.join(
|
if core.Core.logEnabled:
|
||||||
core.Core.logDir, 'render_%s.log' % str(self.component.compPos))
|
logFilename = os.path.join(
|
||||||
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
|
core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
|
||||||
with open(logFilename, 'w') as logf:
|
)
|
||||||
logf.write(" ".join(self.command) + '\n\n')
|
log.debug('Creating ffmpeg process (log at %s)', logFilename)
|
||||||
with open(logFilename, 'a') as logf:
|
with open(logFilename, 'w') as logf:
|
||||||
|
logf.write(" ".join(self.command) + '\n\n')
|
||||||
|
with open(logFilename, 'a') as logf:
|
||||||
|
self.pipe = openPipe(
|
||||||
|
self.command, stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
|
||||||
|
)
|
||||||
|
else:
|
||||||
self.pipe = openPipe(
|
self.pipe = openPipe(
|
||||||
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
|
||||||
stderr=logf, bufsize=10**8
|
stderr=subprocess.DEVNULL, bufsize=10**8
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if self.parent.canceled:
|
if self.parent.canceled:
|
||||||
break
|
break
|
||||||
|
@ -157,7 +165,7 @@ def findFfmpeg():
|
||||||
['ffmpeg', '-version'], stderr=f
|
['ffmpeg', '-version'], stderr=f
|
||||||
)
|
)
|
||||||
return "ffmpeg"
|
return "ffmpeg"
|
||||||
except subprocess.CalledProcessError:
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
return "avconv"
|
return "avconv"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ class FramePainter(QtGui.QPainter):
|
||||||
Pillow image with finalize()
|
Pillow image with finalize()
|
||||||
'''
|
'''
|
||||||
def __init__(self, width, height):
|
def __init__(self, width, height):
|
||||||
log.verbose('Creating new FramePainter')
|
|
||||||
image = BlankFrame(width, height)
|
image = BlankFrame(width, height)
|
||||||
self.image = QtGui.QImage(ImageQt(image))
|
self.image = QtGui.QImage(ImageQt(image))
|
||||||
super().__init__(self.image)
|
super().__init__(self.image)
|
||||||
|
@ -33,6 +32,7 @@ class FramePainter(QtGui.QPainter):
|
||||||
super().setPen(penStyle)
|
super().setPen(penStyle)
|
||||||
|
|
||||||
def finalize(self):
|
def finalize(self):
|
||||||
|
log.verbose("Finalizing FramePainter")
|
||||||
imBytes = self.image.bits().asstring(self.image.byteCount())
|
imBytes = self.image.bits().asstring(self.image.byteCount())
|
||||||
frame = Image.frombytes(
|
frame = Image.frombytes(
|
||||||
'RGBA', (self.image.width(), self.image.height()), imBytes
|
'RGBA', (self.image.width(), self.image.height()), imBytes
|
||||||
|
@ -78,8 +78,6 @@ def defaultSize(framefunc):
|
||||||
|
|
||||||
|
|
||||||
def FloodFrame(width, height, RgbaTuple):
|
def FloodFrame(width, height, RgbaTuple):
|
||||||
log.verbose('Creating new %s*%s %s flood frame' % (
|
|
||||||
width, height, RgbaTuple))
|
|
||||||
return Image.new("RGBA", (width, height), RgbaTuple)
|
return Image.new("RGBA", (width, height), RgbaTuple)
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +96,7 @@ def Checkerboard(width, height):
|
||||||
log.debug('Creating new %s*%s checkerboard' % (width, height))
|
log.debug('Creating new %s*%s 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.Core.wd, "background.png")),
|
os.path.join(core.Core.wd, 'gui', "background.png")),
|
||||||
(0, 0)
|
(0, 0)
|
||||||
)
|
)
|
||||||
image = image.resize((width, height))
|
image = image.resize((width, height))
|
||||||
|
|
|
@ -179,7 +179,7 @@ class Worker(QtCore.QObject):
|
||||||
for num, component in enumerate(reversed(self.components))
|
for num, component in enumerate(reversed(self.components))
|
||||||
])
|
])
|
||||||
print('Loaded Components:', initText)
|
print('Loaded Components:', initText)
|
||||||
log.info('Calling preFrameRender for %s' % initText)
|
log.info('Calling preFrameRender for %s', initText)
|
||||||
self.staticComponents = {}
|
self.staticComponents = {}
|
||||||
for compNo, comp in enumerate(reversed(self.components)):
|
for compNo, comp in enumerate(reversed(self.components)):
|
||||||
try:
|
try:
|
||||||
|
@ -221,12 +221,13 @@ class Worker(QtCore.QObject):
|
||||||
|
|
||||||
if self.canceled:
|
if self.canceled:
|
||||||
if canceledByComponent:
|
if canceledByComponent:
|
||||||
log.error('Export cancelled by component #%s (%s): %s' % (
|
log.error(
|
||||||
|
'Export cancelled by component #%s (%s): %s',
|
||||||
compNo,
|
compNo,
|
||||||
comp.name,
|
comp.name,
|
||||||
'No message.' if comp.error() is None else (
|
'No message.' if comp.error() is None else (
|
||||||
comp.error() if type(comp.error()) is str
|
comp.error() if type(comp.error()) is str
|
||||||
else comp.error()[0])
|
else comp.error()[0]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.cancelExport()
|
self.cancelExport()
|
||||||
|
@ -251,9 +252,14 @@ class Worker(QtCore.QObject):
|
||||||
print('############################')
|
print('############################')
|
||||||
log.info('Opening pipe to ffmpeg')
|
log.info('Opening pipe to ffmpeg')
|
||||||
log.info(cmd)
|
log.info(cmd)
|
||||||
self.out_pipe = openPipe(
|
try:
|
||||||
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
|
self.out_pipe = openPipe(
|
||||||
)
|
ffmpegCommand,
|
||||||
|
stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
|
||||||
|
)
|
||||||
|
except sp.CalledProcessError:
|
||||||
|
log.critical('Ffmpeg pipe couldn\'t be created!')
|
||||||
|
raise
|
||||||
|
|
||||||
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
|
||||||
# START CREATING THE VIDEO
|
# START CREATING THE VIDEO
|
||||||
|
|
Reference in New Issue