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