From a327bec4e42cc572fb84e559025e888a4a20edd3 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 16:39:53 -0400 Subject: organizing GUImode-specific code --- src/gui/presetmanager.py | 358 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 src/gui/presetmanager.py (limited to 'src/gui/presetmanager.py') diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py new file mode 100644 index 0000000..b1eeb34 --- /dev/null +++ b/src/gui/presetmanager.py @@ -0,0 +1,358 @@ +''' + Preset manager object handles all interactions with presets, including + the context menu accessed from MainWindow. +''' +from PyQt5 import QtCore, QtWidgets +import string +import os + +from toolkit import badName +from core import Core + + +class PresetManager(QtWidgets.QDialog): + def __init__(self, window, parent): + super().__init__(parent.window) + self.parent = parent + self.core = parent.core + self.settings = parent.settings + self.presetDir = parent.presetDir + if not self.settings.value('presetDir'): + self.settings.setValue( + "presetDir", + os.path.join(parent.dataDir, 'projects')) + + self.findPresets() + + # window + self.lastFilter = '*' + self.presetRows = [] # list of (comp, vers, name) tuples + self.window = window + self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + + # connect button signals + self.window.pushButton_delete.clicked.connect( + self.openDeletePresetDialog + ) + self.window.pushButton_rename.clicked.connect( + self.openRenamePresetDialog + ) + self.window.pushButton_import.clicked.connect( + self.openImportDialog + ) + self.window.pushButton_export.clicked.connect( + self.openExportDialog + ) + self.window.pushButton_close.clicked.connect( + self.window.close + ) + + # create filter box and preset list + self.drawFilterList() + self.window.comboBox_filter.currentIndexChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() + ) + ) + + # make auto-completion for search bar + self.autocomplete = QtCore.QStringListModel() + completer = QtWidgets.QCompleter() + completer.setModel(self.autocomplete) + self.window.lineEdit_search.setCompleter(completer) + self.window.lineEdit_search.textChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() + ) + ) + self.drawPresetList('*') + + def show(self): + '''Open a new preset manager window from the mainwindow''' + self.findPresets() + self.drawFilterList() + self.drawPresetList('*') + self.window.show() + + def findPresets(self): + parseList = [] + for dirpath, dirnames, filenames in os.walk(self.presetDir): + # anything without a subdirectory must be a preset folder + if dirnames: + continue + for preset in filenames: + compName = os.path.basename(os.path.dirname(dirpath)) + if compName not in self.core.compNames: + continue + compVers = os.path.basename(dirpath) + try: + parseList.append((compName, int(compVers), preset)) + except ValueError: + continue + self.presets = { + compName: [ + (vers, preset) + for name, vers, preset in parseList + if name == compName + ] + for compName, _, __ in parseList + } + + def drawPresetList(self, compFilter=None, presetFilter=''): + self.window.listWidget_presets.clear() + if compFilter: + self.lastFilter = str(compFilter) + else: + compFilter = str(self.lastFilter) + self.presetRows = [] + presetNames = [] + for component, presets in self.presets.items(): + if compFilter != '*' and component != compFilter: + continue + for vers, preset in presets: + if not presetFilter or presetFilter in preset: + self.window.listWidget_presets.addItem( + '%s: %s' % (component, preset) + ) + self.presetRows.append((component, vers, preset)) + if preset not in presetNames: + presetNames.append(preset) + self.autocomplete.setStringList(presetNames) + + def drawFilterList(self): + self.window.comboBox_filter.clear() + self.window.comboBox_filter.addItem('*') + for component in self.presets: + self.window.comboBox_filter.addItem(component) + + def clearPreset(self, compI=None): + '''Functions on mainwindow level from the context menu''' + compI = self.parent.window.listWidget_componentList.currentRow() + self.core.clearPreset(compI) + self.parent.updateComponentTitle(compI, False) + + def openSavePresetDialog(self): + '''Functions on mainwindow level from the context menu''' + window = self.parent.window + selectedComponents = self.core.selectedComponents + componentList = self.parent.window.listWidget_componentList + + if componentList.currentRow() == -1: + return + while True: + index = componentList.currentRow() + currentPreset = selectedComponents[index].currentPreset + newName, OK = QtWidgets.QInputDialog.getText( + self.parent.window, + 'Audio Visualizer', + 'New Preset Name:', + QtWidgets.QLineEdit.Normal, + currentPreset + ) + if OK: + if badName(newName): + self.warnMessage(self.parent.window) + continue + if newName: + if index != -1: + selectedComponents[index].currentPreset = newName + saveValueStore = \ + selectedComponents[index].savePreset() + saveValueStore['preset'] = newName + componentName = str(selectedComponents[index]).strip() + vers = selectedComponents[index].version + self.createNewPreset( + componentName, vers, newName, + saveValueStore, window=self.parent.window) + self.findPresets() + self.drawPresetList() + self.openPreset(newName, index) + break + + def createNewPreset( + self, compName, vers, filename, saveValueStore, **kwargs): + path = os.path.join(self.presetDir, compName, str(vers), filename) + if self.presetExists(path, **kwargs): + return + self.core.createPresetFile(compName, vers, filename, saveValueStore) + + def presetExists(self, path, **kwargs): + if os.path.exists(path): + window = self.window \ + if 'window' not in kwargs else kwargs['window'] + ch = self.parent.showMessage( + msg="%s already exists! Overwrite it?" % + os.path.basename(path), + showCancel=True, + icon='Warning', + parent=window) + if not ch: + # user clicked cancel + return True + + return False + + def openPreset(self, presetName, compPos=None): + componentList = self.parent.window.listWidget_componentList + selectedComponents = self.core.selectedComponents + + index = compPos if compPos is not None else componentList.currentRow() + if index == -1: + return + componentName = str(selectedComponents[index]).strip() + version = selectedComponents[index].version + dirname = os.path.join(self.presetDir, componentName, str(version)) + filepath = os.path.join(dirname, presetName) + self.core.openPreset(filepath, index, presetName) + + self.parent.updateComponentTitle(index) + self.parent.drawPreview() + + def openDeletePresetDialog(self): + row = self.getPresetRow() + if row == -1: + return + comp, vers, name = self.presetRows[row] + ch = self.parent.showMessage( + msg='Really delete %s?' % name, + showCancel=True, + icon='Warning', + parent=self.window + ) + if not ch: + return + 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): + filepath = os.path.join(self.presetDir, comp, str(vers), name) + os.remove(filepath) + + def warnMessage(self, window=None): + self.parent.showMessage( + msg='Preset names must contain only letters, ' + 'numbers, and spaces.', + parent=window if window else self.window) + + def getPresetRow(self): + row = self.window.listWidget_presets.currentRow() + if row > -1: + return row + + # check if component selected in MainWindow has preset loaded + componentList = self.parent.window.listWidget_componentList + compIndex = componentList.currentRow() + if compIndex == -1: + return compIndex + + preset = self.core.selectedComponents[compIndex].currentPreset + if preset is None: + return -1 + else: + rowTuple = ( + self.core.selectedComponents[compIndex].name, + self.core.selectedComponents[compIndex].version, + preset + ) + for i, tup in enumerate(self.presetRows): + if rowTuple == tup: + index = i + break + else: + return -1 + return index + + def openRenamePresetDialog(self): + # TODO: maintain consistency by changing this to call createNewPreset() + presetList = self.window.listWidget_presets + index = self.getPresetRow() + if index == -1: + return + + while True: + newName, OK = QtWidgets.QInputDialog.getText( + self.window, + 'Preset Manager', + 'Rename Preset:', + QtWidgets.QLineEdit.Normal, + self.presetRows[index][2] + ) + if OK: + if badName(newName): + self.warnMessage() + continue + if newName: + comp, vers, oldName = self.presetRows[index] + path = os.path.join( + self.presetDir, comp, str(vers)) + newPath = os.path.join(path, newName) + oldPath = os.path.join(path, oldName) + if self.presetExists(newPath): + return + if os.path.exists(newPath): + os.remove(newPath) + 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 + + def openImportDialog(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self.window, "Import Preset File", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + # get installed path & ask user to overwrite if needed + path = '' + while True: + if path: + if self.presetExists(path): + break + else: + if os.path.exists(path): + os.remove(path) + success, path = self.core.importPreset(filename) + if success: + break + + self.findPresets() + self.drawPresetList() + self.settings.setValue("presetDir", os.path.dirname(filename)) + + def openExportDialog(self): + index = self.getPresetRow() + if index == -1: + return + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + self.window, "Export Preset", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + comp, vers, name = self.presetRows[index] + if not self.core.exportPreset(filename, comp, vers, name): + self.parent.showMessage( + msg='Couldn\'t export %s.' % filename, + parent=self.window + ) + self.settings.setValue("presetDir", os.path.dirname(filename)) + + def clearPresetListSelection(self): + self.window.listWidget_presets.setCurrentRow(-1) + + +def getPresetDir(comp): + '''Get the preset subdir for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) -- cgit v1.2.3 From 733c005eeaf5d3ff15e0f60d320f5c03472bad60 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 18:41:45 -0400 Subject: undoable removeComponent action --- src/command.py | 1 + src/component.py | 3 +-- src/core.py | 36 ++++++++++++++++++++++++------------ src/gui/actions.py | 37 +++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.py | 28 +++++++++++++++------------- src/gui/presetmanager.py | 7 +------ src/main.py | 4 ++-- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/gui/actions.py (limited to 'src/gui/presetmanager.py') diff --git a/src/command.py b/src/command.py index 18f7408..4116c5a 100644 --- a/src/command.py +++ b/src/command.py @@ -19,6 +19,7 @@ class Command(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) self.core = Core() + Core.mode = 'commandline' self.dataDir = self.core.dataDir self.canceled = False diff --git a/src/component.py b/src/component.py index cf3085c..0e5144c 100644 --- a/src/component.py +++ b/src/component.py @@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)): '''Intercepts the command() method to check for global args''' def commandWrapper(self, arg): if arg.startswith('preset='): - from presetmanager import getPresetDir _, 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): print('Couldn\'t locate preset "%s"' % preset) quit(1) diff --git a/src/core.py b/src/core.py index 4dfb210..20b9c1d 100644 --- a/src/core.py +++ b/src/core.py @@ -64,31 +64,39 @@ class Core: for i, component in enumerate(self.selectedComponents): component.compPos = i - def insertComponent(self, compPos, moduleIndex, loader): + def insertComponent(self, compPos, component, loader): ''' 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): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None - log.debug('Inserting Component from module #%s' % moduleIndex) - component = self.modules[moduleIndex].Component( - moduleIndex, compPos, self + if type(component) is int: + # create component using module index in self.modules + moduleIndex = int(component) + log.debug('Creating new component from module #%s' % moduleIndex) + component = self.modules[moduleIndex].Component( + moduleIndex, compPos, self + ) + # init component's widget for loading/saving presets + component.widget(loader) + else: + moduleIndex = -1 + log.debug( + 'Inserting previously-created %s component' % component.name) + + component._error.connect( + loader.videoThreadError ) self.selectedComponents.insert( compPos, 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 moduleIndex > -1: + self.updateComponent(compPos) if hasattr(loader, 'insertComponent'): loader.insertComponent(compPos) @@ -156,6 +164,10 @@ class Core: break return saveValueStore + def getPresetDir(self, comp): + '''Get the preset subdir for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) + def openProject(self, loader, filepath): ''' loader is the object calling this method which must have its own showMessage(**kwargs) method for displaying errors. diff --git a/src/gui/actions.py b/src/gui/actions.py new file mode 100644 index 0000000..5cf64e1 --- /dev/null +++ b/src/gui/actions.py @@ -0,0 +1,37 @@ +''' + QCommand classes for every undoable user action performed in the MainWindow +''' +from PyQt5.QtWidgets import QUndoCommand + + +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): + stackedWidget = self.parent.window.stackedWidget + componentList = self.parent.window.listWidget_componentList + for index in self.selectedRows: + stackedWidget.removeWidget(self.parent.pages[index]) + componentList.takeItem(index) + self.parent.core.removeComponent(index) + self.parent.pages.pop(index) + self.parent.changeComponentWidget() + self.parent.drawPreview() + + 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() + diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index af6e190..2edb750 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,9 +16,10 @@ import time import logging from core import Core -import preview_thread -from preview_win import PreviewWindow -from presetmanager import PresetManager +import gui.preview_thread as preview_thread +from gui.preview_win import PreviewWindow +from gui.presetmanager import PresetManager +from gui.actions import * from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput @@ -43,9 +44,12 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMainWindow.__init__(self) self.window = window self.core = Core() + Core.mode = 'GUI' log.debug( 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) + self.undoStack = QtWidgets.QUndoStack(self) + # widgets of component settings self.pages = [] self.lastAutosave = time.time() @@ -62,7 +66,7 @@ class MainWindow(QtWidgets.QMainWindow): self.presetManager = PresetManager( 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 log.debug('Creating preview window') @@ -298,6 +302,9 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) 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 for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): @@ -685,15 +692,10 @@ class MainWindow(QtWidgets.QMainWindow): def removeComponent(self): componentList = self.window.listWidget_componentList - - for selected in componentList.selectedItems(): - index = componentList.row(selected) - self.window.stackedWidget.removeWidget(self.pages[index]) - componentList.takeItem(index) - self.core.removeComponent(index) - self.pages.pop(index) - self.changeComponentWidget() - self.drawPreview() + selected = componentList.selectedItems() + if selected: + action = RemoveComponent(self, selected) + self.undoStack.push(action) @disableWhenEncoding def moveComponent(self, change): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index b1eeb34..1cc0887 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -302,7 +302,7 @@ class PresetManager(QtWidgets.QDialog): self.findPresets() self.drawPresetList() for i, comp in enumerate(self.core.selectedComponents): - if getPresetDir(comp) == path \ + if self.core.getPresetDir(comp) == path \ and comp.currentPreset == oldName: self.core.openPreset(newPath, i, newName) self.parent.updateComponentTitle(i, False) @@ -351,8 +351,3 @@ class PresetManager(QtWidgets.QDialog): def clearPresetListSelection(self): self.window.listWidget_presets.setCurrentRow(-1) - - -def getPresetDir(comp): - '''Get the preset subdir for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) diff --git a/src/main.py b/src/main.py index 3a6fbe7..c1278da 100644 --- a/src/main.py +++ b/src/main.py @@ -35,11 +35,11 @@ def main(): log.debug("Finished creating command object") 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() desc = QtWidgets.QDesktopWidget() dpi = desc.physicalDpiX() -- cgit v1.2.3 From c06ca5cdcb603f1855cb0122fc2ab6d2473f3c24 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 17 Aug 2017 10:42:15 -0400 Subject: undoable add-comp & clear-preset actions --- src/component.py | 35 +++++++++++++++++++++++++++++++---- src/gui/actions.py | 45 ++++++++++++++++++++++++++++++++++++--------- src/gui/mainwindow.py | 31 ++++++++++++++++++++++++------- src/gui/presetmanager.py | 5 +++-- 4 files changed, 94 insertions(+), 22 deletions(-) (limited to 'src/gui/presetmanager.py') diff --git a/src/component.py b/src/component.py index b883627..f0a8c6b 100644 --- a/src/component.py +++ b/src/component.py @@ -99,6 +99,23 @@ class ComponentMetaclass(type(QtCore.QObject)): return func(self) return errorWrapper + def presetWrapper(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): + return func(self, *args) + return presetWrapper + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -111,7 +128,7 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', + 'frameRender', 'command', 'loadPreset' ) # Auto-decorate methods @@ -140,6 +157,9 @@ class ComponentMetaclass(type(QtCore.QObject)): if key == 'error': attrs[key] = cls.errorWrapper(attrs[key]) + if key == 'loadPreset': + attrs[key] = cls.presetWrapper(attrs[key]) + # Turn version string into a number try: if 'version' not in attrs: @@ -180,6 +200,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.compPos = compPos self.core = core self.currentPreset = None + self.openingPreset = False self._trackedWidgets = {} self._presetNames = {} @@ -207,7 +228,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): preset = self.savePreset() except Exception as e: preset = '%s occurred while saving preset' % str(e) - return '%s\n%s\n%s' % ( + + return 'Component(%s, %s, Core)\n' \ + 'Name: %s v%s\n Preset: %s' % ( + self.moduleIndex, self.compPos, self.__class__.name, str(self.__class__.version), preset ) @@ -308,6 +332,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): A component update triggered by the user changing a widget value Call super() at the END when subclassing this. ''' + if self.openingPreset or not hasattr(self.parent, 'undoStack'): + return self._update() + oldWidgetVals = { attr: getattr(self, attr) for attr in self._trackedWidgets @@ -328,7 +355,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.parent.undoStack.push(action) def _update(self): - '''An internal component update that is not undoable''' + '''A component update that is not undoable''' newWidgetVals = { attr: getWidgetValue(widget) @@ -684,7 +711,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.id_ = -1 if len(self.modifiedVals) == 1: attr, val = self.modifiedVals.popitem() - self.id_ = sum([ord(letter) for letter in attr[:14]]) + self.id_ = sum([ord(letter) for letter in attr[-14:]]) self.modifiedVals[attr] = val else: log.warning( diff --git a/src/gui/actions.py b/src/gui/actions.py index 5a0869d..cdd3dfa 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -4,6 +4,23 @@ from PyQt5.QtWidgets import QUndoCommand +class AddComponent(QUndoCommand): + def __init__(self, parent, compI, moduleI): + super().__init__( + "New %s component" % + parent.core.modules[moduleI].Component.name + ) + self.parent = parent + self.moduleI = moduleI + self.compI = compI + + def redo(self): + self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) + + def undo(self): + self.parent._removeComponent(self.compI) + + class RemoveComponent(QUndoCommand): def __init__(self, parent, selectedRows): super().__init__('Remove component') @@ -17,15 +34,7 @@ class RemoveComponent(QUndoCommand): ] def redo(self): - stackedWidget = self.parent.window.stackedWidget - componentList = self.parent.window.listWidget_componentList - for index in self.selectedRows: - stackedWidget.removeWidget(self.parent.pages[index]) - componentList.takeItem(index) - self.parent.core.removeComponent(index) - self.parent.pages.pop(index) - self.parent.changeComponentWidget() - self.parent.drawPreview() + self.parent._removeComponent(self.selectedRows[0]) def undo(self): componentList = self.parent.window.listWidget_componentList @@ -74,3 +83,21 @@ class MoveComponent(QUndoCommand): def undo(self): self.do(self.newRow, self.row) + + +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) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 26464a9..8000b3b 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -20,7 +20,9 @@ import gui.preview_thread as preview_thread from gui.preview_win import PreviewWindow from gui.presetmanager import PresetManager from gui.actions import * -from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput +from toolkit import ( + disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals +) log = logging.getLogger('AVP.MainWindow') @@ -165,7 +167,7 @@ class MainWindow(QtWidgets.QMainWindow): for i, comp in enumerate(self.core.modules): action = self.compMenu.addAction(comp.Component.name) 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) @@ -686,7 +688,13 @@ class MainWindow(QtWidgets.QMainWindow): 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): + '''Triggered by Core to finish initializing a new component.''' componentList = self.window.listWidget_componentList stackedWidget = self.window.stackedWidget @@ -712,6 +720,16 @@ class MainWindow(QtWidgets.QMainWindow): action = RemoveComponent(self, selected) self.undoStack.push(action) + def _removeComponent(self, index): + stackedWidget = self.window.stackedWidget + componentList = self.window.listWidget_componentList + stackedWidget.removeWidget(self.pages[index]) + componentList.takeItem(index) + self.core.removeComponent(index) + self.pages.pop(index) + self.changeComponentWidget() + self.drawPreview() + @disableWhenEncoding def moveComponent(self, change): '''Moves a component relatively from its current position''' @@ -786,9 +804,8 @@ class MainWindow(QtWidgets.QMainWindow): self.window.lineEdit_audioFile, self.window.lineEdit_outputFile ): - field.blockSignals(True) - field.setText('') - field.blockSignals(False) + with blockSignals(field): + field.setText('') self.progressBarUpdated(0) self.progressBarSetText('') self.undoStack.clear() @@ -938,8 +955,8 @@ class MainWindow(QtWidgets.QMainWindow): for i, comp in enumerate(self.core.modules): menuItem = self.submenu.addAction(comp.Component.name) menuItem.triggered.connect( - lambda _, item=i: self.core.insertComponent( - 0 if insertCompAtTop else index, item, self + lambda _, item=i: self.addComponent( + 0 if insertCompAtTop else index, item ) ) diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 1cc0887..79ec539 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -8,6 +8,7 @@ import os from toolkit import badName from core import Core +from gui.actions import * class PresetManager(QtWidgets.QDialog): @@ -130,8 +131,8 @@ class PresetManager(QtWidgets.QDialog): def clearPreset(self, compI=None): '''Functions on mainwindow level from the context menu''' compI = self.parent.window.listWidget_componentList.currentRow() - self.core.clearPreset(compI) - self.parent.updateComponentTitle(compI, False) + action = ClearPreset(self.parent, compI) + self.parent.undoStack.push(action) def openSavePresetDialog(self): '''Functions on mainwindow level from the context menu''' -- cgit v1.2.3 From 87e762a8aa3fa97a3d43a18c59098b287bb95506 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 17 Aug 2017 20:12:46 -0400 Subject: undoable preset open, rename, and delete' --- src/core.py | 4 +-- src/gui/actions.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ src/gui/presetmanager.py | 49 ++++++++++++++++-------------- 3 files changed, 107 insertions(+), 25 deletions(-) (limited to 'src/gui/presetmanager.py') diff --git a/src/core.py b/src/core.py index 7609698..d9499f7 100644 --- a/src/core.py +++ b/src/core.py @@ -83,7 +83,7 @@ class Core: ) # init component's widget for loading/saving presets component.widget(loader) - # use autoUpdate() method before update() this 1 time to set attrs + # use autoUpdate() method before update() this 1 time to set attrs component._autoUpdate() else: moduleIndex = -1 @@ -169,7 +169,7 @@ class Core: def getPresetDir(self, comp): '''Get the preset subdir for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) + return os.path.join(Core.presetDir, comp.name, str(comp.version)) def openProject(self, loader, filepath): ''' loader is the object calling this method which must have diff --git a/src/gui/actions.py b/src/gui/actions.py index cdd3dfa..0fe97f2 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -2,7 +2,14 @@ QCommand classes for every undoable user action performed in the MainWindow ''' from PyQt5.QtWidgets import QUndoCommand +import os +from core import Core + + +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ +# COMPONENT ACTIONS +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ class AddComponent(QUndoCommand): def __init__(self, parent, compI, moduleI): @@ -85,6 +92,10 @@ class MoveComponent(QUndoCommand): self.do(self.newRow, self.row) +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ +# PRESET ACTIONS +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + class ClearPreset(QUndoCommand): def __init__(self, parent, compI): super().__init__("Clear preset") @@ -101,3 +112,71 @@ class ClearPreset(QUndoCommand): 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'] = str(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() diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 79ec539..dce5333 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -197,11 +197,15 @@ class PresetManager(QtWidgets.QDialog): def openPreset(self, presetName, compPos=None): componentList = self.parent.window.listWidget_componentList - selectedComponents = self.core.selectedComponents - index = compPos if compPos is not None else componentList.currentRow() if index == -1: return + action = OpenPreset(self, presetName, index) + self.parent.undoStack.push(action) + + def _openPreset(self, presetName, index): + selectedComponents = self.core.selectedComponents + componentName = str(selectedComponents[index]).strip() version = selectedComponents[index].version dirname = os.path.join(self.presetDir, componentName, str(version)) @@ -225,16 +229,10 @@ class PresetManager(QtWidgets.QDialog): if not ch: return 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): - filepath = os.path.join(self.presetDir, comp, str(vers), name) - os.remove(filepath) + action = DeletePreset(self, comp, vers, name) + self.parent.undoStack.push(action) def warnMessage(self, window=None): self.parent.showMessage( @@ -271,7 +269,6 @@ class PresetManager(QtWidgets.QDialog): return index def openRenamePresetDialog(self): - # TODO: maintain consistency by changing this to call createNewPreset() presetList = self.window.listWidget_presets index = self.getPresetRow() if index == -1: @@ -294,22 +291,28 @@ class PresetManager(QtWidgets.QDialog): path = os.path.join( self.presetDir, comp, str(vers)) newPath = os.path.join(path, newName) - oldPath = os.path.join(path, oldName) if self.presetExists(newPath): return - if os.path.exists(newPath): - os.remove(newPath) - os.rename(oldPath, newPath) - self.findPresets() - self.drawPresetList() - 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() + action = RenamePreset(self, path, oldName, newName) + self.parent.undoStack.push(action) 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): filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Import Preset File", -- cgit v1.2.3 From c07f2426ceeada205fdacbfba66329179a74a1dc Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 19 Aug 2017 18:32:12 -0400 Subject: fixed issues with undoing relative widgets --- src/component.py | 198 +++++++++++++++++++++++++++++++++------------ src/components/color.py | 2 - src/components/image.py | 2 - src/components/life.py | 1 - src/components/sound.py | 1 - src/components/spectrum.py | 4 +- src/components/text.py | 1 - src/components/video.py | 2 - src/components/waveform.py | 2 +- src/core.py | 11 +-- src/gui/actions.py | 11 ++- src/gui/mainwindow.py | 4 +- src/gui/presetmanager.py | 4 + src/gui/preview_thread.py | 2 +- src/gui/preview_win.py | 2 +- src/main.py | 2 +- src/toolkit/common.py | 47 +++++++++-- 17 files changed, 215 insertions(+), 81 deletions(-) (limited to 'src/gui/presetmanager.py') diff --git a/src/component.py b/src/component.py index 1fe9237..ba86422 100644 --- a/src/component.py +++ b/src/component.py @@ -9,6 +9,7 @@ import sys import math import time import logging +from copy import copy from toolkit.frame import BlankFrame from toolkit import ( @@ -113,14 +114,20 @@ class ComponentMetaclass(type(QtCore.QObject)): def presetWrapper(self, *args): with openingPreset(self): - return func(self, *args) + try: + return func(self, *args) + except Exception: + try: + raise ComponentError(self, 'preset loader') + except ComponentError: + return return presetWrapper def updateWrapper(func): ''' - For undoable updates triggered by the user, - call _userUpdate() after the subclass's update() method. - For non-user updates, call _autoUpdate() + 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): @@ -128,24 +135,57 @@ class ComponentMetaclass(type(QtCore.QObject)): self.auto = auto def __enter__(self): - pass + 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 = False - if 'auto' in kwargs: - auto = kwargs['auto'] - + auto = kwargs['auto'] if 'auto' in kwargs else False with wrap(self, auto): - return func(self) + 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): if 'ui' not in attrs: # Use module name as ui filename by default @@ -153,13 +193,12 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs['__module__'].split('.')[-1] )[0] - # if parents[0] == QtCore.QObject: else: decorate = ( 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', 'frameRender', 'command', - 'loadPreset', 'update' + 'loadPreset', 'update', 'widget', ) # Auto-decorate methods @@ -184,6 +223,8 @@ class ComponentMetaclass(type(QtCore.QObject)): 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 try: @@ -224,23 +265,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.moduleIndex = moduleIndex self.compPos = compPos self.core = core - self.currentPreset = None - self.openingPreset = False + # STATUS VARIABLES + self.currentPreset = None + self._allWidgets = {} self._trackedWidgets = {} self._presetNames = {} self._commandArgs = {} self._colorWidgets = {} self._colorFuncs = {} self._relativeWidgets = {} - # pixel values stored as floats + # Pixel values stored as floats self._relativeValues = {} - # maximum values of spinBoxes at 1080p (Core.resolutions[0]) + # Maximum values of spinBoxes at 1080p (Core.resolutions[0]) self._relativeMaximums = {} + # LOCKING VARIABLES + self.openingPreset = False self._lockedProperties = None self._lockedError = 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 self.canceled = False @@ -338,21 +384,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings + log.verbose('Creating UI for %s #%s\'s widget' % ( + self.name, self.compPos + )) self.page = self.loadUi(self.__class__.ui) - # Connect widget signals - widgets = { + # Find all normal widgets which will be connected after subclass method + self._allWidgets = { 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), 'comboBox': self.page.findChildren(QtWidgets.QComboBox), } - widgets['spinBox'].extend( + self._allWidgets['spinBox'].extend( self.page.findChildren(QtWidgets.QDoubleSpinBox) ) - for widgetList in widgets.values(): - for widget in widgetList: - connectWidget(widget, self.update) def update(self): ''' @@ -427,10 +473,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # "Private" Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + def _preUpdate(self): + '''Happens before subclass update()''' + for attr in self._relativeWidgets: + self.updateRelativeWidget(attr) + def _userUpdate(self): - '''An undoable component update triggered by the user''' + '''Happens after subclass update() for an undoable update by user.''' oldWidgetVals = { - attr: getattr(self, attr) + attr: copy(getattr(self, attr)) for attr in self._trackedWidgets } newWidgetVals = { @@ -443,13 +494,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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): - '''An internal component update that is not undoable''' + '''Happens after subclass update() for an internal component update.''' newWidgetVals = { attr: getWidgetValue(widget) for attr, widget in self._trackedWidgets.items() @@ -459,12 +509,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setAttrs(self, attrDict): ''' - Sets attrs (linked to trackedWidgets) in this preset to + 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: text stored as tuple & update the button color + # Color Widgets must have a tuple & have a button to update if type(val) is tuple: rgbTuple = val else: @@ -475,15 +525,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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, val) - else: # Normal tracked widget setattr(self, 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() @@ -499,6 +559,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): Optional args: 'presetNames': preset variable names to replace 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. ''' @@ -542,6 +604,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._relativeMaximums[attr] = \ self._trackedWidgets[attr].maximum() self.updateRelativeWidgetMaximum(attr) + self._preUpdate() + self._autoUpdate() def pickColor(self, textWidget, button): '''Use color picker to get color input from the user.''' @@ -627,12 +691,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setRelativeWidget(self, attr, floatVal): '''Set a relative widget using a float''' pixelVal = self.pixelValForAttr(attr, floatVal) - self._trackedWidgets[attr].setValue(pixelVal) + with blockSignals(self._allWidgets): + 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: + log.verbose('Using nonstandard oldAttr for %s' % attr) + return self.oldAttrs[attr] + else: + return getattr(self, attr) def updateRelativeWidget(self, attr): + '''Called by _preUpdate() for each relativeWidget before each update''' try: - oldUserValue = getattr(self, attr) - except AttributeError: + oldUserValue = self.getOldAttr(attr) + except (AttributeError, KeyError): + log.info('Using visible values as basis for relative widgets') oldUserValue = self._trackedWidgets[attr].value() newUserValue = self._trackedWidgets[attr].value() newRelativeVal = self.floatValForAttr(attr, newUserValue) @@ -645,11 +725,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # means the pixel value needs to be updated log.debug('Updating %s #%s\'s relative widget: %s' % ( self.name, self.compPos, attr)) - self._trackedWidgets[attr].blockSignals(True) - self.updateRelativeWidgetMaximum(attr) - pixelVal = self.pixelValForAttr(attr, oldRelativeVal) - self._trackedWidgets[attr].setValue(pixelVal) - self._trackedWidgets[attr].blockSignals(False) + with blockSignals(self._trackedWidgets[attr]): + self.updateRelativeWidgetMaximum(attr) + pixelVal = self.pixelValForAttr(attr, oldRelativeVal) + self._trackedWidgets[attr].setValue(pixelVal) if attr not in self._relativeValues \ or oldUserValue != newUserValue: @@ -725,14 +804,22 @@ class ComponentUpdate(QtWidgets.QUndoCommand): parent.name, parent.compPos ) ) + self.undone = False self.parent = parent self.oldWidgetVals = { - attr: val + attr: copy(val) for attr, val in oldWidgetVals.items() if attr in modifiedVals } self.modifiedVals = modifiedVals + # Because relative widgets change themselves every update based on + # their previous value, we must store ALL their values in case of undo + self.redoRelativeWidgetVals = { + 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: @@ -755,15 +842,26 @@ class ComponentUpdate(QtWidgets.QUndoCommand): return True def redo(self): + if self.undone: + log.debug('Redoing component update') + self.parent.setWidgetValues(self.modifiedVals) self.parent.setAttrs(self.modifiedVals) - self.parent._sendUpdateSignal() + if self.undone: + self.parent.oldAttrs = self.redoRelativeWidgetVals + self.parent.update(auto=True) + self.parent.oldAttrs = None + else: + self.undoRelativeWidgetVals = { + attr: copy(getattr(self.parent, attr)) + for attr in self.parent._relativeWidgets + } + self.parent._sendUpdateSignal() def undo(self): + log.debug('Undoing component update') + self.undone = True + self.parent.oldAttrs = self.undoRelativeWidgetVals + self.parent.setWidgetValues(self.oldWidgetVals) self.parent.setAttrs(self.oldWidgetVals) - with blockSignals(self.parent): - for attr, val in self.oldWidgetVals.items(): - widget = self.parent._trackedWidgets[attr] - if attr in self.parent._colorWidgets: - val = '%s,%s,%s' % val - setWidgetValue(widget, val) - self.parent._sendUpdateSignal() + self.parent.update(auto=True) + self.parent.oldAttrs = None diff --git a/src/components/color.py b/src/components/color.py index d09cee8..a55aa10 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -82,8 +82,6 @@ class Component(Component): self.page.pushButton_color2.setEnabled(False) self.page.fillWidget.setCurrentIndex(fillType) - super().update() - def previewRender(self): return self.drawFrame(self.width, self.height) diff --git a/src/components/image.py b/src/components/image.py index 63bee1a..c57b69c 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -84,7 +84,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def command(self, arg): if '=' in arg: @@ -123,4 +122,3 @@ class Component(Component): else: scaleBox.setVisible(True) stretchScaleBox.setVisible(False) - super().update() diff --git a/src/components/life.py b/src/components/life.py index 2383d30..76d2c5f 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def shiftGrid(self, d): def newGrid(Xchange, Ychange): diff --git a/src/components/sound.py b/src/components/sound.py index 26ecf93..b86f40c 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) - self.update() def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 89130a2..2b98dc2 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -76,8 +76,6 @@ class Component(Component): else: self.page.checkBox_mono.setEnabled(True) - super().update() - def previewRender(self): changedSize = self.updateChunksize() if not changedSize \ @@ -138,7 +136,7 @@ class Component(Component): '-r', self.settings.value("outputFrameRate"), '-ss', "{0:.3f}".format(startPt), '-i', - os.path.join(self.core.wd, 'background.png') + self.core.junkStream if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', diff --git a/src/components/text.py b/src/components/text.py index d3afd5c..92f0599 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -68,7 +68,6 @@ class Component(Component): self.page.spinBox_shadY.setHidden(True) self.page.label_shadBlur.setHidden(True) self.page.spinBox_shadBlur.setHidden(True) - super().update() def centerXY(self): self.setRelativeWidget('xPosition', 0.5) diff --git a/src/components/video.py b/src/components/video.py index a189f60..9c0d608 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -52,7 +52,6 @@ class Component(Component): else: self.page.label_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False) - super().update() def previewRender(self): self.updateChunksize() @@ -119,7 +118,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) - self.update() def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): diff --git a/src/components/waveform.py b/src/components/waveform.py index 0743e55..5c02bbf 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -98,7 +98,7 @@ class Component(Component): '-r', self.settings.value("outputFrameRate"), '-ss', "{0:.3f}".format(startPt), '-i', - os.path.join(self.core.wd, 'background.png') + self.core.junkStream if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', diff --git a/src/core.py b/src/core.py index d9499f7..169716c 100644 --- a/src/core.py +++ b/src/core.py @@ -13,7 +13,7 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.WARNING +STDOUT_LOGLVL = logging.VERBOSE FILE_LOGLVL = logging.DEBUG @@ -81,10 +81,7 @@ class Core: component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) - # init component's widget for loading/saving presets component.widget(loader) - # use autoUpdate() method before update() this 1 time to set attrs - component._autoUpdate() else: moduleIndex = -1 log.debug( @@ -186,9 +183,8 @@ class Core: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: widget = eval('loader.window.%s' % widget) - widget.blockSignals(True) - toolkit.setWidgetValue(widget, value) - widget.blockSignals(False) + with toolkit.blockSignals(widget): + toolkit.setWidgetValue(widget, value) for key, value in data['Settings']: Core.settings.setValue(key, value) @@ -474,6 +470,7 @@ class Core: 'logDir': os.path.join(dataDir, 'log'), 'presetDir': os.path.join(dataDir, 'presets'), 'componentsPath': os.path.join(wd, 'components'), + 'junkStream': os.path.join(wd, 'gui', 'background.png'), 'encoderOptions': encoderOptions, 'resolutions': [ '1920x1080', diff --git a/src/gui/actions.py b/src/gui/actions.py index 0fe97f2..1444569 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -20,11 +20,20 @@ class AddComponent(QUndoCommand): self.parent = parent self.moduleI = moduleI self.compI = compI + self.comp = None def redo(self): - self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) + 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) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 8000b3b..76c53af 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -25,7 +25,7 @@ from toolkit import ( ) -log = logging.getLogger('AVP.MainWindow') +log = logging.getLogger('AVP.Gui.MainWindow') class MainWindow(QtWidgets.QMainWindow): @@ -76,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow): # Create the preview window and its thread, queues, and timers log.debug('Creating preview window') self.previewWindow = PreviewWindow(self, os.path.join( - Core.wd, "background.png")) + Core.wd, 'gui', "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) log.debug('Starting preview thread') diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index dce5333..befa7cd 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -5,12 +5,16 @@ from PyQt5 import QtCore, QtWidgets import string import os +import logging from toolkit import badName from core import Core from gui.actions import * +log = logging.getLogger('AVP.Gui.PresetManager') + + class PresetManager(QtWidgets.QDialog): def __init__(self, window, parent): super().__init__(parent.window) diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index 9615884..33a9e7a 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -14,7 +14,7 @@ from toolkit.frame import Checkerboard from toolkit import disableWhenOpeningProject -log = logging.getLogger("AVP.PreviewThread") +log = logging.getLogger("AVP.Gui.PreviewThread") class Worker(QtCore.QObject): diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 40c19c6..c6b9a32 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -7,7 +7,7 @@ class PreviewWindow(QtWidgets.QLabel): Paints the preview QLabel in MainWindow and maintains the aspect ratio when the window is resized. ''' - log = logging.getLogger('AVP.PreviewWindow') + log = logging.getLogger('AVP.Gui.PreviewWindow') def __init__(self, parent, img): super(PreviewWindow, self).__init__() diff --git a/src/main.py b/src/main.py index c1278da..6d18af3 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,7 @@ import logging from __init__ import wd -log = logging.getLogger('AVP.Entrypoint') +log = logging.getLogger('AVP.Main') def main(): diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 51ad023..74143e8 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,19 +6,53 @@ import string import os import sys import subprocess +import logging +from copy import copy from collections import OrderedDict +log = logging.getLogger('AVP.Toolkit.Common') + + class blockSignals: - '''A context manager to temporarily block a Qt widget from updating''' - def __init__(self, widget): - self.widget = widget + ''' + 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): - self.widget.blockSignals(True) + 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): - self.widget.blockSignals(False) + log.verbose('Resetting blockSignals to %s' % 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): @@ -119,12 +153,14 @@ def connectWidget(widget, func): elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) else: + log.warning('Failed to connect %s ' % str(widget.__class__.__name__)) return False return True def setWidgetValue(widget, val): '''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: widget.setText(val) elif type(widget) == QtWidgets.QSpinBox \ @@ -135,6 +171,7 @@ def setWidgetValue(widget, val): elif type(widget) == QtWidgets.QComboBox: widget.setCurrentIndex(val) else: + log.warning('Failed to set %s ' % str(widget.__class__.__name__)) return False return True -- cgit v1.2.3 From d4b63e4d4612db262424fe10c83f8eaa4f741f24 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 19 Aug 2017 20:45:44 -0400 Subject: remove % from log calls --- src/component.py | 32 +++++++++++++++++--------------- src/core.py | 19 ++++++++++--------- src/gui/actions.py | 3 ++- src/gui/mainwindow.py | 26 +++++++++++++++++++++----- src/gui/presetmanager.py | 2 +- src/toolkit/common.py | 16 ++++++++++------ src/toolkit/ffmpeg.py | 2 +- src/video_thread.py | 7 ++++--- 8 files changed, 66 insertions(+), 41 deletions(-) (limited to 'src/gui/presetmanager.py') diff --git a/src/component.py b/src/component.py index ba86422..992a82e 100644 --- a/src/component.py +++ b/src/component.py @@ -40,11 +40,11 @@ class ComponentMetaclass(type(QtCore.QObject)): def renderWrapper(func): def renderWrapper(self, *args, **kwargs): try: - log.verbose('### %s #%s renders%s frame %s###' % ( + log.verbose('### %s #%s renders%s frame %s###', self.__class__.name, str(self.compPos), '' if args else ' a preview', '' if not args else '%s ' % args[0], - )) + ) return func(self, *args, **kwargs) except Exception as e: try: @@ -170,7 +170,7 @@ class ComponentMetaclass(type(QtCore.QObject)): def __exit__(self, *args): for widgetList in self.comp._allWidgets.values(): for widget in widgetList: - log.verbose('Connecting %s' % str( + log.verbose('Connecting %s', str( widget.__class__.__name__)) connectWidget(widget, self.comp.update) @@ -230,16 +230,18 @@ class ComponentMetaclass(type(QtCore.QObject)): try: if 'version' not in attrs: log.error( - 'No version attribute in %s. Defaulting to 1' % + 'No version attribute in %s. Defaulting to 1', attrs['name']) attrs['version'] = 1 else: attrs['version'] = int(attrs['version'].split('.')[0]) except ValueError: - log.critical('%s component has an invalid version string:\n%s' % ( - attrs['name'], str(attrs['version']))) + log.critical( + '%s component has an invalid version string:\n%s', + attrs['name'], str(attrs['version']) + ) except KeyError: - log.critical('%s component has no version string.' % attrs['name']) + log.critical('%s component has no version string.', attrs['name']) else: return super().__new__(cls, name, parents, attrs) quit(1) @@ -384,9 +386,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings - log.verbose('Creating UI for %s #%s\'s widget' % ( + log.verbose('Creating UI for %s #%s\'s widget', self.name, self.compPos - )) + ) self.page = self.loadUi(self.__class__.ui) # Find all normal widgets which will be connected after subclass method @@ -702,7 +704,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): can make determining the 'previous' value tricky. ''' if self.oldAttrs is not None: - log.verbose('Using nonstandard oldAttr for %s' % attr) + log.verbose('Using nonstandard oldAttr for %s', attr) return self.oldAttrs[attr] else: return getattr(self, attr) @@ -723,8 +725,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): and oldRelativeVal != newRelativeVal: # Float changed without pixel value changing, which # means the pixel value needs to be updated - log.debug('Updating %s #%s\'s relative widget: %s' % ( - self.name, self.compPos, attr)) + log.debug( + 'Updating %s #%s\'s relative widget: %s', + self.name, self.compPos, attr) with blockSignals(self._trackedWidgets[attr]): self.updateRelativeWidgetMaximum(attr) pixelVal = self.pixelValForAttr(attr, oldRelativeVal) @@ -828,9 +831,8 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.modifiedVals[attr] = val else: log.warning( - '%s component settings changed at once. (%s)' % ( - len(self.modifiedVals), repr(self.modifiedVals) - ) + '%s component settings changed at once. (%s)', + len(self.modifiedVals), repr(self.modifiedVals) ) def id(self): diff --git a/src/core.py b/src/core.py index 169716c..bfb8272 100644 --- a/src/core.py +++ b/src/core.py @@ -77,7 +77,8 @@ class Core: if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) - log.debug('Creating new component from module #%s' % moduleIndex) + log.debug( + 'Creating new component from module #%s', str(moduleIndex)) component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) @@ -85,7 +86,7 @@ class Core: else: moduleIndex = -1 log.debug( - 'Inserting previously-created %s component' % component.name) + 'Inserting previously-created %s component', component.name) component._error.connect( loader.videoThreadError @@ -117,8 +118,9 @@ class Core: self.componentListChanged() def updateComponent(self, i): - log.debug('Auto-updating %s #%s' % ( - self.selectedComponents[i], str(i))) + log.debug( + 'Auto-updating %s #%s', + self.selectedComponents[i], str(i)) self.selectedComponents[i].update(auto=True) def moduleIndexFor(self, compName): @@ -146,9 +148,8 @@ class Core: ) except KeyError as e: log.warning( - '%s #%s\'s preset is missing value: %s' % ( - comp.name, str(compIndex), str(e) - ) + '%s #%s\'s preset is missing value: %s', + comp.name, str(compIndex), str(e) ) self.savedPresets[presetName] = dict(saveValueStore) @@ -266,7 +267,7 @@ class Core: Returns dictionary with section names as the keys, each one contains a list of tuples: (compName, version, compPresetDict) ''' - log.debug('Parsing av file: %s' % filepath) + log.debug('Parsing av file: %s', filepath) validSections = ( 'Components', 'Settings', @@ -385,7 +386,7 @@ class Core: def createProjectFile(self, filepath, window=None): '''Create a project file (.avp) using the current program state''' - log.info('Creating %s' % filepath) + log.info('Creating %s', filepath) settingsKeys = [ 'componentDir', 'inputDir', diff --git a/src/gui/actions.py b/src/gui/actions.py index 1444569..f101bd7 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -3,6 +3,7 @@ ''' from PyQt5.QtWidgets import QUndoCommand import os +from copy import copy from core import Core @@ -132,7 +133,7 @@ class OpenPreset(QUndoCommand): comp = self.parent.core.selectedComponents[compI] self.store = comp.savePreset() - self.store['preset'] = str(comp.currentPreset) + self.store['preset'] = copy(comp.currentPreset) def redo(self): self.parent._openPreset(self.presetName, self.compI) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 76c53af..833d2d1 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -387,30 +387,46 @@ class MainWindow(QtWidgets.QMainWindow): @QtCore.pyqtSlot(int, dict) 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: name = presetStore['preset'] if name is None or name not in self.core.savedPresets: modified = False else: modified = (presetStore != self.core.savedPresets[name]) + if modified: + log.verbose( + 'Differing values between presets: %s', + ", ".join([ + '%s: %s' % item for item in presetStore.items() + if val != self.core.savedPresets[name][key] + ]) + ) else: modified = bool(presetStore) if pos < 0: pos = len(self.core.selectedComponents)-1 - name = str(self.core.selectedComponents[pos]) + name = self.core.selectedComponents[pos].name title = str(name) if self.core.selectedComponents[pos].currentPreset: title += ' - %s' % self.core.selectedComponents[pos].currentPreset if modified: title += '*' 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 - )) + ) else: - log.debug('Setting %s #%s\'s title: %s' % ( + log.debug( + 'Setting %s #%s\'s title: %s', name, pos, title - )) + ) self.window.listWidget_componentList.item(pos).setText(title) def updateCodecs(self): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index befa7cd..2445760 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -210,7 +210,7 @@ class PresetManager(QtWidgets.QDialog): def _openPreset(self, presetName, index): selectedComponents = self.core.selectedComponents - componentName = str(selectedComponents[index]).strip() + componentName = selectedComponents[index].name.strip() version = selectedComponents[index].version dirname = os.path.join(self.presetDir, componentName, str(version)) filepath = os.path.join(dirname, presetName) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 74143e8..95aeab3 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -29,15 +29,19 @@ class blockSignals: ) def __enter__(self): - log.verbose('Blocking signals for %s' % ", ".join([ - str(w.__class__.__name__) for w in self.widgets - ])) + 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' % sum(self.oldStates)) + log.verbose( + 'Resetting blockSignals to %s', str(bool(sum(self.oldStates)))) for w, state in zip(self.widgets, self.oldStates): w.blockSignals(state) @@ -153,7 +157,7 @@ def connectWidget(widget, func): elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) else: - log.warning('Failed to connect %s ' % str(widget.__class__.__name__)) + log.warning('Failed to connect %s ', str(widget.__class__.__name__)) return False return True @@ -171,7 +175,7 @@ def setWidgetValue(widget, val): elif type(widget) == QtWidgets.QComboBox: widget.setCurrentIndex(val) else: - log.warning('Failed to set %s ' % str(widget.__class__.__name__)) + log.warning('Failed to set %s ', str(widget.__class__.__name__)) return False return True diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 8fe9148..f007f90 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -93,7 +93,7 @@ class FfmpegVideo: from component import ComponentError logFilename = os.path.join( core.Core.logDir, 'render_%s.log' % str(self.component.compPos)) - log.debug('Creating ffmpeg process (log at %s)' % logFilename) + log.debug('Creating ffmpeg process (log at %s)', logFilename) with open(logFilename, 'w') as logf: logf.write(" ".join(self.command) + '\n\n') with open(logFilename, 'a') as logf: diff --git a/src/video_thread.py b/src/video_thread.py index 87fb9bd..823ac73 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -179,7 +179,7 @@ class Worker(QtCore.QObject): for num, component in enumerate(reversed(self.components)) ]) print('Loaded Components:', initText) - log.info('Calling preFrameRender for %s' % initText) + log.info('Calling preFrameRender for %s', initText) self.staticComponents = {} for compNo, comp in enumerate(reversed(self.components)): try: @@ -221,12 +221,13 @@ class Worker(QtCore.QObject): if self.canceled: if canceledByComponent: - log.error('Export cancelled by component #%s (%s): %s' % ( + log.error( + 'Export cancelled by component #%s (%s): %s', compNo, comp.name, 'No message.' if comp.error() is None else ( comp.error() if type(comp.error()) is str - else comp.error()[0]) + else comp.error()[0] ) ) self.cancelExport() -- cgit v1.2.3