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/__init__.py | 0
src/gui/mainwindow.py | 946 ++++++++++++++++++++++++++++++++++++++++++++++
src/gui/mainwindow.ui | 828 ++++++++++++++++++++++++++++++++++++++++
src/gui/presetmanager.py | 358 ++++++++++++++++++
src/gui/presetmanager.ui | 150 ++++++++
src/gui/preview_thread.py | 90 +++++
src/gui/preview_win.py | 62 +++
src/mainwindow.py | 946 ----------------------------------------------
src/mainwindow.ui | 828 ----------------------------------------
src/presetmanager.py | 358 ------------------
src/presetmanager.ui | 150 --------
src/preview_thread.py | 90 -----
src/preview_win.py | 62 ---
13 files changed, 2434 insertions(+), 2434 deletions(-)
create mode 100644 src/gui/__init__.py
create mode 100644 src/gui/mainwindow.py
create mode 100644 src/gui/mainwindow.ui
create mode 100644 src/gui/presetmanager.py
create mode 100644 src/gui/presetmanager.ui
create mode 100644 src/gui/preview_thread.py
create mode 100644 src/gui/preview_win.py
delete mode 100644 src/mainwindow.py
delete mode 100644 src/mainwindow.ui
delete mode 100644 src/presetmanager.py
delete mode 100644 src/presetmanager.ui
delete mode 100644 src/preview_thread.py
delete mode 100644 src/preview_win.py
(limited to 'src')
diff --git a/src/gui/__init__.py b/src/gui/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
new file mode 100644
index 0000000..af6e190
--- /dev/null
+++ b/src/gui/mainwindow.py
@@ -0,0 +1,946 @@
+'''
+ When using GUI mode, this module's object (the main window) takes
+ user input to construct a program state (stored in the Core object).
+ This shows a preview of the video being created and allows for saving
+ projects and exporting the video at a later time.
+'''
+from PyQt5 import QtCore, QtGui, uic, QtWidgets
+from PyQt5.QtWidgets import QMenu, QShortcut
+from PIL import Image
+from queue import Queue
+import sys
+import os
+import signal
+import filecmp
+import time
+import logging
+
+from core import Core
+import preview_thread
+from preview_win import PreviewWindow
+from presetmanager import PresetManager
+from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
+
+
+log = logging.getLogger('AVP.MainWindow')
+
+
+class MainWindow(QtWidgets.QMainWindow):
+ '''
+ The MainWindow wraps many Core methods in order to update the GUI
+ accordingly. E.g., instead of self.core.openProject(), it will use
+ self.openProject() and update the window titlebar within the wrapper.
+
+ MainWindow manages the autosave feature, although Core has the
+ primary functions for opening and creating project files.
+ '''
+
+ createVideo = QtCore.pyqtSignal()
+ newTask = QtCore.pyqtSignal(list) # for the preview window
+ processTask = QtCore.pyqtSignal()
+
+ def __init__(self, window, project):
+ QtWidgets.QMainWindow.__init__(self)
+ self.window = window
+ self.core = Core()
+ log.debug(
+ 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+
+ # widgets of component settings
+ self.pages = []
+ self.lastAutosave = time.time()
+ # list of previous five autosave times, used to reduce update spam
+ self.autosaveTimes = []
+ self.autosaveCooldown = 0.2
+ self.encoding = False
+
+ # Find settings created by Core object
+ self.dataDir = Core.dataDir
+ self.presetDir = Core.presetDir
+ self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
+ self.settings = Core.settings
+
+ self.presetManager = PresetManager(
+ uic.loadUi(
+ os.path.join(Core.wd, 'presetmanager.ui')), self)
+
+ # 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"))
+ window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
+
+ log.debug('Starting preview thread')
+ self.previewQueue = Queue()
+ self.previewThread = QtCore.QThread(self)
+ self.previewWorker = preview_thread.Worker(self, self.previewQueue)
+ self.previewWorker.error.connect(self.previewWindow.threadError)
+ self.previewWorker.moveToThread(self.previewThread)
+ self.previewWorker.imageCreated.connect(self.showPreviewImage)
+ self.previewThread.start()
+
+ log.debug('Starting preview timer')
+ self.timer = QtCore.QTimer(self)
+ self.timer.timeout.connect(self.processTask.emit)
+ self.timer.start(500)
+
+ # Begin decorating the window and connecting events
+ self.window.installEventFilter(self)
+ componentList = self.window.listWidget_componentList
+
+ if sys.platform == 'darwin':
+ log.debug(
+ 'Darwin detected: showing progress label below progress bar')
+ window.progressBar_createVideo.setTextVisible(False)
+ else:
+ window.progressLabel.setHidden(True)
+
+ window.toolButton_selectAudioFile.clicked.connect(
+ self.openInputFileDialog)
+
+ window.toolButton_selectOutputFile.clicked.connect(
+ self.openOutputFileDialog)
+
+ def changedField():
+ self.autosave()
+ self.updateWindowTitle()
+
+ window.lineEdit_audioFile.textChanged.connect(changedField)
+ window.lineEdit_outputFile.textChanged.connect(changedField)
+
+ window.progressBar_createVideo.setValue(0)
+
+ window.pushButton_createVideo.clicked.connect(
+ self.createAudioVisualisation)
+
+ window.pushButton_Cancel.clicked.connect(self.stopVideo)
+
+ for i, container in enumerate(Core.encoderOptions['containers']):
+ window.comboBox_videoContainer.addItem(container['name'])
+ if container['name'] == self.settings.value('outputContainer'):
+ selectedContainer = i
+
+ window.comboBox_videoContainer.setCurrentIndex(selectedContainer)
+ window.comboBox_videoContainer.currentIndexChanged.connect(
+ self.updateCodecs
+ )
+
+ self.updateCodecs()
+
+ for i in range(window.comboBox_videoCodec.count()):
+ codec = window.comboBox_videoCodec.itemText(i)
+ if codec == self.settings.value('outputVideoCodec'):
+ window.comboBox_videoCodec.setCurrentIndex(i)
+
+ for i in range(window.comboBox_audioCodec.count()):
+ codec = window.comboBox_audioCodec.itemText(i)
+ if codec == self.settings.value('outputAudioCodec'):
+ window.comboBox_audioCodec.setCurrentIndex(i)
+
+ window.comboBox_videoCodec.currentIndexChanged.connect(
+ self.updateCodecSettings
+ )
+
+ window.comboBox_audioCodec.currentIndexChanged.connect(
+ self.updateCodecSettings
+ )
+
+ vBitrate = int(self.settings.value('outputVideoBitrate'))
+ aBitrate = int(self.settings.value('outputAudioBitrate'))
+
+ window.spinBox_vBitrate.setValue(vBitrate)
+ window.spinBox_aBitrate.setValue(aBitrate)
+ window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
+ window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
+
+ # Make component buttons
+ self.compMenu = QMenu()
+ 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)
+ )
+
+ self.window.pushButton_addComponent.setMenu(self.compMenu)
+
+ componentList.dropEvent = self.dragComponent
+ componentList.itemSelectionChanged.connect(
+ self.changeComponentWidget
+ )
+ componentList.itemSelectionChanged.connect(
+ self.presetManager.clearPresetListSelection
+ )
+ self.window.pushButton_removeComponent.clicked.connect(
+ lambda: self.removeComponent()
+ )
+
+ componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ componentList.customContextMenuRequested.connect(
+ self.componentContextMenu
+ )
+
+ currentRes = str(self.settings.value('outputWidth'))+'x' + \
+ str(self.settings.value('outputHeight'))
+ for i, res in enumerate(Core.resolutions):
+ window.comboBox_resolution.addItem(res)
+ if res == currentRes:
+ currentRes = i
+ window.comboBox_resolution.setCurrentIndex(currentRes)
+ window.comboBox_resolution.currentIndexChanged.connect(
+ self.updateResolution
+ )
+
+ self.window.pushButton_listMoveUp.clicked.connect(
+ lambda: self.moveComponent(-1)
+ )
+ self.window.pushButton_listMoveDown.clicked.connect(
+ lambda: self.moveComponent(1)
+ )
+
+ # Configure the Projects Menu
+ self.projectMenu = QMenu()
+ self.window.menuButton_newProject = self.projectMenu.addAction(
+ "New Project"
+ )
+ self.window.menuButton_newProject.triggered.connect(
+ lambda: self.createNewProject()
+ )
+ self.window.menuButton_openProject = self.projectMenu.addAction(
+ "Open Project"
+ )
+ self.window.menuButton_openProject.triggered.connect(
+ lambda: self.openOpenProjectDialog()
+ )
+
+ action = self.projectMenu.addAction("Save Project")
+ action.triggered.connect(self.saveCurrentProject)
+
+ action = self.projectMenu.addAction("Save Project As")
+ action.triggered.connect(self.openSaveProjectDialog)
+
+ self.window.pushButton_projects.setMenu(self.projectMenu)
+
+ # Configure the Presets Button
+ self.window.pushButton_presets.clicked.connect(
+ self.openPresetManager
+ )
+
+ self.updateWindowTitle()
+ log.debug('Showing main window')
+ window.show()
+
+ if project and project != self.autosavePath:
+ if not project.endswith('.avp'):
+ project += '.avp'
+ # open a project from the commandline
+ if not os.path.dirname(project):
+ project = os.path.join(
+ self.settings.value("projectDir"), project
+ )
+ self.currentProject = project
+ self.settings.setValue("currentProject", project)
+ if os.path.exists(self.autosavePath):
+ os.remove(self.autosavePath)
+ else:
+ # open the last currentProject from settings
+ self.currentProject = self.settings.value("currentProject")
+
+ # delete autosave if it's identical to this project
+ if self.autosaveExists(identical=True):
+ os.remove(self.autosavePath)
+
+ if self.currentProject and os.path.exists(self.autosavePath):
+ ch = self.showMessage(
+ msg="Restore unsaved changes in project '%s'?"
+ % os.path.basename(self.currentProject)[:-4],
+ showCancel=True)
+ if ch:
+ self.saveProjectChanges()
+ else:
+ os.remove(self.autosavePath)
+
+ self.openProject(self.currentProject, prompt=False)
+ self.drawPreview(True)
+
+ # verify Pillow version
+ if not self.settings.value("pilMsgShown") \
+ and 'post' not in Image.PILLOW_VERSION:
+ self.showMessage(
+ msg="You are using the standard version of the "
+ "Python imaging library (Pillow %s). Upgrade "
+ "to the Pillow-SIMD fork to enable hardware accelerations "
+ "and export videos faster." % Image.PILLOW_VERSION
+ )
+ self.settings.setValue("pilMsgShown", True)
+
+ # verify Ffmpeg version
+ if not self.settings.value("ffmpegMsgShown"):
+ try:
+ with open(os.devnull, "w") as f:
+ ffmpegVers = checkOutput(
+ ['ffmpeg', '-version'], stderr=f
+ )
+ goodVersion = str(ffmpegVers).split()[2].startswith('3')
+ except Exception:
+ goodVersion = False
+ else:
+ goodVersion = True
+
+ if not goodVersion:
+ self.showMessage(
+ msg="You're using an old version of Ffmpeg. "
+ "Some features may not work as expected."
+ )
+ self.settings.setValue("ffmpegMsgShown", True)
+
+ # Hotkeys for projects
+ QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
+ QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
+ QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
+ QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
+
+ # Hotkeys for component list
+ for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
+ QtWidgets.QShortcut(
+ inskey, self.window,
+ activated=lambda: self.window.pushButton_addComponent.click()
+ )
+ for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
+ QtWidgets.QShortcut(
+ delkey, self.window.listWidget_componentList,
+ self.removeComponent
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Space", self.window,
+ activated=lambda: self.window.listWidget_componentList.setFocus()
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+S", self.window,
+ self.presetManager.openSavePresetDialog
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Shift+C", self.window, self.presetManager.clearPreset
+ )
+
+ QtWidgets.QShortcut(
+ "Ctrl+Up", self.window.listWidget_componentList,
+ activated=lambda: self.moveComponent(-1)
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Down", self.window.listWidget_componentList,
+ activated=lambda: self.moveComponent(1)
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Home", self.window.listWidget_componentList,
+ activated=lambda: self.moveComponent('top')
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+End", self.window.listWidget_componentList,
+ activated=lambda: self.moveComponent('bottom')
+ )
+
+ # Debug Hotkeys
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+R", self.window, self.drawPreview
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
+ )
+
+ @QtCore.pyqtSlot()
+ def cleanUp(self, *args):
+ log.info('Ending the preview thread')
+ self.timer.stop()
+ self.previewThread.quit()
+ self.previewThread.wait()
+
+ @disableWhenOpeningProject
+ def updateWindowTitle(self):
+ appName = 'Audio Visualizer'
+ try:
+ if self.currentProject:
+ appName += ' - %s' % \
+ os.path.splitext(
+ os.path.basename(self.currentProject))[0]
+ if self.autosaveExists(identical=False):
+ appName += '*'
+ except AttributeError:
+ pass
+ log.debug('Setting window title to %s' % appName)
+ self.window.setWindowTitle(appName)
+
+ @QtCore.pyqtSlot(int, dict)
+ def updateComponentTitle(self, pos, presetStore=False):
+ 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])
+ else:
+ modified = bool(presetStore)
+ if pos < 0:
+ pos = len(self.core.selectedComponents)-1
+ name = str(self.core.selectedComponents[pos])
+ 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' % (
+ name, pos, modified, title
+ ))
+ else:
+ log.debug('Setting %s #%s\'s title: %s' % (
+ name, pos, title
+ ))
+ self.window.listWidget_componentList.item(pos).setText(title)
+
+ def updateCodecs(self):
+ containerWidget = self.window.comboBox_videoContainer
+ vCodecWidget = self.window.comboBox_videoCodec
+ aCodecWidget = self.window.comboBox_audioCodec
+ index = containerWidget.currentIndex()
+ name = containerWidget.itemText(index)
+ self.settings.setValue('outputContainer', name)
+
+ vCodecWidget.clear()
+ aCodecWidget.clear()
+
+ for container in Core.encoderOptions['containers']:
+ if container['name'] == name:
+ for vCodec in container['video-codecs']:
+ vCodecWidget.addItem(vCodec)
+ for aCodec in container['audio-codecs']:
+ aCodecWidget.addItem(aCodec)
+
+ def updateCodecSettings(self):
+ '''Updates settings.ini to match encoder option widgets'''
+ vCodecWidget = self.window.comboBox_videoCodec
+ vBitrateWidget = self.window.spinBox_vBitrate
+ aBitrateWidget = self.window.spinBox_aBitrate
+ aCodecWidget = self.window.comboBox_audioCodec
+ currentVideoCodec = vCodecWidget.currentIndex()
+ currentVideoCodec = vCodecWidget.itemText(currentVideoCodec)
+ currentVideoBitrate = vBitrateWidget.value()
+ currentAudioCodec = aCodecWidget.currentIndex()
+ currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
+ currentAudioBitrate = aBitrateWidget.value()
+ self.settings.setValue('outputVideoCodec', currentVideoCodec)
+ self.settings.setValue('outputAudioCodec', currentAudioCodec)
+ self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
+ self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
+
+ @disableWhenOpeningProject
+ def autosave(self, force=False):
+ if not self.currentProject:
+ if os.path.exists(self.autosavePath):
+ os.remove(self.autosavePath)
+ elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
+ self.core.createProjectFile(self.autosavePath, self.window)
+ self.lastAutosave = time.time()
+ if len(self.autosaveTimes) >= 5:
+ # Do some math to reduce autosave spam. This gives a smooth
+ # curve up to 5 seconds cooldown and maintains that for 30 secs
+ # if a component is continuously updated
+ timeDiff = self.lastAutosave - self.autosaveTimes.pop()
+ if not force and timeDiff >= 1.0 \
+ and timeDiff <= 10.0:
+ if self.autosaveCooldown / 4.0 < 0.5:
+ self.autosaveCooldown += 1.0
+ self.autosaveCooldown = (
+ 5.0 * (self.autosaveCooldown / 5.0)
+ ) + (self.autosaveCooldown / 5.0) * 2
+ elif force or timeDiff >= self.autosaveCooldown * 5:
+ self.autosaveCooldown = 0.2
+ self.autosaveTimes.insert(0, self.lastAutosave)
+ else:
+ log.debug('Autosave rejected by cooldown')
+
+ def autosaveExists(self, identical=True):
+ '''Determines if creating the autosave should be blocked.'''
+ try:
+ if self.currentProject and os.path.exists(self.autosavePath) \
+ and filecmp.cmp(
+ self.autosavePath, self.currentProject) == identical:
+ log.debug(
+ 'Autosave found %s to be identical'
+ % 'not' if not identical else ''
+ )
+ return True
+ except FileNotFoundError:
+ log.error(
+ 'Project file couldn\'t be located:', self.currentProject)
+ return identical
+ return False
+
+ def saveProjectChanges(self):
+ '''Overwrites project file with autosave file'''
+ try:
+ os.remove(self.currentProject)
+ os.rename(self.autosavePath, self.currentProject)
+ return True
+ except (FileNotFoundError, IsADirectoryError) as e:
+ self.showMessage(
+ msg='Project file couldn\'t be saved.',
+ detail=str(e))
+ return False
+
+ def openInputFileDialog(self):
+ inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
+
+ fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.window, "Open Audio File",
+ inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
+
+ if fileName:
+ self.settings.setValue("inputDir", os.path.dirname(fileName))
+ self.window.lineEdit_audioFile.setText(fileName)
+
+ def openOutputFileDialog(self):
+ outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
+
+ fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
+ self.window, "Set Output Video File",
+ outputDir,
+ "Video Files (%s);; All Files (*)" % " ".join(
+ Core.videoFormats))
+
+ if fileName:
+ self.settings.setValue("outputDir", os.path.dirname(fileName))
+ self.window.lineEdit_outputFile.setText(fileName)
+
+ def stopVideo(self):
+ log.info('Export cancelled')
+ self.videoWorker.cancel()
+ self.canceled = True
+
+ def createAudioVisualisation(self):
+ # create output video if mandatory settings are filled in
+ audioFile = self.window.lineEdit_audioFile.text()
+ outputPath = self.window.lineEdit_outputFile.text()
+
+ if audioFile and outputPath and self.core.selectedComponents:
+ if not os.path.dirname(outputPath):
+ outputPath = os.path.join(
+ os.path.expanduser("~"), outputPath)
+ if outputPath and os.path.isdir(outputPath):
+ self.showMessage(
+ msg='Chosen filename matches a directory, which '
+ 'cannot be overwritten. Please choose a different '
+ 'filename or move the directory.',
+ icon='Warning',
+ )
+ return
+ else:
+ if not audioFile or not outputPath:
+ self.showMessage(
+ msg="You must select an audio file and output filename."
+ )
+ elif not self.core.selectedComponents:
+ self.showMessage(
+ msg="Not enough components."
+ )
+ return
+
+ self.canceled = False
+ self.progressBarUpdated(-1)
+ self.videoWorker = self.core.newVideoWorker(
+ self, audioFile, outputPath
+ )
+ self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
+ self.videoWorker.progressBarSetText.connect(
+ self.progressBarSetText)
+ self.videoWorker.imageCreated.connect(self.showPreviewImage)
+ self.videoWorker.encoding.connect(self.changeEncodingStatus)
+ self.createVideo.emit()
+
+ @QtCore.pyqtSlot(str, str)
+ def videoThreadError(self, msg, detail):
+ try:
+ self.stopVideo()
+ except AttributeError as e:
+ if 'videoWorker' not in str(e):
+ raise
+ self.showMessage(
+ msg=msg,
+ detail=detail,
+ icon='Critical',
+ )
+
+ def changeEncodingStatus(self, status):
+ self.encoding = status
+ if status:
+ self.window.pushButton_createVideo.setEnabled(False)
+ self.window.pushButton_Cancel.setEnabled(True)
+ self.window.comboBox_resolution.setEnabled(False)
+ self.window.stackedWidget.setEnabled(False)
+ self.window.tab_encoderSettings.setEnabled(False)
+ self.window.label_audioFile.setEnabled(False)
+ self.window.toolButton_selectAudioFile.setEnabled(False)
+ self.window.label_outputFile.setEnabled(False)
+ self.window.toolButton_selectOutputFile.setEnabled(False)
+ self.window.lineEdit_audioFile.setEnabled(False)
+ self.window.lineEdit_outputFile.setEnabled(False)
+ self.window.pushButton_addComponent.setEnabled(False)
+ self.window.pushButton_removeComponent.setEnabled(False)
+ self.window.pushButton_listMoveDown.setEnabled(False)
+ self.window.pushButton_listMoveUp.setEnabled(False)
+ self.window.menuButton_newProject.setEnabled(False)
+ self.window.menuButton_openProject.setEnabled(False)
+ if sys.platform == 'darwin':
+ self.window.progressLabel.setHidden(False)
+ else:
+ self.window.listWidget_componentList.setEnabled(False)
+ else:
+ self.window.pushButton_createVideo.setEnabled(True)
+ self.window.pushButton_Cancel.setEnabled(False)
+ self.window.comboBox_resolution.setEnabled(True)
+ self.window.stackedWidget.setEnabled(True)
+ self.window.tab_encoderSettings.setEnabled(True)
+ self.window.label_audioFile.setEnabled(True)
+ self.window.toolButton_selectAudioFile.setEnabled(True)
+ self.window.lineEdit_audioFile.setEnabled(True)
+ self.window.label_outputFile.setEnabled(True)
+ self.window.toolButton_selectOutputFile.setEnabled(True)
+ self.window.lineEdit_outputFile.setEnabled(True)
+ self.window.pushButton_addComponent.setEnabled(True)
+ self.window.pushButton_removeComponent.setEnabled(True)
+ self.window.pushButton_listMoveDown.setEnabled(True)
+ self.window.pushButton_listMoveUp.setEnabled(True)
+ self.window.menuButton_newProject.setEnabled(True)
+ self.window.menuButton_openProject.setEnabled(True)
+ self.window.listWidget_componentList.setEnabled(True)
+ self.window.progressLabel.setHidden(True)
+ self.drawPreview(True)
+
+ @QtCore.pyqtSlot(int)
+ def progressBarUpdated(self, value):
+ self.window.progressBar_createVideo.setValue(value)
+
+ @QtCore.pyqtSlot(str)
+ def progressBarSetText(self, value):
+ if sys.platform == 'darwin':
+ self.window.progressLabel.setText(value)
+ else:
+ self.window.progressBar_createVideo.setFormat(value)
+
+ def updateResolution(self):
+ resIndex = int(self.window.comboBox_resolution.currentIndex())
+ res = Core.resolutions[resIndex].split('x')
+ changed = res[0] != self.settings.value("outputWidth")
+ self.settings.setValue('outputWidth', res[0])
+ self.settings.setValue('outputHeight', res[1])
+ if changed:
+ for i in range(len(self.core.selectedComponents)):
+ self.core.updateComponent(i)
+
+ def drawPreview(self, force=False, **kwargs):
+ '''Use autosave keyword arg to force saving or not saving if needed'''
+ self.newTask.emit(self.core.selectedComponents)
+ # self.processTask.emit()
+ if force or 'autosave' in kwargs:
+ if force or kwargs['autosave']:
+ self.autosave(True)
+ else:
+ self.autosave()
+ self.updateWindowTitle()
+
+ @QtCore.pyqtSlot(QtGui.QImage)
+ def showPreviewImage(self, image):
+ self.previewWindow.changePixmap(image)
+
+ def showFfmpegCommand(self):
+ from textwrap import wrap
+ from toolkit.ffmpeg import createFfmpegCommand
+ command = createFfmpegCommand(
+ self.window.lineEdit_audioFile.text(),
+ self.window.lineEdit_outputFile.text(),
+ self.core.selectedComponents
+ )
+ lines = wrap(" ".join(command), 49)
+ self.showMessage(
+ msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
+ )
+
+ def insertComponent(self, index):
+ componentList = self.window.listWidget_componentList
+ stackedWidget = self.window.stackedWidget
+
+ componentList.insertItem(
+ index,
+ self.core.selectedComponents[index].name)
+ componentList.setCurrentRow(index)
+
+ # connect to signal that adds an asterisk when modified
+ self.core.selectedComponents[index].modified.connect(
+ self.updateComponentTitle)
+
+ self.pages.insert(index, self.core.selectedComponents[index].page)
+ stackedWidget.insertWidget(index, self.pages[index])
+ stackedWidget.setCurrentIndex(index)
+
+ return index
+
+ 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()
+
+ @disableWhenEncoding
+ def moveComponent(self, change):
+ '''Moves a component relatively from its current position'''
+ componentList = self.window.listWidget_componentList
+ if change == 'top':
+ change = -componentList.currentRow()
+ elif change == 'bottom':
+ change = len(componentList)-componentList.currentRow()-1
+ stackedWidget = self.window.stackedWidget
+
+ row = componentList.currentRow()
+ newRow = row + change
+ if newRow > -1 and newRow < componentList.count():
+ self.core.moveComponent(row, newRow)
+
+ # 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):
+ '''
+ Given a QPos, returns the component index under the mouse cursor
+ or -1 if no component is there.
+ '''
+ componentList = self.window.listWidget_componentList
+
+ modelIndexes = [
+ componentList.model().index(i)
+ for i in range(componentList.count())
+ ]
+ rects = [
+ componentList.visualRect(modelIndex)
+ for modelIndex in modelIndexes
+ ]
+ mousePos = [rect.contains(position) for rect in rects]
+ if not any(mousePos):
+ # Not clicking a component
+ mousePos = -1
+ else:
+ mousePos = mousePos.index(True)
+ log.debug('Click component list row %s' % mousePos)
+ return mousePos
+
+ @disableWhenEncoding
+ def dragComponent(self, event):
+ '''Used as Qt drop event for the component listwidget'''
+ componentList = self.window.listWidget_componentList
+ mousePos = self.getComponentListMousePos(event.pos())
+ if mousePos > -1:
+ change = (componentList.currentRow() - mousePos) * -1
+ else:
+ change = (componentList.count() - componentList.currentRow() - 1)
+ self.moveComponent(change)
+
+ def changeComponentWidget(self):
+ selected = self.window.listWidget_componentList.selectedItems()
+ if selected:
+ index = self.window.listWidget_componentList.row(selected[0])
+ self.window.stackedWidget.setCurrentIndex(index)
+
+ def openPresetManager(self):
+ '''Preset manager for importing, exporting, renaming, deleting'''
+ self.presetManager.show()
+
+ def clear(self):
+ '''Get a blank slate'''
+ self.core.clearComponents()
+ self.window.listWidget_componentList.clear()
+ for widget in self.pages:
+ self.window.stackedWidget.removeWidget(widget)
+ self.pages = []
+ for field in (
+ self.window.lineEdit_audioFile,
+ self.window.lineEdit_outputFile
+ ):
+ field.blockSignals(True)
+ field.setText('')
+ field.blockSignals(False)
+ self.progressBarUpdated(0)
+ self.progressBarSetText('')
+
+ @disableWhenEncoding
+ def createNewProject(self, prompt=True):
+ if prompt:
+ self.openSaveChangesDialog('starting a new project')
+
+ self.clear()
+ self.currentProject = None
+ self.settings.setValue("currentProject", None)
+ self.drawPreview(True)
+
+ def saveCurrentProject(self):
+ if self.currentProject:
+ self.core.createProjectFile(self.currentProject, self.window)
+ try:
+ os.remove(self.autosavePath)
+ except FileNotFoundError:
+ pass
+ self.updateWindowTitle()
+ else:
+ self.openSaveProjectDialog()
+
+ def openSaveChangesDialog(self, phrase):
+ success = True
+ if self.autosaveExists(identical=False):
+ ch = self.showMessage(
+ msg="You have unsaved changes in project '%s'. "
+ "Save before %s?" % (
+ os.path.basename(self.currentProject)[:-4],
+ phrase
+ ),
+ showCancel=True)
+ if ch:
+ success = self.saveProjectChanges()
+
+ if success and os.path.exists(self.autosavePath):
+ os.remove(self.autosavePath)
+
+ def openSaveProjectDialog(self):
+ filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+ self.window, "Create Project File",
+ self.settings.value("projectDir"),
+ "Project Files (*.avp)")
+ if not filename:
+ return
+ if not filename.endswith(".avp"):
+ filename += '.avp'
+ self.settings.setValue("projectDir", os.path.dirname(filename))
+ self.settings.setValue("currentProject", filename)
+ self.currentProject = filename
+ self.core.createProjectFile(filename, self.window)
+ self.updateWindowTitle()
+
+ @disableWhenEncoding
+ def openOpenProjectDialog(self):
+ filename, _ = QtWidgets.QFileDialog.getOpenFileName(
+ self.window, "Open Project File",
+ self.settings.value("projectDir"),
+ "Project Files (*.avp)")
+ self.openProject(filename)
+
+ def openProject(self, filepath, prompt=True):
+ if not filepath or not os.path.exists(filepath) \
+ or not filepath.endswith('.avp'):
+ return
+
+ self.clear()
+ # ask to save any changes that are about to get deleted
+ if prompt:
+ self.openSaveChangesDialog('opening another project')
+
+ self.currentProject = filepath
+ self.settings.setValue("currentProject", filepath)
+ self.settings.setValue("projectDir", os.path.dirname(filepath))
+ # actually load the project using core method
+ self.core.openProject(self, filepath)
+ self.drawPreview(autosave=False)
+ self.updateWindowTitle()
+
+ def showMessage(self, **kwargs):
+ parent = kwargs['parent'] if 'parent' in kwargs else self.window
+ msg = QtWidgets.QMessageBox(parent)
+ msg.setModal(True)
+ msg.setText(kwargs['msg'])
+ msg.setIcon(
+ eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
+ if 'icon' in kwargs else QtWidgets.QMessageBox.Information
+ )
+ msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
+ if 'showCancel'in kwargs and kwargs['showCancel']:
+ msg.setStandardButtons(
+ QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
+ else:
+ msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
+ ch = msg.exec_()
+ if ch == 1024:
+ return True
+ return False
+
+ @disableWhenEncoding
+ def componentContextMenu(self, QPos):
+ '''Appears when right-clicking the component list'''
+ componentList = self.window.listWidget_componentList
+ self.menu = QMenu()
+ parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
+
+ index = self.getComponentListMousePos(QPos)
+ if index > -1:
+ # Show preset menu if clicking a component
+ self.presetManager.findPresets()
+ menuItem = self.menu.addAction("Save Preset")
+ menuItem.triggered.connect(
+ self.presetManager.openSavePresetDialog
+ )
+
+ # submenu for opening presets
+ try:
+ presets = self.presetManager.presets[
+ str(self.core.selectedComponents[index])
+ ]
+ self.presetSubmenu = QMenu("Open Preset")
+ self.menu.addMenu(self.presetSubmenu)
+
+ for version, presetName in presets:
+ menuItem = self.presetSubmenu.addAction(presetName)
+ menuItem.triggered.connect(
+ lambda _, presetName=presetName:
+ self.presetManager.openPreset(presetName)
+ )
+ except KeyError:
+ pass
+
+ if self.core.selectedComponents[index].currentPreset:
+ menuItem = self.menu.addAction("Clear Preset")
+ menuItem.triggered.connect(
+ self.presetManager.clearPreset
+ )
+ self.menu.addSeparator()
+
+ # "Add Component" submenu
+ self.submenu = QMenu("Add")
+ self.menu.addMenu(self.submenu)
+ insertCompAtTop = self.settings.value("pref_insertCompAtTop")
+ for i, comp in enumerate(self.core.modules):
+ menuItem = self.submenu.addAction(comp.Component.name)
+ menuItem.triggered.connect(
+ lambda _, item=i: self.core.insertComponent(
+ 0 if insertCompAtTop else index, item, self
+ )
+ )
+
+ self.menu.move(parentPosition + QPos)
+ self.menu.show()
+
+ def eventFilter(self, object, event):
+ if event.type() == QtCore.QEvent.WindowActivate \
+ or event.type() == QtCore.QEvent.FocusIn:
+ Core.windowHasFocus = True
+ elif event.type() == QtCore.QEvent.WindowDeactivate \
+ or event.type() == QtCore.QEvent.FocusOut:
+ Core.windowHasFocus = False
+ return False
diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui
new file mode 100644
index 0000000..b43d375
--- /dev/null
+++ b/src/gui/mainwindow.ui
@@ -0,0 +1,828 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1008
+ 575
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::StrongFocus
+
+
+ MainWindow
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+
+ 9
+
+
+ 0
+
+ -
+
+
-
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 0
+ 360
+
+
+
+
+ -
+
+
+ QLayout::SetDefaultConstraint
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 420
+ 0
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetMinimumSize
+
+
+ 3
+
+
-
+
+
+ QLayout::SetMinimumSize
+
+
+ 3
+
+
-
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 140
+ 20
+
+
+
+
+ -
+
+
+ Projects
+
+
+
+ -
+
+
+ Presets
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 20
+ 2
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ true
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Sunken
+
+
+ 1
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ QAbstractItemView::InternalMove
+
+
+ Qt::MoveAction
+
+
+
+ -
+
+
-
+
+
+ Add
+
+
+
+ -
+
+
+ Remove
+
+
+
+ -
+
+
+ Up
+
+
+
+ -
+
+
+ Down
+
+
+
+
+
+
+
+ -
+
+
+ 4
+
+
+ 2
+
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetFixedSize
+
+
+ 4
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 500
+ 0
+
+
+
+
+ 16777215
+ 180
+
+
+
+ QTabWidget::North
+
+
+ QTabWidget::Rounded
+
+
+ 0
+
+
+
+ Export Video
+
+
+
+ 10
+
+
-
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+
+ 80
+ 16777215
+
+
+
+
+ 80
+ 0
+
+
+
+ Audio File
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+ ...
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Output File
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+
+ 16777215
+ 28
+
+
+
+ ...
+
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 24
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 10
+ 20
+
+
+
+
+ -
+
+
+ Create Video
+
+
+
+ -
+
+
+ false
+
+
+ Cancel
+
+
+
+
+
+ -
+
+
+
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+ -1
+
+
+
+
+
+
+ progressLabel
+
+
+
+ Encoder Settings
+
+
+
+ 10
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Container
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Minimum
+
+
+
+ 5
+ 5
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Resolution
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Video Codec
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 5
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Video Bitrate (Kbps)
+
+
+
+ -
+
+
+ 99999
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 85
+ 0
+
+
+
+ Audio Codec
+
+
+
+ -
+
+
+
+ 150
+ 0
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 5
+ 10
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Audio Bitrate (Kbps)
+
+
+
+ -
+
+
+ 9999
+
+
+
+
+
+
+
+
+
+ -
+
+
+ QLayout::SetDefaultConstraint
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 500
+ 0
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 180
+
+
+
+
+ 16777215
+ 180
+
+
+
+ -1
+
+
+
+
+
+
+
+
+
+
+
+
+
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))
diff --git a/src/gui/presetmanager.ui b/src/gui/presetmanager.ui
new file mode 100644
index 0000000..5257b1c
--- /dev/null
+++ b/src/gui/presetmanager.ui
@@ -0,0 +1,150 @@
+
+
+ presetmanager
+
+
+ Qt::NonModal
+
+
+ true
+
+
+
+ 0
+ 0
+ 497
+ 377
+
+
+
+ Preset Manager
+
+
+ -
+
+
-
+
+
+
+
+
+ Filter by name
+
+
+
+ -
+
+
+
+ 200
+ 0
+
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+
+
+ -
+
+
+ QLayout::SetMinimumSize
+
+
-
+
+
+ Import
+
+
+
+ -
+
+
+ Export
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ true
+
+
+ Rename
+
+
+
+ -
+
+
+ Delete
+
+
+
+
+
+ -
+
+
-
+
+
+ <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html>
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Close
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
new file mode 100644
index 0000000..9615884
--- /dev/null
+++ b/src/gui/preview_thread.py
@@ -0,0 +1,90 @@
+'''
+ Thread that runs to create QImages for MainWindow's preview label.
+ Processes a queue of component lists.
+'''
+from PyQt5 import QtCore, QtGui, uic
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PIL import Image
+from PIL.ImageQt import ImageQt
+from queue import Queue, Empty
+import os
+import logging
+
+from toolkit.frame import Checkerboard
+from toolkit import disableWhenOpeningProject
+
+
+log = logging.getLogger("AVP.PreviewThread")
+
+
+class Worker(QtCore.QObject):
+
+ imageCreated = pyqtSignal(QtGui.QImage)
+ error = pyqtSignal(str)
+
+ def __init__(self, parent=None, queue=None):
+ QtCore.QObject.__init__(self)
+ parent.newTask.connect(self.createPreviewImage)
+ parent.processTask.connect(self.process)
+ self.parent = parent
+ self.core = parent.core
+ self.settings = parent.settings
+ self.queue = queue
+
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
+ self.background = Checkerboard(width, height)
+
+ @disableWhenOpeningProject
+ @pyqtSlot(list)
+ def createPreviewImage(self, components):
+ dic = {
+ "components": components,
+ }
+ self.queue.put(dic)
+
+ @pyqtSlot()
+ def process(self):
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
+ try:
+ nextPreviewInformation = self.queue.get(block=False)
+ while self.queue.qsize() >= 2:
+ try:
+ self.queue.get(block=False)
+ except Empty:
+ continue
+ if self.background.width != width \
+ or self.background.height != height:
+ self.background = Checkerboard(width, height)
+
+ frame = self.background.copy()
+ log.debug('Creating new preview frame')
+ components = nextPreviewInformation["components"]
+ for component in reversed(components):
+ try:
+ component.lockSize(width, height)
+ newFrame = component.previewRender()
+ component.unlockSize()
+ frame = Image.alpha_composite(
+ frame, newFrame
+ )
+
+ except ValueError as e:
+ errMsg = "Bad frame returned by %s's preview renderer. " \
+ "%s. New frame size was %s*%s; should be %s*%s." % (
+ str(component), str(e).capitalize(),
+ newFrame.width, newFrame.height,
+ width, height
+ )
+ log.critical(errMsg)
+ self.error.emit(errMsg)
+ break
+ except RuntimeError as e:
+ log.error(str(e))
+ else:
+ self.frame = ImageQt(frame)
+ self.imageCreated.emit(QtGui.QImage(self.frame))
+
+ except Empty:
+ True
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
new file mode 100644
index 0000000..40c19c6
--- /dev/null
+++ b/src/gui/preview_win.py
@@ -0,0 +1,62 @@
+from PyQt5 import QtCore, QtGui, QtWidgets
+import logging
+
+
+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')
+
+ def __init__(self, parent, img):
+ super(PreviewWindow, self).__init__()
+ self.parent = parent
+ self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
+ self.pixmap = QtGui.QPixmap(img)
+
+ def paintEvent(self, event):
+ size = self.size()
+ painter = QtGui.QPainter(self)
+ point = QtCore.QPoint(0, 0)
+ scaledPix = self.pixmap.scaled(
+ size,
+ QtCore.Qt.KeepAspectRatio,
+ transformMode=QtCore.Qt.SmoothTransformation)
+
+ # start painting the label from left upper corner
+ point.setX((size.width() - scaledPix.width())/2)
+ point.setY((size.height() - scaledPix.height())/2)
+ painter.drawPixmap(point, scaledPix)
+
+ def changePixmap(self, img):
+ self.pixmap = QtGui.QPixmap(img)
+ self.repaint()
+
+ def mousePressEvent(self, event):
+ if self.parent.encoding:
+ return
+
+ i = self.parent.window.listWidget_componentList.currentRow()
+ if i >= 0:
+ component = self.parent.core.selectedComponents[i]
+ if not hasattr(component, 'previewClickEvent'):
+ self.log.info('Ignored click event')
+ return
+ pos = (event.x(), event.y())
+ size = (self.width(), self.height())
+ butt = event.button()
+ self.log.info('Click event for #%s: %s button %s' % (
+ i, pos, butt))
+ component.previewClickEvent(
+ pos, size, butt
+ )
+ self.parent.core.updateComponent(i)
+
+ @QtCore.pyqtSlot(str)
+ def threadError(self, msg):
+ self.parent.showMessage(
+ msg=msg,
+ icon='Critical',
+ parent=self
+ )
diff --git a/src/mainwindow.py b/src/mainwindow.py
deleted file mode 100644
index af6e190..0000000
--- a/src/mainwindow.py
+++ /dev/null
@@ -1,946 +0,0 @@
-'''
- When using GUI mode, this module's object (the main window) takes
- user input to construct a program state (stored in the Core object).
- This shows a preview of the video being created and allows for saving
- projects and exporting the video at a later time.
-'''
-from PyQt5 import QtCore, QtGui, uic, QtWidgets
-from PyQt5.QtWidgets import QMenu, QShortcut
-from PIL import Image
-from queue import Queue
-import sys
-import os
-import signal
-import filecmp
-import time
-import logging
-
-from core import Core
-import preview_thread
-from preview_win import PreviewWindow
-from presetmanager import PresetManager
-from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
-
-
-log = logging.getLogger('AVP.MainWindow')
-
-
-class MainWindow(QtWidgets.QMainWindow):
- '''
- The MainWindow wraps many Core methods in order to update the GUI
- accordingly. E.g., instead of self.core.openProject(), it will use
- self.openProject() and update the window titlebar within the wrapper.
-
- MainWindow manages the autosave feature, although Core has the
- primary functions for opening and creating project files.
- '''
-
- createVideo = QtCore.pyqtSignal()
- newTask = QtCore.pyqtSignal(list) # for the preview window
- processTask = QtCore.pyqtSignal()
-
- def __init__(self, window, project):
- QtWidgets.QMainWindow.__init__(self)
- self.window = window
- self.core = Core()
- log.debug(
- 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
-
- # widgets of component settings
- self.pages = []
- self.lastAutosave = time.time()
- # list of previous five autosave times, used to reduce update spam
- self.autosaveTimes = []
- self.autosaveCooldown = 0.2
- self.encoding = False
-
- # Find settings created by Core object
- self.dataDir = Core.dataDir
- self.presetDir = Core.presetDir
- self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = Core.settings
-
- self.presetManager = PresetManager(
- uic.loadUi(
- os.path.join(Core.wd, 'presetmanager.ui')), self)
-
- # 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"))
- window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
-
- log.debug('Starting preview thread')
- self.previewQueue = Queue()
- self.previewThread = QtCore.QThread(self)
- self.previewWorker = preview_thread.Worker(self, self.previewQueue)
- self.previewWorker.error.connect(self.previewWindow.threadError)
- self.previewWorker.moveToThread(self.previewThread)
- self.previewWorker.imageCreated.connect(self.showPreviewImage)
- self.previewThread.start()
-
- log.debug('Starting preview timer')
- self.timer = QtCore.QTimer(self)
- self.timer.timeout.connect(self.processTask.emit)
- self.timer.start(500)
-
- # Begin decorating the window and connecting events
- self.window.installEventFilter(self)
- componentList = self.window.listWidget_componentList
-
- if sys.platform == 'darwin':
- log.debug(
- 'Darwin detected: showing progress label below progress bar')
- window.progressBar_createVideo.setTextVisible(False)
- else:
- window.progressLabel.setHidden(True)
-
- window.toolButton_selectAudioFile.clicked.connect(
- self.openInputFileDialog)
-
- window.toolButton_selectOutputFile.clicked.connect(
- self.openOutputFileDialog)
-
- def changedField():
- self.autosave()
- self.updateWindowTitle()
-
- window.lineEdit_audioFile.textChanged.connect(changedField)
- window.lineEdit_outputFile.textChanged.connect(changedField)
-
- window.progressBar_createVideo.setValue(0)
-
- window.pushButton_createVideo.clicked.connect(
- self.createAudioVisualisation)
-
- window.pushButton_Cancel.clicked.connect(self.stopVideo)
-
- for i, container in enumerate(Core.encoderOptions['containers']):
- window.comboBox_videoContainer.addItem(container['name'])
- if container['name'] == self.settings.value('outputContainer'):
- selectedContainer = i
-
- window.comboBox_videoContainer.setCurrentIndex(selectedContainer)
- window.comboBox_videoContainer.currentIndexChanged.connect(
- self.updateCodecs
- )
-
- self.updateCodecs()
-
- for i in range(window.comboBox_videoCodec.count()):
- codec = window.comboBox_videoCodec.itemText(i)
- if codec == self.settings.value('outputVideoCodec'):
- window.comboBox_videoCodec.setCurrentIndex(i)
-
- for i in range(window.comboBox_audioCodec.count()):
- codec = window.comboBox_audioCodec.itemText(i)
- if codec == self.settings.value('outputAudioCodec'):
- window.comboBox_audioCodec.setCurrentIndex(i)
-
- window.comboBox_videoCodec.currentIndexChanged.connect(
- self.updateCodecSettings
- )
-
- window.comboBox_audioCodec.currentIndexChanged.connect(
- self.updateCodecSettings
- )
-
- vBitrate = int(self.settings.value('outputVideoBitrate'))
- aBitrate = int(self.settings.value('outputAudioBitrate'))
-
- window.spinBox_vBitrate.setValue(vBitrate)
- window.spinBox_aBitrate.setValue(aBitrate)
- window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
- window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
-
- # Make component buttons
- self.compMenu = QMenu()
- 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)
- )
-
- self.window.pushButton_addComponent.setMenu(self.compMenu)
-
- componentList.dropEvent = self.dragComponent
- componentList.itemSelectionChanged.connect(
- self.changeComponentWidget
- )
- componentList.itemSelectionChanged.connect(
- self.presetManager.clearPresetListSelection
- )
- self.window.pushButton_removeComponent.clicked.connect(
- lambda: self.removeComponent()
- )
-
- componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- componentList.customContextMenuRequested.connect(
- self.componentContextMenu
- )
-
- currentRes = str(self.settings.value('outputWidth'))+'x' + \
- str(self.settings.value('outputHeight'))
- for i, res in enumerate(Core.resolutions):
- window.comboBox_resolution.addItem(res)
- if res == currentRes:
- currentRes = i
- window.comboBox_resolution.setCurrentIndex(currentRes)
- window.comboBox_resolution.currentIndexChanged.connect(
- self.updateResolution
- )
-
- self.window.pushButton_listMoveUp.clicked.connect(
- lambda: self.moveComponent(-1)
- )
- self.window.pushButton_listMoveDown.clicked.connect(
- lambda: self.moveComponent(1)
- )
-
- # Configure the Projects Menu
- self.projectMenu = QMenu()
- self.window.menuButton_newProject = self.projectMenu.addAction(
- "New Project"
- )
- self.window.menuButton_newProject.triggered.connect(
- lambda: self.createNewProject()
- )
- self.window.menuButton_openProject = self.projectMenu.addAction(
- "Open Project"
- )
- self.window.menuButton_openProject.triggered.connect(
- lambda: self.openOpenProjectDialog()
- )
-
- action = self.projectMenu.addAction("Save Project")
- action.triggered.connect(self.saveCurrentProject)
-
- action = self.projectMenu.addAction("Save Project As")
- action.triggered.connect(self.openSaveProjectDialog)
-
- self.window.pushButton_projects.setMenu(self.projectMenu)
-
- # Configure the Presets Button
- self.window.pushButton_presets.clicked.connect(
- self.openPresetManager
- )
-
- self.updateWindowTitle()
- log.debug('Showing main window')
- window.show()
-
- if project and project != self.autosavePath:
- if not project.endswith('.avp'):
- project += '.avp'
- # open a project from the commandline
- if not os.path.dirname(project):
- project = os.path.join(
- self.settings.value("projectDir"), project
- )
- self.currentProject = project
- self.settings.setValue("currentProject", project)
- if os.path.exists(self.autosavePath):
- os.remove(self.autosavePath)
- else:
- # open the last currentProject from settings
- self.currentProject = self.settings.value("currentProject")
-
- # delete autosave if it's identical to this project
- if self.autosaveExists(identical=True):
- os.remove(self.autosavePath)
-
- if self.currentProject and os.path.exists(self.autosavePath):
- ch = self.showMessage(
- msg="Restore unsaved changes in project '%s'?"
- % os.path.basename(self.currentProject)[:-4],
- showCancel=True)
- if ch:
- self.saveProjectChanges()
- else:
- os.remove(self.autosavePath)
-
- self.openProject(self.currentProject, prompt=False)
- self.drawPreview(True)
-
- # verify Pillow version
- if not self.settings.value("pilMsgShown") \
- and 'post' not in Image.PILLOW_VERSION:
- self.showMessage(
- msg="You are using the standard version of the "
- "Python imaging library (Pillow %s). Upgrade "
- "to the Pillow-SIMD fork to enable hardware accelerations "
- "and export videos faster." % Image.PILLOW_VERSION
- )
- self.settings.setValue("pilMsgShown", True)
-
- # verify Ffmpeg version
- if not self.settings.value("ffmpegMsgShown"):
- try:
- with open(os.devnull, "w") as f:
- ffmpegVers = checkOutput(
- ['ffmpeg', '-version'], stderr=f
- )
- goodVersion = str(ffmpegVers).split()[2].startswith('3')
- except Exception:
- goodVersion = False
- else:
- goodVersion = True
-
- if not goodVersion:
- self.showMessage(
- msg="You're using an old version of Ffmpeg. "
- "Some features may not work as expected."
- )
- self.settings.setValue("ffmpegMsgShown", True)
-
- # Hotkeys for projects
- QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
- QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
- QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
- QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
-
- # Hotkeys for component list
- for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
- QtWidgets.QShortcut(
- inskey, self.window,
- activated=lambda: self.window.pushButton_addComponent.click()
- )
- for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
- QtWidgets.QShortcut(
- delkey, self.window.listWidget_componentList,
- self.removeComponent
- )
- QtWidgets.QShortcut(
- "Ctrl+Space", self.window,
- activated=lambda: self.window.listWidget_componentList.setFocus()
- )
- QtWidgets.QShortcut(
- "Ctrl+Shift+S", self.window,
- self.presetManager.openSavePresetDialog
- )
- QtWidgets.QShortcut(
- "Ctrl+Shift+C", self.window, self.presetManager.clearPreset
- )
-
- QtWidgets.QShortcut(
- "Ctrl+Up", self.window.listWidget_componentList,
- activated=lambda: self.moveComponent(-1)
- )
- QtWidgets.QShortcut(
- "Ctrl+Down", self.window.listWidget_componentList,
- activated=lambda: self.moveComponent(1)
- )
- QtWidgets.QShortcut(
- "Ctrl+Home", self.window.listWidget_componentList,
- activated=lambda: self.moveComponent('top')
- )
- QtWidgets.QShortcut(
- "Ctrl+End", self.window.listWidget_componentList,
- activated=lambda: self.moveComponent('bottom')
- )
-
- # Debug Hotkeys
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+R", self.window, self.drawPreview
- )
- QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
- )
-
- @QtCore.pyqtSlot()
- def cleanUp(self, *args):
- log.info('Ending the preview thread')
- self.timer.stop()
- self.previewThread.quit()
- self.previewThread.wait()
-
- @disableWhenOpeningProject
- def updateWindowTitle(self):
- appName = 'Audio Visualizer'
- try:
- if self.currentProject:
- appName += ' - %s' % \
- os.path.splitext(
- os.path.basename(self.currentProject))[0]
- if self.autosaveExists(identical=False):
- appName += '*'
- except AttributeError:
- pass
- log.debug('Setting window title to %s' % appName)
- self.window.setWindowTitle(appName)
-
- @QtCore.pyqtSlot(int, dict)
- def updateComponentTitle(self, pos, presetStore=False):
- 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])
- else:
- modified = bool(presetStore)
- if pos < 0:
- pos = len(self.core.selectedComponents)-1
- name = str(self.core.selectedComponents[pos])
- 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' % (
- name, pos, modified, title
- ))
- else:
- log.debug('Setting %s #%s\'s title: %s' % (
- name, pos, title
- ))
- self.window.listWidget_componentList.item(pos).setText(title)
-
- def updateCodecs(self):
- containerWidget = self.window.comboBox_videoContainer
- vCodecWidget = self.window.comboBox_videoCodec
- aCodecWidget = self.window.comboBox_audioCodec
- index = containerWidget.currentIndex()
- name = containerWidget.itemText(index)
- self.settings.setValue('outputContainer', name)
-
- vCodecWidget.clear()
- aCodecWidget.clear()
-
- for container in Core.encoderOptions['containers']:
- if container['name'] == name:
- for vCodec in container['video-codecs']:
- vCodecWidget.addItem(vCodec)
- for aCodec in container['audio-codecs']:
- aCodecWidget.addItem(aCodec)
-
- def updateCodecSettings(self):
- '''Updates settings.ini to match encoder option widgets'''
- vCodecWidget = self.window.comboBox_videoCodec
- vBitrateWidget = self.window.spinBox_vBitrate
- aBitrateWidget = self.window.spinBox_aBitrate
- aCodecWidget = self.window.comboBox_audioCodec
- currentVideoCodec = vCodecWidget.currentIndex()
- currentVideoCodec = vCodecWidget.itemText(currentVideoCodec)
- currentVideoBitrate = vBitrateWidget.value()
- currentAudioCodec = aCodecWidget.currentIndex()
- currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
- currentAudioBitrate = aBitrateWidget.value()
- self.settings.setValue('outputVideoCodec', currentVideoCodec)
- self.settings.setValue('outputAudioCodec', currentAudioCodec)
- self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
- self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
-
- @disableWhenOpeningProject
- def autosave(self, force=False):
- if not self.currentProject:
- if os.path.exists(self.autosavePath):
- os.remove(self.autosavePath)
- elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
- self.core.createProjectFile(self.autosavePath, self.window)
- self.lastAutosave = time.time()
- if len(self.autosaveTimes) >= 5:
- # Do some math to reduce autosave spam. This gives a smooth
- # curve up to 5 seconds cooldown and maintains that for 30 secs
- # if a component is continuously updated
- timeDiff = self.lastAutosave - self.autosaveTimes.pop()
- if not force and timeDiff >= 1.0 \
- and timeDiff <= 10.0:
- if self.autosaveCooldown / 4.0 < 0.5:
- self.autosaveCooldown += 1.0
- self.autosaveCooldown = (
- 5.0 * (self.autosaveCooldown / 5.0)
- ) + (self.autosaveCooldown / 5.0) * 2
- elif force or timeDiff >= self.autosaveCooldown * 5:
- self.autosaveCooldown = 0.2
- self.autosaveTimes.insert(0, self.lastAutosave)
- else:
- log.debug('Autosave rejected by cooldown')
-
- def autosaveExists(self, identical=True):
- '''Determines if creating the autosave should be blocked.'''
- try:
- if self.currentProject and os.path.exists(self.autosavePath) \
- and filecmp.cmp(
- self.autosavePath, self.currentProject) == identical:
- log.debug(
- 'Autosave found %s to be identical'
- % 'not' if not identical else ''
- )
- return True
- except FileNotFoundError:
- log.error(
- 'Project file couldn\'t be located:', self.currentProject)
- return identical
- return False
-
- def saveProjectChanges(self):
- '''Overwrites project file with autosave file'''
- try:
- os.remove(self.currentProject)
- os.rename(self.autosavePath, self.currentProject)
- return True
- except (FileNotFoundError, IsADirectoryError) as e:
- self.showMessage(
- msg='Project file couldn\'t be saved.',
- detail=str(e))
- return False
-
- def openInputFileDialog(self):
- inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
-
- fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.window, "Open Audio File",
- inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
-
- if fileName:
- self.settings.setValue("inputDir", os.path.dirname(fileName))
- self.window.lineEdit_audioFile.setText(fileName)
-
- def openOutputFileDialog(self):
- outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
-
- fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.window, "Set Output Video File",
- outputDir,
- "Video Files (%s);; All Files (*)" % " ".join(
- Core.videoFormats))
-
- if fileName:
- self.settings.setValue("outputDir", os.path.dirname(fileName))
- self.window.lineEdit_outputFile.setText(fileName)
-
- def stopVideo(self):
- log.info('Export cancelled')
- self.videoWorker.cancel()
- self.canceled = True
-
- def createAudioVisualisation(self):
- # create output video if mandatory settings are filled in
- audioFile = self.window.lineEdit_audioFile.text()
- outputPath = self.window.lineEdit_outputFile.text()
-
- if audioFile and outputPath and self.core.selectedComponents:
- if not os.path.dirname(outputPath):
- outputPath = os.path.join(
- os.path.expanduser("~"), outputPath)
- if outputPath and os.path.isdir(outputPath):
- self.showMessage(
- msg='Chosen filename matches a directory, which '
- 'cannot be overwritten. Please choose a different '
- 'filename or move the directory.',
- icon='Warning',
- )
- return
- else:
- if not audioFile or not outputPath:
- self.showMessage(
- msg="You must select an audio file and output filename."
- )
- elif not self.core.selectedComponents:
- self.showMessage(
- msg="Not enough components."
- )
- return
-
- self.canceled = False
- self.progressBarUpdated(-1)
- self.videoWorker = self.core.newVideoWorker(
- self, audioFile, outputPath
- )
- self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
- self.videoWorker.progressBarSetText.connect(
- self.progressBarSetText)
- self.videoWorker.imageCreated.connect(self.showPreviewImage)
- self.videoWorker.encoding.connect(self.changeEncodingStatus)
- self.createVideo.emit()
-
- @QtCore.pyqtSlot(str, str)
- def videoThreadError(self, msg, detail):
- try:
- self.stopVideo()
- except AttributeError as e:
- if 'videoWorker' not in str(e):
- raise
- self.showMessage(
- msg=msg,
- detail=detail,
- icon='Critical',
- )
-
- def changeEncodingStatus(self, status):
- self.encoding = status
- if status:
- self.window.pushButton_createVideo.setEnabled(False)
- self.window.pushButton_Cancel.setEnabled(True)
- self.window.comboBox_resolution.setEnabled(False)
- self.window.stackedWidget.setEnabled(False)
- self.window.tab_encoderSettings.setEnabled(False)
- self.window.label_audioFile.setEnabled(False)
- self.window.toolButton_selectAudioFile.setEnabled(False)
- self.window.label_outputFile.setEnabled(False)
- self.window.toolButton_selectOutputFile.setEnabled(False)
- self.window.lineEdit_audioFile.setEnabled(False)
- self.window.lineEdit_outputFile.setEnabled(False)
- self.window.pushButton_addComponent.setEnabled(False)
- self.window.pushButton_removeComponent.setEnabled(False)
- self.window.pushButton_listMoveDown.setEnabled(False)
- self.window.pushButton_listMoveUp.setEnabled(False)
- self.window.menuButton_newProject.setEnabled(False)
- self.window.menuButton_openProject.setEnabled(False)
- if sys.platform == 'darwin':
- self.window.progressLabel.setHidden(False)
- else:
- self.window.listWidget_componentList.setEnabled(False)
- else:
- self.window.pushButton_createVideo.setEnabled(True)
- self.window.pushButton_Cancel.setEnabled(False)
- self.window.comboBox_resolution.setEnabled(True)
- self.window.stackedWidget.setEnabled(True)
- self.window.tab_encoderSettings.setEnabled(True)
- self.window.label_audioFile.setEnabled(True)
- self.window.toolButton_selectAudioFile.setEnabled(True)
- self.window.lineEdit_audioFile.setEnabled(True)
- self.window.label_outputFile.setEnabled(True)
- self.window.toolButton_selectOutputFile.setEnabled(True)
- self.window.lineEdit_outputFile.setEnabled(True)
- self.window.pushButton_addComponent.setEnabled(True)
- self.window.pushButton_removeComponent.setEnabled(True)
- self.window.pushButton_listMoveDown.setEnabled(True)
- self.window.pushButton_listMoveUp.setEnabled(True)
- self.window.menuButton_newProject.setEnabled(True)
- self.window.menuButton_openProject.setEnabled(True)
- self.window.listWidget_componentList.setEnabled(True)
- self.window.progressLabel.setHidden(True)
- self.drawPreview(True)
-
- @QtCore.pyqtSlot(int)
- def progressBarUpdated(self, value):
- self.window.progressBar_createVideo.setValue(value)
-
- @QtCore.pyqtSlot(str)
- def progressBarSetText(self, value):
- if sys.platform == 'darwin':
- self.window.progressLabel.setText(value)
- else:
- self.window.progressBar_createVideo.setFormat(value)
-
- def updateResolution(self):
- resIndex = int(self.window.comboBox_resolution.currentIndex())
- res = Core.resolutions[resIndex].split('x')
- changed = res[0] != self.settings.value("outputWidth")
- self.settings.setValue('outputWidth', res[0])
- self.settings.setValue('outputHeight', res[1])
- if changed:
- for i in range(len(self.core.selectedComponents)):
- self.core.updateComponent(i)
-
- def drawPreview(self, force=False, **kwargs):
- '''Use autosave keyword arg to force saving or not saving if needed'''
- self.newTask.emit(self.core.selectedComponents)
- # self.processTask.emit()
- if force or 'autosave' in kwargs:
- if force or kwargs['autosave']:
- self.autosave(True)
- else:
- self.autosave()
- self.updateWindowTitle()
-
- @QtCore.pyqtSlot(QtGui.QImage)
- def showPreviewImage(self, image):
- self.previewWindow.changePixmap(image)
-
- def showFfmpegCommand(self):
- from textwrap import wrap
- from toolkit.ffmpeg import createFfmpegCommand
- command = createFfmpegCommand(
- self.window.lineEdit_audioFile.text(),
- self.window.lineEdit_outputFile.text(),
- self.core.selectedComponents
- )
- lines = wrap(" ".join(command), 49)
- self.showMessage(
- msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
- )
-
- def insertComponent(self, index):
- componentList = self.window.listWidget_componentList
- stackedWidget = self.window.stackedWidget
-
- componentList.insertItem(
- index,
- self.core.selectedComponents[index].name)
- componentList.setCurrentRow(index)
-
- # connect to signal that adds an asterisk when modified
- self.core.selectedComponents[index].modified.connect(
- self.updateComponentTitle)
-
- self.pages.insert(index, self.core.selectedComponents[index].page)
- stackedWidget.insertWidget(index, self.pages[index])
- stackedWidget.setCurrentIndex(index)
-
- return index
-
- 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()
-
- @disableWhenEncoding
- def moveComponent(self, change):
- '''Moves a component relatively from its current position'''
- componentList = self.window.listWidget_componentList
- if change == 'top':
- change = -componentList.currentRow()
- elif change == 'bottom':
- change = len(componentList)-componentList.currentRow()-1
- stackedWidget = self.window.stackedWidget
-
- row = componentList.currentRow()
- newRow = row + change
- if newRow > -1 and newRow < componentList.count():
- self.core.moveComponent(row, newRow)
-
- # 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):
- '''
- Given a QPos, returns the component index under the mouse cursor
- or -1 if no component is there.
- '''
- componentList = self.window.listWidget_componentList
-
- modelIndexes = [
- componentList.model().index(i)
- for i in range(componentList.count())
- ]
- rects = [
- componentList.visualRect(modelIndex)
- for modelIndex in modelIndexes
- ]
- mousePos = [rect.contains(position) for rect in rects]
- if not any(mousePos):
- # Not clicking a component
- mousePos = -1
- else:
- mousePos = mousePos.index(True)
- log.debug('Click component list row %s' % mousePos)
- return mousePos
-
- @disableWhenEncoding
- def dragComponent(self, event):
- '''Used as Qt drop event for the component listwidget'''
- componentList = self.window.listWidget_componentList
- mousePos = self.getComponentListMousePos(event.pos())
- if mousePos > -1:
- change = (componentList.currentRow() - mousePos) * -1
- else:
- change = (componentList.count() - componentList.currentRow() - 1)
- self.moveComponent(change)
-
- def changeComponentWidget(self):
- selected = self.window.listWidget_componentList.selectedItems()
- if selected:
- index = self.window.listWidget_componentList.row(selected[0])
- self.window.stackedWidget.setCurrentIndex(index)
-
- def openPresetManager(self):
- '''Preset manager for importing, exporting, renaming, deleting'''
- self.presetManager.show()
-
- def clear(self):
- '''Get a blank slate'''
- self.core.clearComponents()
- self.window.listWidget_componentList.clear()
- for widget in self.pages:
- self.window.stackedWidget.removeWidget(widget)
- self.pages = []
- for field in (
- self.window.lineEdit_audioFile,
- self.window.lineEdit_outputFile
- ):
- field.blockSignals(True)
- field.setText('')
- field.blockSignals(False)
- self.progressBarUpdated(0)
- self.progressBarSetText('')
-
- @disableWhenEncoding
- def createNewProject(self, prompt=True):
- if prompt:
- self.openSaveChangesDialog('starting a new project')
-
- self.clear()
- self.currentProject = None
- self.settings.setValue("currentProject", None)
- self.drawPreview(True)
-
- def saveCurrentProject(self):
- if self.currentProject:
- self.core.createProjectFile(self.currentProject, self.window)
- try:
- os.remove(self.autosavePath)
- except FileNotFoundError:
- pass
- self.updateWindowTitle()
- else:
- self.openSaveProjectDialog()
-
- def openSaveChangesDialog(self, phrase):
- success = True
- if self.autosaveExists(identical=False):
- ch = self.showMessage(
- msg="You have unsaved changes in project '%s'. "
- "Save before %s?" % (
- os.path.basename(self.currentProject)[:-4],
- phrase
- ),
- showCancel=True)
- if ch:
- success = self.saveProjectChanges()
-
- if success and os.path.exists(self.autosavePath):
- os.remove(self.autosavePath)
-
- def openSaveProjectDialog(self):
- filename, _ = QtWidgets.QFileDialog.getSaveFileName(
- self.window, "Create Project File",
- self.settings.value("projectDir"),
- "Project Files (*.avp)")
- if not filename:
- return
- if not filename.endswith(".avp"):
- filename += '.avp'
- self.settings.setValue("projectDir", os.path.dirname(filename))
- self.settings.setValue("currentProject", filename)
- self.currentProject = filename
- self.core.createProjectFile(filename, self.window)
- self.updateWindowTitle()
-
- @disableWhenEncoding
- def openOpenProjectDialog(self):
- filename, _ = QtWidgets.QFileDialog.getOpenFileName(
- self.window, "Open Project File",
- self.settings.value("projectDir"),
- "Project Files (*.avp)")
- self.openProject(filename)
-
- def openProject(self, filepath, prompt=True):
- if not filepath or not os.path.exists(filepath) \
- or not filepath.endswith('.avp'):
- return
-
- self.clear()
- # ask to save any changes that are about to get deleted
- if prompt:
- self.openSaveChangesDialog('opening another project')
-
- self.currentProject = filepath
- self.settings.setValue("currentProject", filepath)
- self.settings.setValue("projectDir", os.path.dirname(filepath))
- # actually load the project using core method
- self.core.openProject(self, filepath)
- self.drawPreview(autosave=False)
- self.updateWindowTitle()
-
- def showMessage(self, **kwargs):
- parent = kwargs['parent'] if 'parent' in kwargs else self.window
- msg = QtWidgets.QMessageBox(parent)
- msg.setModal(True)
- msg.setText(kwargs['msg'])
- msg.setIcon(
- eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
- if 'icon' in kwargs else QtWidgets.QMessageBox.Information
- )
- msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
- if 'showCancel'in kwargs and kwargs['showCancel']:
- msg.setStandardButtons(
- QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
- else:
- msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
- ch = msg.exec_()
- if ch == 1024:
- return True
- return False
-
- @disableWhenEncoding
- def componentContextMenu(self, QPos):
- '''Appears when right-clicking the component list'''
- componentList = self.window.listWidget_componentList
- self.menu = QMenu()
- parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
-
- index = self.getComponentListMousePos(QPos)
- if index > -1:
- # Show preset menu if clicking a component
- self.presetManager.findPresets()
- menuItem = self.menu.addAction("Save Preset")
- menuItem.triggered.connect(
- self.presetManager.openSavePresetDialog
- )
-
- # submenu for opening presets
- try:
- presets = self.presetManager.presets[
- str(self.core.selectedComponents[index])
- ]
- self.presetSubmenu = QMenu("Open Preset")
- self.menu.addMenu(self.presetSubmenu)
-
- for version, presetName in presets:
- menuItem = self.presetSubmenu.addAction(presetName)
- menuItem.triggered.connect(
- lambda _, presetName=presetName:
- self.presetManager.openPreset(presetName)
- )
- except KeyError:
- pass
-
- if self.core.selectedComponents[index].currentPreset:
- menuItem = self.menu.addAction("Clear Preset")
- menuItem.triggered.connect(
- self.presetManager.clearPreset
- )
- self.menu.addSeparator()
-
- # "Add Component" submenu
- self.submenu = QMenu("Add")
- self.menu.addMenu(self.submenu)
- insertCompAtTop = self.settings.value("pref_insertCompAtTop")
- for i, comp in enumerate(self.core.modules):
- menuItem = self.submenu.addAction(comp.Component.name)
- menuItem.triggered.connect(
- lambda _, item=i: self.core.insertComponent(
- 0 if insertCompAtTop else index, item, self
- )
- )
-
- self.menu.move(parentPosition + QPos)
- self.menu.show()
-
- def eventFilter(self, object, event):
- if event.type() == QtCore.QEvent.WindowActivate \
- or event.type() == QtCore.QEvent.FocusIn:
- Core.windowHasFocus = True
- elif event.type() == QtCore.QEvent.WindowDeactivate \
- or event.type() == QtCore.QEvent.FocusOut:
- Core.windowHasFocus = False
- return False
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
deleted file mode 100644
index b43d375..0000000
--- a/src/mainwindow.ui
+++ /dev/null
@@ -1,828 +0,0 @@
-
-
- MainWindow
-
-
-
- 0
- 0
- 1008
- 575
-
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
- Qt::StrongFocus
-
-
- MainWindow
-
-
-
-
- 0
- 0
-
-
-
- false
-
-
-
- 9
-
-
- 0
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 0
- 360
-
-
-
-
- -
-
-
- QLayout::SetDefaultConstraint
-
-
- 0
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 420
- 0
-
-
-
-
-
-
- -
-
-
- QLayout::SetMinimumSize
-
-
- 3
-
-
-
-
-
- QLayout::SetMinimumSize
-
-
- 3
-
-
-
-
-
- QLayout::SetMinimumSize
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 140
- 20
-
-
-
-
- -
-
-
- Projects
-
-
-
- -
-
-
- Presets
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 20
- 2
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 16777215
-
-
-
- true
-
-
- QFrame::StyledPanel
-
-
- QFrame::Sunken
-
-
- 1
-
-
- true
-
-
- true
-
-
- false
-
-
- QAbstractItemView::InternalMove
-
-
- Qt::MoveAction
-
-
-
- -
-
-
-
-
-
- Add
-
-
-
- -
-
-
- Remove
-
-
-
- -
-
-
- Up
-
-
-
- -
-
-
- Down
-
-
-
-
-
-
-
- -
-
-
- 4
-
-
- 2
-
-
-
-
-
-
-
- -
-
-
- QLayout::SetFixedSize
-
-
- 4
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 500
- 0
-
-
-
-
- 16777215
- 180
-
-
-
- QTabWidget::North
-
-
- QTabWidget::Rounded
-
-
- 0
-
-
-
- Export Video
-
-
-
- 10
-
-
-
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
-
- 80
- 16777215
-
-
-
-
- 80
- 0
-
-
-
- Audio File
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
-
- 0
- 0
-
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
- ...
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
-
- 0
- 0
-
-
-
- Output File
-
-
- Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
-
- 16777215
- 28
-
-
-
- ...
-
-
-
-
-
-
-
- -
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- 24
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 10
- 20
-
-
-
-
- -
-
-
- Create Video
-
-
-
- -
-
-
- false
-
-
- Cancel
-
-
-
-
-
- -
-
-
-
-
-
- true
-
-
- Qt::AlignCenter
-
-
- -1
-
-
-
-
-
-
- progressLabel
-
-
-
- Encoder Settings
-
-
-
- 10
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Container
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Minimum
-
-
-
- 5
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Resolution
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Video Codec
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 5
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Video Bitrate (Kbps)
-
-
-
- -
-
-
- 99999
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 85
- 0
-
-
-
- Audio Codec
-
-
-
- -
-
-
-
- 150
- 0
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 5
- 10
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Audio Bitrate (Kbps)
-
-
-
- -
-
-
- 9999
-
-
-
-
-
-
-
-
-
- -
-
-
- QLayout::SetDefaultConstraint
-
-
-
-
-
- Qt::Horizontal
-
-
- QSizePolicy::MinimumExpanding
-
-
-
- 500
- 0
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 0
- 180
-
-
-
-
- 16777215
- 180
-
-
-
- -1
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/presetmanager.py b/src/presetmanager.py
deleted file mode 100644
index b1eeb34..0000000
--- a/src/presetmanager.py
+++ /dev/null
@@ -1,358 +0,0 @@
-'''
- 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))
diff --git a/src/presetmanager.ui b/src/presetmanager.ui
deleted file mode 100644
index 5257b1c..0000000
--- a/src/presetmanager.ui
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
- presetmanager
-
-
- Qt::NonModal
-
-
- true
-
-
-
- 0
- 0
- 497
- 377
-
-
-
- Preset Manager
-
-
- -
-
-
-
-
-
-
-
-
- Filter by name
-
-
-
- -
-
-
-
- 200
- 0
-
-
-
-
-
-
- -
-
-
-
-
-
-
- 0
- 0
-
-
-
- true
-
-
-
-
-
- -
-
-
- QLayout::SetMinimumSize
-
-
-
-
-
- Import
-
-
-
- -
-
-
- Export
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- true
-
-
- Rename
-
-
-
- -
-
-
- Delete
-
-
-
-
-
- -
-
-
-
-
-
- <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html>
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Close
-
-
-
-
-
-
-
-
-
-
diff --git a/src/preview_thread.py b/src/preview_thread.py
deleted file mode 100644
index 9615884..0000000
--- a/src/preview_thread.py
+++ /dev/null
@@ -1,90 +0,0 @@
-'''
- Thread that runs to create QImages for MainWindow's preview label.
- Processes a queue of component lists.
-'''
-from PyQt5 import QtCore, QtGui, uic
-from PyQt5.QtCore import pyqtSignal, pyqtSlot
-from PIL import Image
-from PIL.ImageQt import ImageQt
-from queue import Queue, Empty
-import os
-import logging
-
-from toolkit.frame import Checkerboard
-from toolkit import disableWhenOpeningProject
-
-
-log = logging.getLogger("AVP.PreviewThread")
-
-
-class Worker(QtCore.QObject):
-
- imageCreated = pyqtSignal(QtGui.QImage)
- error = pyqtSignal(str)
-
- def __init__(self, parent=None, queue=None):
- QtCore.QObject.__init__(self)
- parent.newTask.connect(self.createPreviewImage)
- parent.processTask.connect(self.process)
- self.parent = parent
- self.core = parent.core
- self.settings = parent.settings
- self.queue = queue
-
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- self.background = Checkerboard(width, height)
-
- @disableWhenOpeningProject
- @pyqtSlot(list)
- def createPreviewImage(self, components):
- dic = {
- "components": components,
- }
- self.queue.put(dic)
-
- @pyqtSlot()
- def process(self):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
- try:
- nextPreviewInformation = self.queue.get(block=False)
- while self.queue.qsize() >= 2:
- try:
- self.queue.get(block=False)
- except Empty:
- continue
- if self.background.width != width \
- or self.background.height != height:
- self.background = Checkerboard(width, height)
-
- frame = self.background.copy()
- log.debug('Creating new preview frame')
- components = nextPreviewInformation["components"]
- for component in reversed(components):
- try:
- component.lockSize(width, height)
- newFrame = component.previewRender()
- component.unlockSize()
- frame = Image.alpha_composite(
- frame, newFrame
- )
-
- except ValueError as e:
- errMsg = "Bad frame returned by %s's preview renderer. " \
- "%s. New frame size was %s*%s; should be %s*%s." % (
- str(component), str(e).capitalize(),
- newFrame.width, newFrame.height,
- width, height
- )
- log.critical(errMsg)
- self.error.emit(errMsg)
- break
- except RuntimeError as e:
- log.error(str(e))
- else:
- self.frame = ImageQt(frame)
- self.imageCreated.emit(QtGui.QImage(self.frame))
-
- except Empty:
- True
diff --git a/src/preview_win.py b/src/preview_win.py
deleted file mode 100644
index 40c19c6..0000000
--- a/src/preview_win.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from PyQt5 import QtCore, QtGui, QtWidgets
-import logging
-
-
-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')
-
- def __init__(self, parent, img):
- super(PreviewWindow, self).__init__()
- self.parent = parent
- self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
- self.pixmap = QtGui.QPixmap(img)
-
- def paintEvent(self, event):
- size = self.size()
- painter = QtGui.QPainter(self)
- point = QtCore.QPoint(0, 0)
- scaledPix = self.pixmap.scaled(
- size,
- QtCore.Qt.KeepAspectRatio,
- transformMode=QtCore.Qt.SmoothTransformation)
-
- # start painting the label from left upper corner
- point.setX((size.width() - scaledPix.width())/2)
- point.setY((size.height() - scaledPix.height())/2)
- painter.drawPixmap(point, scaledPix)
-
- def changePixmap(self, img):
- self.pixmap = QtGui.QPixmap(img)
- self.repaint()
-
- def mousePressEvent(self, event):
- if self.parent.encoding:
- return
-
- i = self.parent.window.listWidget_componentList.currentRow()
- if i >= 0:
- component = self.parent.core.selectedComponents[i]
- if not hasattr(component, 'previewClickEvent'):
- self.log.info('Ignored click event')
- return
- pos = (event.x(), event.y())
- size = (self.width(), self.height())
- butt = event.button()
- self.log.info('Click event for #%s: %s button %s' % (
- i, pos, butt))
- component.previewClickEvent(
- pos, size, butt
- )
- self.parent.core.updateComponent(i)
-
- @QtCore.pyqtSlot(str)
- def threadError(self, msg):
- self.parent.showMessage(
- msg=msg,
- icon='Critical',
- parent=self
- )
--
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')
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 a1d7cbb984f2a6c2ea976daa8914a2c9845ee21c Mon Sep 17 00:00:00 2001
From: tassaron
Date: Tue, 15 Aug 2017 22:20:25 -0400
Subject: undoable edits for normal component settings; TODO: merge small edits
---
src/background.png | Bin 45367 -> 0 bytes
src/component.py | 77 +++++++++++++++++++++++++++++++++++++++++-------
src/components/color.py | 3 --
src/components/color.ui | 6 ++++
src/components/text.py | 4 ---
src/components/text.ui | 6 ++++
src/core.py | 20 ++++++++-----
src/gui/background.png | Bin 0 -> 45367 bytes
src/gui/mainwindow.py | 34 +++++++++++++++------
src/toolkit/common.py | 12 ++++++++
src/toolkit/frame.py | 2 +-
11 files changed, 130 insertions(+), 34 deletions(-)
delete mode 100644 src/background.png
create mode 100644 src/gui/background.png
(limited to 'src')
diff --git a/src/background.png b/src/background.png
deleted file mode 100644
index fb58593..0000000
Binary files a/src/background.png and /dev/null differ
diff --git a/src/component.py b/src/component.py
index 0e5144c..dcba082 100644
--- a/src/component.py
+++ b/src/component.py
@@ -12,7 +12,7 @@ import logging
from toolkit.frame import BlankFrame
from toolkit import (
- getWidgetValue, setWidgetValue, connectWidget, rgbFromString
+ getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
)
@@ -305,14 +305,46 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def update(self):
'''
- Reads all tracked widget values into instance attributes
- and tells the MainWindow that the component was modified.
- Call super() at the END if you need to subclass this.
+ A component update triggered by the user changing a widget value
+ Call super() at the END when subclassing this.
'''
- for attr, widget in self._trackedWidgets.items():
+ oldWidgetVals = {
+ attr: 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()
+ }
+ if any([val != oldWidgetVals[attr]
+ for attr, val in newWidgetVals.items()
+ ]):
+ action = ComponentUpdate(self, oldWidgetVals, newWidgetVals)
+ self.parent.undoStack.push(action)
+
+ def _update(self):
+ '''An internal component update that is not undoable'''
+
+ 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 preset 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
- rgbTuple = rgbFromString(widget.text())
+ if type(val) is tuple:
+ rgbTuple = val
+ else:
+ rgbTuple = rgbFromString(val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
@@ -322,12 +354,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
elif attr in self._relativeWidgets:
# Relative widgets: number scales to fit export resolution
self.updateRelativeWidget(attr)
- setattr(self, attr, self._trackedWidgets[attr].value())
+ setattr(self, attr, val)
else:
# Normal tracked widget
- setattr(self, attr, getWidgetValue(widget))
- self.sendUpdateSignal()
+ setattr(self, attr, val)
def sendUpdateSignal(self):
if not self.core.openingProject:
@@ -541,7 +572,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
-
def updateRelativeWidget(self, attr):
try:
oldUserValue = getattr(self, attr)
@@ -628,3 +658,30 @@ class ComponentError(RuntimeError):
super().__init__(string)
caller.lockError(string)
caller._error.emit(string, detail)
+
+
+class ComponentUpdate(QtWidgets.QUndoCommand):
+ '''Command object for making a component action undoable'''
+ def __init__(self, parent, oldWidgetVals, newWidgetVals):
+ super().__init__(
+ 'Changed %s component #%s' % (
+ parent.name, parent.compPos
+ )
+ )
+ self.parent = parent
+ self.oldWidgetVals = oldWidgetVals
+ self.newWidgetVals = newWidgetVals
+
+ def redo(self):
+ self.parent.setAttrs(self.newWidgetVals)
+ self.parent.sendUpdateSignal()
+
+ def undo(self):
+ self.parent.setAttrs(self.oldWidgetVals)
+ with blockSignals(self.parent):
+ for attr, widget in self.parent._trackedWidgets.items():
+ val = self.oldWidgetVals[attr]
+ if attr in self.parent._colorWidgets:
+ val = '%s,%s,%s' % val
+ setWidgetValue(widget, val)
+ self.parent.sendUpdateSignal()
diff --git a/src/components/color.py b/src/components/color.py
index 5d1233e..d09cee8 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -17,9 +17,6 @@ class Component(Component):
self.y = 0
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
self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True)
diff --git a/src/components/color.ui b/src/components/color.ui
index a9dacea..1865e60 100644
--- a/src/components/color.ui
+++ b/src/components/color.ui
@@ -73,6 +73,9 @@
0
+
+ 0,0,0
+
12
@@ -146,6 +149,9 @@
0
+
+ 133,133,133
+
12
diff --git a/src/components/text.py b/src/components/text.py
index 4d4f5d3..d3afd5c 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -13,8 +13,6 @@ class Component(Component):
def widget(self, *args):
super().widget(*args)
- self.textColor = (255, 255, 255)
- self.strokeColor = (0, 0, 0)
self.title = 'Text'
self.alignment = 1
self.titleFont = QFont()
@@ -25,8 +23,6 @@ class Component(Component):
self.page.comboBox_textAlign.addItem("Right")
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.lineEdit_title.setText(self.title)
diff --git a/src/components/text.ui b/src/components/text.ui
index 13d3467..b62e0ed 100644
--- a/src/components/text.ui
+++ b/src/components/text.ui
@@ -427,6 +427,9 @@
Qt::NoFocus
+
+ 255,255,255
+
-
@@ -485,6 +488,9 @@
Qt::NoFocus
+
+ 0,0,0
+
-
diff --git a/src/core.py b/src/core.py
index 20b9c1d..cee0f56 100644
--- a/src/core.py
+++ b/src/core.py
@@ -94,12 +94,11 @@ class Core:
compPos,
component
)
- self.componentListChanged()
- if moduleIndex > -1:
- self.updateComponent(compPos)
-
if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos)
+
+ self.componentListChanged()
+ self.updateComponent(compPos)
return compPos
def moveComponent(self, startI, endI):
@@ -119,7 +118,7 @@ class Core:
def updateComponent(self, i):
log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
- self.selectedComponents[i].update()
+ self.selectedComponents[i]._update()
def moduleIndexFor(self, compName):
try:
@@ -540,6 +539,7 @@ class Core:
"projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True,
"pref_genericPreview": True,
+ "pref_undoLimit": 10,
}
for parm, value in cls.defaultSettings.items():
@@ -552,8 +552,14 @@ class Core:
if not key.startswith('pref_'):
continue
val = cls.settings.value(key)
- if val in ('true', 'false'):
- cls.settings.setValue(key, True if val == 'true' else False)
+ try:
+ val = int(val)
+ except ValueError:
+ if val == 'true':
+ val = True
+ elif val == 'false':
+ val = False
+ cls.settings.setValue(key, val)
@staticmethod
def makeLogger():
diff --git a/src/gui/background.png b/src/gui/background.png
new file mode 100644
index 0000000..fb58593
Binary files /dev/null and b/src/gui/background.png differ
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 2edb750..47111a0 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -42,13 +42,22 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
+ log.debug(
+ 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
self.window = window
self.core = Core()
Core.mode = 'GUI'
- log.debug(
- 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+ # Find settings created by Core object
+ self.dataDir = Core.dataDir
+ self.presetDir = Core.presetDir
+ self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
+ self.settings = Core.settings
+
+ # Create stack of undoable user actions
self.undoStack = QtWidgets.QUndoStack(self)
+ undoLimit = self.settings.value("pref_undoLimit")
+ self.undoStack.setUndoLimit(undoLimit)
# widgets of component settings
self.pages = []
@@ -58,12 +67,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosaveCooldown = 0.2
self.encoding = False
- # Find settings created by Core object
- self.dataDir = Core.dataDir
- self.presetDir = Core.presetDir
- self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = Core.settings
-
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
@@ -302,6 +305,7 @@ 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)
@@ -353,6 +357,9 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
)
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+U", self.window, self.showUndoStack
+ )
@QtCore.pyqtSlot()
def cleanUp(self, *args):
@@ -658,6 +665,14 @@ class MainWindow(QtWidgets.QMainWindow):
def showPreviewImage(self, 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):
from textwrap import wrap
from toolkit.ffmpeg import createFfmpegCommand
@@ -784,6 +799,7 @@ class MainWindow(QtWidgets.QMainWindow):
field.blockSignals(False)
self.progressBarUpdated(0)
self.progressBarSetText('')
+ self.undoStack.clear()
@disableWhenEncoding
def createNewProject(self, prompt=True):
@@ -847,7 +863,7 @@ class MainWindow(QtWidgets.QMainWindow):
def openProject(self, filepath, prompt=True):
if not filepath or not os.path.exists(filepath) \
- or not filepath.endswith('.avp'):
+ or not filepath.endswith('.avp'):
return
self.clear()
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index eba57d9..51ad023 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -9,6 +9,18 @@ import subprocess
from collections import OrderedDict
+class blockSignals:
+ '''A context manager to temporarily block a Qt widget from updating'''
+ def __init__(self, widget):
+ self.widget = widget
+
+ def __enter__(self):
+ self.widget.blockSignals(True)
+
+ def __exit__(self, *args):
+ self.widget.blockSignals(False)
+
+
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name])
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index ad8537c..2104978 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -98,7 +98,7 @@ def Checkerboard(width, height):
log.debug('Creating new %s*%s checkerboard' % (width, height))
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
- os.path.join(core.Core.wd, "background.png")),
+ os.path.join(core.Core.wd, 'gui', "background.png")),
(0, 0)
)
image = image.resize((width, height))
--
cgit v1.2.3
From f65ced2853a07b312516bcb729cc28509f524077 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Wed, 16 Aug 2017 20:44:37 -0400
Subject: merge consecutive actions on the same widget type
---
src/component.py | 54 ++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 44 insertions(+), 10 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index dcba082..488b92a 100644
--- a/src/component.py
+++ b/src/component.py
@@ -317,10 +317,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if attr not in self._colorWidgets else rgbFromString(widget.text())
for attr, widget in self._trackedWidgets.items()
}
- if any([val != oldWidgetVals[attr]
- for attr, val in newWidgetVals.items()
- ]):
- action = ComponentUpdate(self, oldWidgetVals, newWidgetVals)
+ 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 _update(self):
@@ -662,25 +666,55 @@ class ComponentError(RuntimeError):
class ComponentUpdate(QtWidgets.QUndoCommand):
'''Command object for making a component action undoable'''
- def __init__(self, parent, oldWidgetVals, newWidgetVals):
+ def __init__(self, parent, oldWidgetVals, modifiedVals):
super().__init__(
'Changed %s component #%s' % (
parent.name, parent.compPos
)
)
self.parent = parent
- self.oldWidgetVals = oldWidgetVals
- self.newWidgetVals = newWidgetVals
+ self.oldWidgetVals = {
+ attr: val
+ for attr, val in oldWidgetVals.items()
+ if attr in modifiedVals
+ }
+ self.modifiedVals = modifiedVals
+
+ # Determine if this update is mergeable
+ self.id_ = -1
+ if len(self.modifiedVals) == 1:
+ attr, val = self.modifiedVals.popitem()
+ widget = self.parent._trackedWidgets[attr]
+ if type(widget) is QtWidgets.QLineEdit:
+ self.id_ = 10
+ elif type(widget) is QtWidgets.QSpinBox \
+ or type(widget) is QtWidgets.QDoubleSpinBox:
+ self.id_ = 20
+ 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 redo(self):
- self.parent.setAttrs(self.newWidgetVals)
+ self.parent.setAttrs(self.modifiedVals)
self.parent.sendUpdateSignal()
def undo(self):
self.parent.setAttrs(self.oldWidgetVals)
with blockSignals(self.parent):
- for attr, widget in self.parent._trackedWidgets.items():
- val = self.oldWidgetVals[attr]
+ 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)
--
cgit v1.2.3
From ddb04f3a2fe6454a9c98bba39d07a12bd6a91b45 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Wed, 16 Aug 2017 21:02:53 -0400
Subject: undo merge IDs given per attr instead of widget type
---
src/component.py | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 488b92a..b883627 100644
--- a/src/component.py
+++ b/src/component.py
@@ -684,12 +684,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
self.id_ = -1
if len(self.modifiedVals) == 1:
attr, val = self.modifiedVals.popitem()
- widget = self.parent._trackedWidgets[attr]
- if type(widget) is QtWidgets.QLineEdit:
- self.id_ = 10
- elif type(widget) is QtWidgets.QSpinBox \
- or type(widget) is QtWidgets.QDoubleSpinBox:
- self.id_ = 20
+ self.id_ = sum([ord(letter) for letter in attr[:14]])
self.modifiedVals[attr] = val
else:
log.warning(
--
cgit v1.2.3
From f66ec40ba6e9c4062d1e41894e0a88f713add96d Mon Sep 17 00:00:00 2001
From: tassaron
Date: Wed, 16 Aug 2017 22:17:12 -0400
Subject: undoable component movement
---
src/core.py | 1 +
src/gui/actions.py | 39 +++++++++++++++++++++++++++++++++++++++
src/gui/mainwindow.py | 18 +++++-------------
3 files changed, 45 insertions(+), 13 deletions(-)
(limited to 'src')
diff --git a/src/core.py b/src/core.py
index cee0f56..14517b0 100644
--- a/src/core.py
+++ b/src/core.py
@@ -73,6 +73,7 @@ class Core:
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
+
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
diff --git a/src/gui/actions.py b/src/gui/actions.py
index 5cf64e1..5a0869d 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -35,3 +35,42 @@ class RemoveComponent(QUndoCommand):
)
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)
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 47111a0..26464a9 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -716,27 +716,19 @@ class MainWindow(QtWidgets.QMainWindow):
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
+ tag = change
if change == 'top':
change = -componentList.currentRow()
elif change == 'bottom':
change = len(componentList)-componentList.currentRow()-1
- stackedWidget = self.window.stackedWidget
+ else:
+ tag = 'down' if change == 1 else 'up'
row = componentList.currentRow()
newRow = row + change
if newRow > -1 and newRow < componentList.count():
- self.core.moveComponent(row, newRow)
-
- # 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)
+ action = MoveComponent(self, row, newRow, tag)
+ self.undoStack.push(action)
def getComponentListMousePos(self, position):
'''
--
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')
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 43ea3bfd733f63e5b22d2f1eb7ef7c8ad2cc97c9 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Thu, 17 Aug 2017 15:12:22 -0400
Subject: component updateWrapper and more obvious method names
---
src/component.py | 208 +++++++++++++++++++++++++++++++------------------------
src/core.py | 7 +-
2 files changed, 122 insertions(+), 93 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index f0a8c6b..1fe9237 100644
--- a/src/component.py
+++ b/src/component.py
@@ -99,7 +99,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self)
return errorWrapper
- def presetWrapper(func):
+ def loadPresetWrapper(func):
'''Wraps loadPreset to handle the self.openingPreset boolean'''
class openingPreset:
def __init__(self, comp):
@@ -116,6 +116,36 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self, *args)
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()
+ '''
+ class wrap:
+ def __init__(self, comp, auto):
+ self.comp = comp
+ self.auto = auto
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, *args):
+ if self.auto or self.comp.openingPreset \
+ or not hasattr(self.comp.parent, 'undoStack'):
+ self.comp._autoUpdate()
+ else:
+ self.comp._userUpdate()
+
+ def updateWrapper(self, **kwargs):
+ auto = False
+ if 'auto' in kwargs:
+ auto = kwargs['auto']
+
+ with wrap(self, auto):
+ return func(self)
+ return updateWrapper
+
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
# Use module name as ui filename by default
@@ -128,37 +158,32 @@ class ComponentMetaclass(type(QtCore.QObject)):
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
- 'frameRender', 'command', 'loadPreset'
+ 'frameRender', 'command',
+ 'loadPreset', 'update'
)
# Auto-decorate methods
for key in decorate:
if key not in attrs:
continue
-
if key in ('names'):
attrs[key] = classmethod(attrs[key])
-
- if key in ('audio'):
+ elif key in ('audio'):
attrs[key] = property(attrs[key])
-
- if key == 'command':
+ elif key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
-
- if key in ('previewRender', 'frameRender'):
+ elif key in ('previewRender', 'frameRender'):
attrs[key] = cls.renderWrapper(attrs[key])
-
- if key == 'preFrameRender':
+ elif key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key])
-
- if key == 'properties':
+ elif key == 'properties':
attrs[key] = cls.propertiesWrapper(attrs[key])
-
- if key == 'error':
+ elif key == 'error':
attrs[key] = cls.errorWrapper(attrs[key])
-
- if key == 'loadPreset':
- attrs[key] = cls.presetWrapper(attrs[key])
+ elif key == 'loadPreset':
+ attrs[key] = cls.loadPresetWrapper(attrs[key])
+ elif key == 'update':
+ attrs[key] = cls.updateWrapper(attrs[key])
# Turn version string into a number
try:
@@ -229,10 +254,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
except Exception as e:
preset = '%s occurred while saving preset' % str(e)
- 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
+ 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
+ )
)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@@ -329,74 +356,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def update(self):
'''
- A component update triggered by the user changing a widget value
- Call super() at the END when subclassing this.
+ Starting point for a component update. A subclass should override
+ this method, and the base class will then magically insert a call
+ to either _autoUpdate() or _userUpdate() at the end.
'''
- if self.openingPreset or not hasattr(self.parent, 'undoStack'):
- return self._update()
-
- oldWidgetVals = {
- attr: 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 _update(self):
- '''A component update that is not undoable'''
-
- 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 preset 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
- 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)
-
- 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 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):
'''
@@ -464,6 +427,69 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ def _userUpdate(self):
+ '''An undoable component update triggered by the user'''
+ oldWidgetVals = {
+ attr: 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):
+ '''An internal component update that is not undoable'''
+ 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 preset 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
+ 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)
+
+ 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 _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):
'''
@@ -730,7 +756,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
def redo(self):
self.parent.setAttrs(self.modifiedVals)
- self.parent.sendUpdateSignal()
+ self.parent._sendUpdateSignal()
def undo(self):
self.parent.setAttrs(self.oldWidgetVals)
@@ -740,4 +766,4 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
if attr in self.parent._colorWidgets:
val = '%s,%s,%s' % val
setWidgetValue(widget, val)
- self.parent.sendUpdateSignal()
+ self.parent._sendUpdateSignal()
diff --git a/src/core.py b/src/core.py
index 14517b0..7609698 100644
--- a/src/core.py
+++ b/src/core.py
@@ -83,6 +83,8 @@ class Core:
)
# 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(
@@ -118,8 +120,9 @@ class Core:
self.componentListChanged()
def updateComponent(self, i):
- log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
- self.selectedComponents[i]._update()
+ log.debug('Auto-updating %s #%s' % (
+ self.selectedComponents[i], str(i)))
+ self.selectedComponents[i].update(auto=True)
def moduleIndexFor(self, compName):
try:
--
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')
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')
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')
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
From be9eb9077b2234e6d91c78d70bb8e1d8347b03aa Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 20 Aug 2017 17:47:00 -0400
Subject: relative widgets scale properly when undoing at different resolutions
---
src/component.py | 81 +++++++++++++++++++++++++++++++++--------------
src/components/life.py | 2 +-
src/gui/mainwindow.py | 7 ++--
src/gui/preview_thread.py | 6 ++--
src/toolkit/common.py | 1 +
5 files changed, 68 insertions(+), 29 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 992a82e..0ff2fbd 100644
--- a/src/component.py
+++ b/src/component.py
@@ -40,7 +40,8 @@ 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],
@@ -289,7 +290,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
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
@@ -386,7 +386,8 @@ 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)
@@ -530,6 +531,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
else:
# Normal tracked widget
setattr(self, attr, val)
+ log.verbose('Setting %s self.%s to %s' % (self.name, attr, val))
def setWidgetValues(self, attrDict):
'''
@@ -669,12 +671,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def relativeWidgetAxis(func):
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:
axis = self.width
- if 'height' in attr.lower() \
- or 'ypos' in attr.lower() or attr == 'y':
+ if hasVerticalWords(attr):
axis = self.height
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 relativeWidgetAxis
@@ -682,7 +694,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._relativeValues[attr]
- return math.ceil(kwargs['axis'] * val)
+ 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
def floatValForAttr(self, attr, val=None, **kwargs):
@@ -693,7 +710,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal)
- with blockSignals(self._allWidgets):
+ with blockSignals(self._trackedWidgets[attr]):
self._trackedWidgets[attr].setValue(pixelVal)
self.update(auto=True)
@@ -707,15 +724,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
log.verbose('Using nonstandard oldAttr for %s', attr)
return self.oldAttrs[attr]
else:
- return getattr(self, attr)
+ try:
+ return getattr(self, attr)
+ except AttributeError:
+ log.info('Using visible values instead of attrs')
+ return self._trackedWidgets[attr].value()
def updateRelativeWidget(self, attr):
'''Called by _preUpdate() for each relativeWidget before each update'''
- try:
- oldUserValue = self.getOldAttr(attr)
- except (AttributeError, KeyError):
- log.info('Using visible values as basis for relative widgets')
- oldUserValue = self._trackedWidgets[attr].value()
+ oldUserValue = self.getOldAttr(attr)
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
@@ -808,17 +825,25 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
)
)
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 = 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.redoRelativeWidgetVals = {
+ self.relativeWidgetValsAfterUndo = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
@@ -843,17 +868,28 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
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.debug('Redoing component update')
- self.parent.setWidgetValues(self.modifiedVals)
- self.parent.setAttrs(self.modifiedVals)
- if self.undone:
- self.parent.oldAttrs = self.redoRelativeWidgetVals
+ self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
+ self.setWidgetValues(self.modifiedVals)
self.parent.update(auto=True)
self.parent.oldAttrs = None
else:
- self.undoRelativeWidgetVals = {
+ self.parent.setAttrs(self.modifiedVals)
+ self.relativeWidgetValsAfterRedo = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
@@ -862,8 +898,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
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)
+ self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
+ self.setWidgetValues(self.oldWidgetVals)
self.parent.update(auto=True)
self.parent.oldAttrs = None
diff --git a/src/components/life.py b/src/components/life.py
index 76d2c5f..5d00987 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -70,7 +70,7 @@ class Component(Component):
elif d == 3:
newGrid = newGrid(1, 0)
self.startingGrid = newGrid
- self.sendUpdateSignal()
+ self._sendUpdateSignal()
def update(self):
self.updateGridSize()
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 833d2d1..2841896 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -88,10 +88,13 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
- 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.timeout.connect(self.processTask.emit)
- self.timer.start(500)
+ self.timer.start(timeout)
# Begin decorating the window and connecting events
self.window.installEventFilter(self)
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py
index 33a9e7a..d3e0581 100644
--- a/src/gui/preview_thread.py
+++ b/src/gui/preview_thread.py
@@ -45,8 +45,6 @@ class Worker(QtCore.QObject):
@pyqtSlot()
def process(self):
- width = int(self.settings.value('outputWidth'))
- height = int(self.settings.value('outputHeight'))
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
@@ -54,12 +52,14 @@ class Worker(QtCore.QObject):
self.queue.get(block=False)
except Empty:
continue
+ width = int(self.settings.value('outputWidth'))
+ height = int(self.settings.value('outputHeight'))
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
frame = self.background.copy()
- log.debug('Creating new preview frame')
+ log.info('Creating new preview frame')
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index 95aeab3..2e800eb 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -84,6 +84,7 @@ def appendUppercase(lst):
lst.append(form.upper())
return lst
+
def pipeWrapper(func):
'''A decorator to insert proper kwargs into Popen objects.'''
def pipeWrapper(commandList, **kwargs):
--
cgit v1.2.3
From 6bf8a553d6170e0ca6e7d2002e46ae327a6e5e81 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 20 Aug 2017 18:36:43 -0400
Subject: don't merge undos when setting text with a button
plus changes to life.py for pep8 compliance
---
src/component.py | 5 ++++-
src/components/image.py | 2 ++
src/components/life.py | 46 +++++++++++++++++++++++++++-------------------
src/components/sound.py | 2 ++
src/components/video.py | 2 ++
src/gui/actions.py | 1 -
6 files changed, 37 insertions(+), 21 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 0ff2fbd..1f55a19 100644
--- a/src/component.py
+++ b/src/component.py
@@ -285,6 +285,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# LOCKING VARIABLES
self.openingPreset = False
+ self.mergeUndo = True
self._lockedProperties = None
self._lockedError = None
self._lockedSize = None
@@ -587,10 +588,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if kwarg == 'colorWidgets':
def makeColorFunc(attr):
def pickColor_():
+ self.mergeUndo = False
self.pickColor(
self._trackedWidgets[attr],
self._colorWidgets[attr]
)
+ self.mergeUndo = True
return pickColor_
self._colorFuncs = {
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
@@ -850,7 +853,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
# Determine if this update is mergeable
self.id_ = -1
- if len(self.modifiedVals) == 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
diff --git a/src/components/image.py b/src/components/image.py
index c57b69c..dd363bf 100644
--- a/src/components/image.py
+++ b/src/components/image.py
@@ -83,7 +83,9 @@ class Component(Component):
"Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
self.page.lineEdit_image.setText(filename)
+ self.mergeUndo = True
def command(self, arg):
if '=' in arg:
diff --git a/src/components/life.py b/src/components/life.py
index 5d00987..d4a455d 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -35,6 +35,7 @@ class Component(Component):
self.page.toolButton_left,
self.page.toolButton_right,
)
+
def shiftFunc(i):
def shift():
self.shiftGrid(i)
@@ -52,7 +53,9 @@ class Component(Component):
"Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
self.page.lineEdit_image.setText(filename)
+ self.mergeUndo = True
def shiftGrid(self, d):
def newGrid(Xchange, Ychange):
@@ -197,7 +200,7 @@ class Component(Component):
# Circle
if shape == 'circle':
drawer.ellipse(outlineShape, fill=self.color)
- drawer.ellipse(smallerShape, fill=(0,0,0,0))
+ drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
# Lilypad
elif shape == 'lilypad':
@@ -207,9 +210,9 @@ class Component(Component):
elif shape == 'pac-man':
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
- hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
- tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
- qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
+ hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
+ tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
+ qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
# Path
if shape == 'path':
@@ -245,19 +248,19 @@ class Component(Component):
sect = (
(drawPtX, drawPtY + hY),
(drawPtX + self.pxWidth,
- drawPtY + self.pxHeight)
+ drawPtY + self.pxHeight)
)
elif direction == 'left':
sect = (
(drawPtX, drawPtY),
(drawPtX + hX,
- drawPtY + self.pxHeight)
+ drawPtY + self.pxHeight)
)
elif direction == 'right':
sect = (
(drawPtX + hX, drawPtY),
(drawPtX + self.pxWidth,
- drawPtY + self.pxHeight)
+ drawPtY + self.pxHeight)
)
drawer.rectangle(sect, fill=self.color)
@@ -287,20 +290,25 @@ class Component(Component):
# Peace
elif shape == 'peace':
- line = (
- (drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
+ line = ((
+ drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 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(smallerShape, fill=(0,0,0,0))
+ drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
drawer.rectangle(line, fill=self.color)
- slantLine = lambda difference: (
- ((drawPtX + difference),
- (drawPtY + self.pxHeight - qY)),
- ((drawPtX + hX),
- (drawPtY + hY)),
- )
+
+ def slantLine(difference):
+ return (
+ (drawPtX + difference),
+ (drawPtY + self.pxHeight - qY)
+ ),
+ (
+ (drawPtX + hX),
+ (drawPtY + hY)
+ )
+
drawer.line(
slantLine(qX),
fill=self.color,
@@ -337,13 +345,13 @@ class Component(Component):
for x in range(self.pxWidth, self.width, self.pxWidth):
drawer.rectangle(
((x, 0),
- (x + w, self.height)),
+ (x + w, self.height)),
fill=self.color,
)
for y in range(self.pxHeight, self.height, self.pxHeight):
drawer.rectangle(
((0, y),
- (self.width, y + h)),
+ (self.width, y + h)),
fill=self.color,
)
diff --git a/src/components/sound.py b/src/components/sound.py
index b86f40c..18d2a65 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -52,7 +52,9 @@ class Component(Component):
"Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
self.page.lineEdit_sound.setText(filename)
+ self.mergeUndo = True
def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg')
diff --git a/src/components/video.py b/src/components/video.py
index 9c0d608..e6486ea 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -117,7 +117,9 @@ class Component(Component):
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
+ self.mergeUndo = False
self.page.lineEdit_video.setText(filename)
+ self.mergeUndo = True
def getPreviewFrame(self, width, height):
if not self.videoPath or not os.path.exists(self.videoPath):
diff --git a/src/gui/actions.py b/src/gui/actions.py
index f101bd7..ebd9702 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -32,7 +32,6 @@ class AddComponent(QUndoCommand):
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)
--
cgit v1.2.3
From 9d9c4076ac1dfccdd1a753d137d87bcf5f179e3b Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 20 Aug 2017 22:04:57 -0400
Subject: added undo button to GUI
with icons that theoretically should look ok cross-platform
---
src/component.py | 2 +-
src/gui/actions.py | 14 +++++++-------
src/gui/mainwindow.py | 36 ++++++++++++++++++++++++++++++++++++
src/gui/mainwindow.ui | 7 +++++++
4 files changed, 51 insertions(+), 8 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 1f55a19..35fc717 100644
--- a/src/component.py
+++ b/src/component.py
@@ -823,7 +823,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
'''Command object for making a component action undoable'''
def __init__(self, parent, oldWidgetVals, modifiedVals):
super().__init__(
- 'Changed %s component #%s' % (
+ 'change %s component #%s' % (
parent.name, parent.compPos
)
)
diff --git a/src/gui/actions.py b/src/gui/actions.py
index ebd9702..8e867b9 100644
--- a/src/gui/actions.py
+++ b/src/gui/actions.py
@@ -15,7 +15,7 @@ from core import Core
class AddComponent(QUndoCommand):
def __init__(self, parent, compI, moduleI):
super().__init__(
- "New %s component" %
+ "create new %s component" %
parent.core.modules[moduleI].Component.name
)
self.parent = parent
@@ -39,7 +39,7 @@ class AddComponent(QUndoCommand):
class RemoveComponent(QUndoCommand):
def __init__(self, parent, selectedRows):
- super().__init__('Remove component')
+ super().__init__('remove component')
self.parent = parent
componentList = self.parent.window.listWidget_componentList
self.selectedRows = [
@@ -63,7 +63,7 @@ class RemoveComponent(QUndoCommand):
class MoveComponent(QUndoCommand):
def __init__(self, parent, row, newRow, tag):
- super().__init__("Move component %s" % tag)
+ super().__init__("move component %s" % tag)
self.parent = parent
self.row = row
self.newRow = newRow
@@ -107,7 +107,7 @@ class MoveComponent(QUndoCommand):
class ClearPreset(QUndoCommand):
def __init__(self, parent, compI):
- super().__init__("Clear preset")
+ super().__init__("clear preset")
self.parent = parent
self.compI = compI
self.component = self.parent.core.selectedComponents[compI]
@@ -125,7 +125,7 @@ class ClearPreset(QUndoCommand):
class OpenPreset(QUndoCommand):
def __init__(self, parent, presetName, compI):
- super().__init__("Open %s preset" % presetName)
+ super().__init__("open %s preset" % presetName)
self.parent = parent
self.presetName = presetName
self.compI = compI
@@ -145,7 +145,7 @@ class OpenPreset(QUndoCommand):
class RenamePreset(QUndoCommand):
def __init__(self, parent, path, oldName, newName):
- super().__init__('Rename preset')
+ super().__init__('rename preset')
self.parent = parent
self.path = path
self.oldName = oldName
@@ -167,7 +167,7 @@ class DeletePreset(QUndoCommand):
)
self.store = self.parent.core.getPreset(self.path)
self.presetName = self.store['preset']
- super().__init__('Delete %s preset (%s)' % (self.presetName, compName))
+ 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)
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 2841896..3b204b7 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -100,6 +100,42 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.installEventFilter(self)
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':
log.debug(
'Darwin detected: showing progress label below progress bar')
diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui
index b43d375..cd8454d 100644
--- a/src/gui/mainwindow.ui
+++ b/src/gui/mainwindow.ui
@@ -110,6 +110,13 @@
QLayout::SetMinimumSize
+
-
+
+
+ Undo
+
+
+
-
--
cgit v1.2.3
From 62e2ef18a3a31c15f88a96f07b2bc587808f5ad5 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 21 Aug 2017 07:06:12 -0400
Subject: potential dataDir paths in comments for future reference
---
src/core.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
(limited to 'src')
diff --git a/src/core.py b/src/core.py
index bfb8272..784f3b8 100644
--- a/src/core.py
+++ b/src/core.py
@@ -14,7 +14,7 @@ import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.VERBOSE
-FILE_LOGLVL = logging.DEBUG
+FILE_LOGLVL = logging.VERBOSE
class Core:
@@ -460,6 +460,9 @@ class Core:
dataDir = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.AppConfigLocation
)
+ # Windows: C:/Users//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:
encoderOptions = json.load(json_file)
--
cgit v1.2.3
From 85d3b779d07ad92b0f540ea52185777c3c3f5e48 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sat, 26 Aug 2017 21:23:44 -0400
Subject: fixed too-large Color sizes, fixed a redoing bug, rm pointless things
and now Ctrl+Alt+Shift+A gives a bunch of debug info
---
src/component.py | 30 +++++++++++------------
src/components/color.py | 2 +-
src/components/color.ui | 4 ++--
src/components/text.py | 13 ++++++----
src/core.py | 8 +++++--
src/gui/mainwindow.py | 63 +++++++++++++++++++++++++++++--------------------
src/gui/preview_win.py | 1 +
src/main.py | 5 ----
src/toolkit/ffmpeg.py | 2 +-
src/toolkit/frame.py | 3 ---
10 files changed, 72 insertions(+), 59 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 35fc717..de4b6a7 100644
--- a/src/component.py
+++ b/src/component.py
@@ -41,10 +41,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(self, *args, **kwargs):
try:
log.verbose(
- '### %s #%s renders%s frame %s###',
+ '### %s #%s renders a preview frame ###',
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:
@@ -198,8 +196,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
- 'frameRender', 'command',
- 'loadPreset', 'update', 'widget',
+ 'loadPreset', 'command',
+ 'update', 'widget',
)
# Auto-decorate methods
@@ -212,7 +210,7 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs[key] = property(attrs[key])
elif key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
- elif key in ('previewRender', 'frameRender'):
+ elif key == 'previewRender':
attrs[key] = cls.renderWrapper(attrs[key])
elif key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key])
@@ -298,16 +296,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return self.__class__.name
def __repr__(self):
+ import pprint
try:
preset = self.savePreset()
except Exception as e:
preset = '%s occurred while saving preset' % str(e)
return (
- 'Component(%s, %s, Core)\n'
- 'Name: %s v%s\n Preset: %s' % (
+ 'Component(module %s, pos %s) (%s)\n'
+ 'Name: %s v%s\nPreset: %s' % (
self.moduleIndex, self.compPos,
- self.__class__.name, str(self.__class__.version), preset
+ object.__repr__(self),
+ self.__class__.name, str(self.__class__.version),
+ pprint.pformat(preset)
)
)
@@ -886,12 +887,11 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
def redo(self):
if self.undone:
log.debug('Redoing component update')
- self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
- self.setWidgetValues(self.modifiedVals)
- self.parent.update(auto=True)
- self.parent.oldAttrs = None
- else:
- self.parent.setAttrs(self.modifiedVals)
+ 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
diff --git a/src/components/color.py b/src/components/color.py
index a55aa10..7d4f86d 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -102,7 +102,7 @@ class Component(Component):
# Return a solid image at x, y
if self.fillType == 0:
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))
return frame
diff --git a/src/components/color.ui b/src/components/color.ui
index 1865e60..c1713fb 100644
--- a/src/components/color.ui
+++ b/src/components/color.ui
@@ -204,7 +204,7 @@
0
- 999999999
+ 19200
0
@@ -239,7 +239,7 @@
- 999999999
+ 10800
diff --git a/src/components/text.py b/src/components/text.py
index 92f0599..32a108e 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -2,10 +2,13 @@ from PIL import ImageEnhance, ImageFilter, ImageChops
from PyQt5.QtGui import QColor, QFont
from PyQt5 import QtGui, QtCore, QtWidgets
import os
+import logging
from component import Component
from toolkit.frame import FramePainter, PaintColor
+log = logging.getLogger('AVP.Components.Text')
+
class Component(Component):
name = 'Title Text'
@@ -76,16 +79,15 @@ class Component(Component):
def getXY(self):
'''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont)
- if self.alignment == 0: # Left
- x = int(self.xPosition)
+ x = self.pixelValForAttr('xPosition')
if self.alignment == 1: # Middle
offset = int(fm.width(self.title)/2)
- x = self.xPosition - offset
-
+ x -= offset
if self.alignment == 2: # Right
offset = fm.width(self.title)
- x = self.xPosition - offset
+ x -= offset
+
return x, self.yPosition
def loadPreset(self, pr, *args):
@@ -137,6 +139,7 @@ class Component(Component):
image = FramePainter(width, height)
x, y = self.getXY()
+ log.debug('Text position translates to %s, %s', x, y)
if self.stroke > 0:
outliner = QtGui.QPainterPathStroker()
outliner.setWidth(self.stroke)
diff --git a/src/core.py b/src/core.py
index 784f3b8..b9e2335 100644
--- a/src/core.py
+++ b/src/core.py
@@ -14,7 +14,7 @@ import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.VERBOSE
-FILE_LOGLVL = logging.VERBOSE
+FILE_LOGLVL = logging.DEBUG
class Core:
@@ -32,6 +32,11 @@ class Core:
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
+ def __repr__(self):
+ return "\n=~=~=~=\n".join(
+ [repr(comp) for comp in self.selectedComponents]
+ )
+
def importComponents(self):
def findComponents():
for f in os.listdir(Core.componentsPath):
@@ -482,7 +487,6 @@ class Core:
'854x480',
],
'FFMPEG_BIN': findFfmpeg(),
- 'windowHasFocus': False,
'canceled': False,
}
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 3b204b7..d7fde5c 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -11,6 +11,7 @@ from queue import Queue
import sys
import os
import signal
+import atexit
import filecmp
import time
import logging
@@ -49,6 +50,13 @@ class MainWindow(QtWidgets.QMainWindow):
self.window = window
self.core = Core()
Core.mode = 'GUI'
+ # widgets of component settings
+ self.pages = []
+ self.lastAutosave = time.time()
+ # list of previous five autosave times, used to reduce update spam
+ self.autosaveTimes = []
+ self.autosaveCooldown = 0.2
+ self.encoding = False
# Find settings created by Core object
self.dataDir = Core.dataDir
@@ -56,19 +64,16 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
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)
- # widgets of component settings
- self.pages = []
- self.lastAutosave = time.time()
- # list of previous five autosave times, used to reduce update spam
- self.autosaveTimes = []
- self.autosaveCooldown = 0.2
- self.encoding = False
-
+ # Create Preset Manager
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
@@ -97,7 +102,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.timer.start(timeout)
# Begin decorating the window and connecting events
- self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList
style = window.pushButton_undo.style()
@@ -391,24 +395,41 @@ class MainWindow(QtWidgets.QMainWindow):
activated=lambda: self.moveComponent('bottom')
)
- # Debug Hotkeys
QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+R", self.window, self.drawPreview
+ "Ctrl+Shift+F", self.window, self.showFfmpegCommand
)
QtWidgets.QShortcut(
- "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
+ "Ctrl+Shift+U", self.window, self.showUndoStack
)
- QtWidgets.QShortcut(
- "Ctrl+Alt+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):
log.info('Ending the preview thread')
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
+ def terminate(self, *args):
+ self.cleanUp()
+ sys.exit(0)
+
@disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
@@ -542,7 +563,7 @@ class MainWindow(QtWidgets.QMainWindow):
return True
except FileNotFoundError:
log.error(
- 'Project file couldn\'t be located:', self.currentProject)
+ 'Project file couldn\'t be located: %s', self.currentProject)
return identical
return False
@@ -639,6 +660,7 @@ class MainWindow(QtWidgets.QMainWindow):
detail=detail,
icon='Critical',
)
+ log.info('%s', repr(self))
def changeEncodingStatus(self, status):
self.encoding = status
@@ -1017,12 +1039,3 @@ class MainWindow(QtWidgets.QMainWindow):
self.menu.move(parentPosition + QPos)
self.menu.show()
-
- def eventFilter(self, object, event):
- if event.type() == QtCore.QEvent.WindowActivate \
- or event.type() == QtCore.QEvent.FocusIn:
- Core.windowHasFocus = True
- elif event.type() == QtCore.QEvent.WindowDeactivate \
- or event.type() == QtCore.QEvent.FocusOut:
- Core.windowHasFocus = False
- return False
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
index c6b9a32..49a22eb 100644
--- a/src/gui/preview_win.py
+++ b/src/gui/preview_win.py
@@ -60,3 +60,4 @@ class PreviewWindow(QtWidgets.QLabel):
icon='Critical',
parent=self
)
+ log.info('%', repr(self.parent))
diff --git a/src/main.py b/src/main.py
index 6d18af3..f767de1 100644
--- a/src/main.py
+++ b/src/main.py
@@ -36,8 +36,6 @@ def main():
elif mode == 'GUI':
from gui.mainwindow import MainWindow
- import atexit
- import signal
window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
# window.adjustSize()
@@ -56,9 +54,6 @@ def main():
log.debug("Finished creating main window")
window.raise_()
- signal.signal(signal.SIGINT, main.cleanUp)
- atexit.register(main.cleanUp)
-
sys.exit(app.exec_())
if __name__ == "__main__":
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index f007f90..a77831e 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -157,7 +157,7 @@ def findFfmpeg():
['ffmpeg', '-version'], stderr=f
)
return "ffmpeg"
- except subprocess.CalledProcessError:
+ except (subprocess.CalledProcessError, FileNotFoundError):
return "avconv"
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index 2104978..aefb55f 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -21,7 +21,6 @@ class FramePainter(QtGui.QPainter):
Pillow image with finalize()
'''
def __init__(self, width, height):
- log.verbose('Creating new FramePainter')
image = BlankFrame(width, height)
self.image = QtGui.QImage(ImageQt(image))
super().__init__(self.image)
@@ -78,8 +77,6 @@ def defaultSize(framefunc):
def FloodFrame(width, height, RgbaTuple):
- log.verbose('Creating new %s*%s %s flood frame' % (
- width, height, RgbaTuple))
return Image.new("RGBA", (width, height), RgbaTuple)
--
cgit v1.2.3
From e8a7b18293768497df272bb4cb64b678d57f58da Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 27 Aug 2017 09:53:18 -0400
Subject: disallow suspiciously enormous floats
this stops a bad project file from crashing my computer...
---
src/component.py | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index de4b6a7..01c1d06 100644
--- a/src/component.py
+++ b/src/component.py
@@ -390,7 +390,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.settings = parent.settings
log.verbose(
'Creating UI for %s #%s\'s widget',
- self.name, self.compPos
+ self.__class__.name, self.compPos
)
self.page = self.loadUi(self.__class__.ui)
@@ -533,7 +533,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
else:
# Normal tracked widget
setattr(self, attr, val)
- log.verbose('Setting %s self.%s to %s' % (self.name, attr, val))
+ log.verbose('Setting %s self.%s to %s' % (
+ self.__class__.name, attr, val))
def setWidgetValues(self, attrDict):
'''
@@ -698,6 +699,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._relativeValues[attr]
+ 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',
@@ -748,7 +755,7 @@ 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.__class__.name, self.compPos, attr)
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
--
cgit v1.2.3
From 4a310ffb2870babf6774da843cad271f8a477bcc Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 27 Aug 2017 12:10:21 -0400
Subject: file logging can be turned completely off
and various changes to log levels and messages everywhere
---
src/component.py | 22 ++++++++----
src/components/spectrum.py | 21 ++++++++----
src/components/video.py | 21 ++++++++----
src/components/waveform.py | 20 +++++++----
src/core.py | 85 ++++++++++++++++++++++------------------------
src/gui/mainwindow.py | 18 ++++------
src/toolkit/ffmpeg.py | 22 ++++++++----
7 files changed, 119 insertions(+), 90 deletions(-)
(limited to 'src')
diff --git a/src/component.py b/src/component.py
index 01c1d06..f3ee188 100644
--- a/src/component.py
+++ b/src/component.py
@@ -423,7 +423,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for attr, widget in self._trackedWidgets.items():
key = attr if attr not in self._presetNames \
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:
widget.setText('%s,%s,%s' % val)
@@ -580,7 +587,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'colorWidgets',
'relativeWidgets',
):
- setattr(self, '_%s' % kwarg, kwargs[kwarg])
+ setattr(self, '_{}'.format(kwarg), kwargs[kwarg])
else:
raise ComponentError(
self, 'Nonsensical keywords to trackWidgets.')
@@ -613,6 +620,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
+ setattr(
+ self, attr, getWidgetValue(self._trackedWidgets[attr])
+ )
+
self._preUpdate()
self._autoUpdate()
@@ -732,13 +743,12 @@ 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)
return self.oldAttrs[attr]
else:
try:
return getattr(self, attr)
except AttributeError:
- log.info('Using visible values instead of attrs')
+ log.error('Using visible values instead of oldAttrs')
return self._trackedWidgets[attr].value()
def updateRelativeWidget(self, attr):
@@ -893,7 +903,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
def redo(self):
if self.undone:
- log.debug('Redoing component update')
+ log.info('Redoing component update')
self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
self.setWidgetValues(self.modifiedVals)
self.parent.update(auto=True)
@@ -906,7 +916,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
self.parent._sendUpdateSignal()
def undo(self):
- log.debug('Undoing component update')
+ log.info('Undoing component update')
self.undone = True
self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
self.setWidgetValues(self.oldWidgetVals)
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 2b98dc2..77cb086 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -148,15 +148,22 @@ class Component(Component):
'-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
- logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating ffmpeg process (log at %s)' % logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ 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(
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)
closePipe(self.previewPipe)
diff --git a/src/components/video.py b/src/components/video.py
index e6486ea..8ad21b5 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -139,16 +139,23 @@ class Component(Component):
'-frames:v', '1',
])
- logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating ffmpeg process (log at %s)' % logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg process (log at %s)' % logFilename)
+ with open(logFilename, 'w') 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(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ stderr=subprocess.DEVNULL, bufsize=10**8
)
+
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
diff --git a/src/components/waveform.py b/src/components/waveform.py
index 5c02bbf..cbfc47f 100644
--- a/src/components/waveform.py
+++ b/src/components/waveform.py
@@ -110,15 +110,21 @@ class Component(Component):
'-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
- logFilename = os.path.join(
- self.core.logDir, 'preview_%s.log' % str(self.compPos))
- log.debug('Creating ffmpeg process (log at %s)' % logFilename)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, 'preview_%s.log' % str(self.compPos))
+ log.debug('Creating ffmpeg log at %s', logFilename)
+ with open(logFilename, 'w') 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(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
diff --git a/src/core.py b/src/core.py
index b9e2335..1a90296 100644
--- a/src/core.py
+++ b/src/core.py
@@ -13,8 +13,8 @@ import toolkit
log = logging.getLogger('AVP.Core')
-STDOUT_LOGLVL = logging.VERBOSE
-FILE_LOGLVL = logging.DEBUG
+STDOUT_LOGLVL = logging.INFO
+FILE_LOGLVL = logging.VERBOSE
class Core:
@@ -145,17 +145,11 @@ class Core:
saveValueStore = self.getPreset(filepath)
if not saveValueStore:
return False
- try:
- comp = self.selectedComponents[compIndex]
- comp.loadPreset(
- saveValueStore,
- presetName
- )
- except KeyError as e:
- log.warning(
- '%s #%s\'s preset is missing value: %s',
- comp.name, str(compIndex), str(e)
- )
+ comp = self.selectedComponents[compIndex]
+ comp.loadPreset(
+ saveValueStore,
+ presetName
+ )
self.savedPresets[presetName] = dict(saveValueStore)
return True
@@ -472,11 +466,12 @@ class Core:
encoderOptions = json.load(json_file)
settings = {
+ 'canceled': False,
+ 'FFMPEG_BIN': findFfmpeg(),
'dataDir': dataDir,
'settings': QtCore.QSettings(
os.path.join(dataDir, 'settings.ini'),
QtCore.QSettings.IniFormat),
- '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'),
@@ -486,8 +481,8 @@ class Core:
'1280x720',
'854x480',
],
- 'FFMPEG_BIN': findFfmpeg(),
- 'canceled': False,
+ 'logDir': os.path.join(dataDir, 'log'),
+ 'logEnabled': False,
}
settings['videoFormats'] = toolkit.appendUppercase([
@@ -572,42 +567,42 @@ class Core:
@staticmethod
def makeLogger():
- 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)
-
- # 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
+ # send critical log messages to stdout
logStream = logging.StreamHandler()
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(
- '<%(name)s> %(message)s'
+ '<%(name)s> %(levelname)s: %(message)s'
)
- logFile.setFormatter(fileFormatter)
- libLogFile.setFormatter(fileFormatter)
logStream.setFormatter(streamFormatter)
-
log = logging.getLogger('AVP')
- log.addHandler(logFile)
log.addHandler(logStream)
- libLog = logging.getLogger()
- libLog.addHandler(libLogFile)
- # lowest level must be explicitly set on the root Logger
- libLog.setLevel(0)
+
+ if FILE_LOGLVL is not None:
+ # write log files as well!
+ 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
Core.storeSettings()
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index d7fde5c..81c5d7c 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -92,6 +92,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
+ self.previewThread.finished.connect(
+ lambda:
+ log.critical('PREVIEW THREAD DIED! This should never happen.')
+ )
timeout = 500
log.debug(
@@ -442,7 +446,7 @@ class MainWindow(QtWidgets.QMainWindow):
appName += '*'
except AttributeError:
pass
- log.debug('Setting window title to %s' % appName)
+ log.verbose('Setting window title to %s' % appName)
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
@@ -459,16 +463,8 @@ class MainWindow(QtWidgets.QMainWindow):
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)
+
+ modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
name = self.core.selectedComponents[pos].name
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py
index a77831e..d78d803 100644
--- a/src/toolkit/ffmpeg.py
+++ b/src/toolkit/ffmpeg.py
@@ -91,16 +91,24 @@ class FfmpegVideo:
def fillBuffer(self):
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)
- with open(logFilename, 'w') as logf:
- logf.write(" ".join(self.command) + '\n\n')
- with open(logFilename, 'a') as logf:
+ if core.Core.logEnabled:
+ logFilename = os.path.join(
+ core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
+ )
+ 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:
+ self.pipe = openPipe(
+ self.command, stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
+ )
+ else:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
- stderr=logf, bufsize=10**8
+ stderr=subprocess.DEVNULL, bufsize=10**8
)
+
while True:
if self.parent.canceled:
break
--
cgit v1.2.3
From ad6dd9f5329f3e23e75c181c21ca8701028b538f Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 27 Aug 2017 19:59:51 -0400
Subject: undoable Life component grid actions
---
src/components/life.py | 132 ++++++++++++++++++++++++++++++++++++-------------
src/gui/preview_win.py | 8 ++-
2 files changed, 102 insertions(+), 38 deletions(-)
(limited to 'src')
diff --git a/src/components/life.py b/src/components/life.py
index d4a455d..7a610eb 100644
--- a/src/components/life.py
+++ b/src/components/life.py
@@ -1,4 +1,5 @@
from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtWidgets import QUndoCommand
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
import os
import math
@@ -58,22 +59,8 @@ class Component(Component):
self.mergeUndo = True
def shiftGrid(self, d):
- def newGrid(Xchange, Ychange):
- return {
- (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()
+ action = ShiftGrid(self, d)
+ self.parent.undoStack.push(action)
def update(self):
self.updateGridSize()
@@ -98,17 +85,14 @@ class Component(Component):
enabled = (len(self.startingGrid) > 0)
for widget in self.shiftButtons:
widget.setEnabled(enabled)
- super().update()
def previewClickEvent(self, pos, size, button):
pos = (
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
)
- if button == 1:
- self.startingGrid.add(pos)
- elif button == 2:
- self.startingGrid.discard(pos)
+ action = ClickGrid(self, pos, button)
+ self.parent.undoStack.push(action)
def updateGridSize(self):
w, h = self.core.resolutions[-1].split('x')
@@ -223,7 +207,7 @@ class Component(Component):
'up', 'down', 'left', 'right',
)
}
- for cell in nearbyCoords(x, y):
+ for cell in self.nearbyCoords(x, y):
if cell not in grid:
continue
if cell[0] == x:
@@ -363,7 +347,7 @@ class Component(Component):
def neighbours(x, y):
return {
- cell for cell in nearbyCoords(x, y)
+ cell for cell in self.nearbyCoords(x, y)
if cell in lastGrid
}
@@ -374,7 +358,7 @@ class Component(Component):
newGrid.add((x, y))
potentialNewCells = {
coordTup for origin in lastGrid
- for coordTup in list(nearbyCoords(*origin))
+ for coordTup in list(self.nearbyCoords(*origin))
}
for x, y in potentialNewCells:
if (x, y) in newGrid:
@@ -397,13 +381,95 @@ class Component(Component):
widget.setEnabled(True)
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
+
+
+class ClickGrid(QUndoCommand):
+ def __init__(self, comp, pos, id_):
+ super().__init__(
+ "click %s component #%s" % (comp.name, comp.compPos))
+ self.comp = comp
+ self.pos = [pos]
+ self.id_ = id_
+
+ 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 nearbyCoords(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 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()
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py
index 49a22eb..3db420c 100644
--- a/src/gui/preview_win.py
+++ b/src/gui/preview_win.py
@@ -1,14 +1,14 @@
from PyQt5 import QtCore, QtGui, QtWidgets
import logging
+log = logging.getLogger('AVP.Gui.PreviewWindow')
+
class PreviewWindow(QtWidgets.QLabel):
'''
Paints the preview QLabel in MainWindow and maintains the aspect ratio
when the window is resized.
'''
- log = logging.getLogger('AVP.Gui.PreviewWindow')
-
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()
self.parent = parent
@@ -41,17 +41,15 @@ class PreviewWindow(QtWidgets.QLabel):
if i >= 0:
component = self.parent.core.selectedComponents[i]
if not hasattr(component, 'previewClickEvent'):
- self.log.info('Ignored click event')
return
pos = (event.x(), event.y())
size = (self.width(), self.height())
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))
component.previewClickEvent(
pos, size, butt
)
- self.parent.core.updateComponent(i)
@QtCore.pyqtSlot(str)
def threadError(self, msg):
--
cgit v1.2.3
From 8411857030d92e448d5c64682f396e677161afbe Mon Sep 17 00:00:00 2001
From: tassaron
Date: Mon, 28 Aug 2017 18:54:54 -0400
Subject: ctrl-c ends commandline mode properly
---
setup.py | 2 +-
src/command.py | 9 +++++++++
src/components/spectrum.py | 3 ++-
src/core.py | 10 ++++------
src/toolkit/frame.py | 1 +
src/video_thread.py | 11 ++++++++---
6 files changed, 25 insertions(+), 11 deletions(-)
(limited to 'src')
diff --git a/setup.py b/setup.py
index dd546e2..cdf4c4a 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
import os
-__version__ = '2.0.0.rc4'
+__version__ = '2.0.0rc5'
def package_files(directory):
diff --git a/src/command.py b/src/command.py
index 4116c5a..cd3c6c3 100644
--- a/src/command.py
+++ b/src/command.py
@@ -8,6 +8,7 @@ import argparse
import os
import sys
import time
+import signal
from core import Core
@@ -91,6 +92,9 @@ class Command(QtCore.QObject):
for arg in args:
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:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
@@ -124,6 +128,11 @@ class Command(QtCore.QObject):
self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
+ def stopVideo(self, *args):
+ self.worker.error = True
+ self.worker.cancelExport()
+ self.worker.cancel()
+
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if 'Export ' in value:
diff --git a/src/components/spectrum.py b/src/components/spectrum.py
index 77cb086..6675f5b 100644
--- a/src/components/spectrum.py
+++ b/src/components/spectrum.py
@@ -98,7 +98,8 @@ class Component(Component):
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
- self.previewPipe.wait()
+ if self.previewPipe is not None:
+ self.previewPipe.wait()
self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo(
diff --git a/src/core.py b/src/core.py
index 1a90296..d7445c9 100644
--- a/src/core.py
+++ b/src/core.py
@@ -13,8 +13,8 @@ import toolkit
log = logging.getLogger('AVP.Core')
-STDOUT_LOGLVL = logging.INFO
-FILE_LOGLVL = logging.VERBOSE
+STDOUT_LOGLVL = logging.WARNING
+FILE_LOGLVL = None
class Core:
@@ -77,8 +77,7 @@ class Core:
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
- return None
-
+ return -1
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
@@ -188,7 +187,6 @@ class Core:
for key, value in data['Settings']:
Core.settings.setValue(key, value)
-
for tup in data['Components']:
name, vers, preset = tup
clearThis = False
@@ -213,7 +211,7 @@ class Core:
self.moduleIndexFor(name),
loader
)
- if i is None:
+ if i == -1:
loader.showMessage(msg="Too many components!")
break
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index aefb55f..0e200b5 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -32,6 +32,7 @@ class FramePainter(QtGui.QPainter):
super().setPen(penStyle)
def finalize(self):
+ log.verbose("Finalizing FramePainter")
imBytes = self.image.bits().asstring(self.image.byteCount())
frame = Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes
diff --git a/src/video_thread.py b/src/video_thread.py
index 823ac73..91ebe93 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -252,9 +252,14 @@ class Worker(QtCore.QObject):
print('############################')
log.info('Opening pipe to ffmpeg')
log.info(cmd)
- self.out_pipe = openPipe(
- ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
- )
+ try:
+ 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
--
cgit v1.2.3