This repository has been archived on 2020-08-22. You can view files and clone it, but cannot push or open issues or pull requests.
pyaudviz/src/gui/mainwindow.py

1038 lines
39 KiB
Python

'''
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 atexit
import filecmp
import time
import logging
from core import Core
import gui.preview_thread as preview_thread
from gui.preview_win import PreviewWindow
from gui.presetmanager import PresetManager
from gui.actions import *
from toolkit import (
disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals
)
log = logging.getLogger('AVP.Gui.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)
log.debug(
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
self.window = window
self.core = Core()
Core.mode = 'GUI'
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
# list of previous five autosave times, used to reduce update spam
self.autosaveTimes = []
self.autosaveCooldown = 0.2
self.encoding = False
# Find settings created by Core object
self.dataDir = Core.dataDir
self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings
# Register clean-up functions
signal.signal(signal.SIGINT, self.terminate)
atexit.register(self.cleanUp)
# Create stack of undoable user actions
self.undoStack = QtWidgets.QUndoStack(self)
undoLimit = self.settings.value("pref_undoLimit")
self.undoStack.setUndoLimit(undoLimit)
# Create Preset Manager
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'gui', '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, 'gui', "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()
self.previewThread.finished.connect(
lambda:
log.critical('PREVIEW THREAD DIED! This should never happen.')
)
timeout = 500
log.debug(
'Preview timer set to trigger when idle for %sms' % str(timeout)
)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit)
self.timer.start(timeout)
# Begin decorating the window and connecting events
componentList = self.window.listWidget_componentList
style = window.pushButton_undo.style()
undoButton = window.pushButton_undo
undoButton.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack)
)
undoButton.clicked.connect(self.undoStack.undo)
undoButton.setEnabled(False)
self.undoStack.cleanChanged.connect(
lambda change: undoButton.setEnabled(self.undoStack.count())
)
self.undoMenu = QMenu()
self.undoMenu.addAction(
self.undoStack.createUndoAction(self)
)
self.undoMenu.addAction(
self.undoStack.createRedoAction(self)
)
action = self.undoMenu.addAction('Show History...')
action.triggered.connect(
lambda _: self.showUndoStack()
)
undoButton.setMenu(self.undoMenu)
style = window.pushButton_listMoveUp.style()
window.pushButton_listMoveUp.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_ArrowUp)
)
style = window.pushButton_listMoveDown.style()
window.pushButton_listMoveDown.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_ArrowDown)
)
style = window.pushButton_removeComponent.style()
window.pushButton_removeComponent.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_DialogDiscardButton)
)
if sys.platform == 'darwin':
log.debug(
'Darwin detected: showing progress label below progress bar')
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.addComponent(0, item)
)
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)
QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo)
QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo)
QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo)
# Hotkeys for component list
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
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')
)
QtWidgets.QShortcut(
"Ctrl+Shift+F", self.window, self.showFfmpegCommand
)
QtWidgets.QShortcut(
"Ctrl+Shift+U", self.window, self.showUndoStack
)
if log.isEnabledFor(logging.DEBUG):
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
)
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self))
)
def __repr__(self):
return (
'\n%s\n'
'#####\n'
'Preview thread is %s\n' % (
repr(self.core),
'live' if self.previewThread.isRunning() else 'dead',
)
)
def cleanUp(self, *args):
log.info('Ending the preview thread')
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
def terminate(self, *args):
self.cleanUp()
sys.exit(0)
@disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
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.verbose('Setting window title to %s' % appName)
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False):
'''
Sets component title to modified or unmodified when given boolean.
If given a preset dict, compares it against the component to
determine if it is modified.
A component with no preset is always unmodified.
'''
if type(presetStore) is dict:
name = presetStore['preset']
if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
name = self.core.selectedComponents[pos].name
title = str(name)
if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified:
title += '*'
if type(presetStore) is bool:
log.debug(
'Forcing %s #%s\'s modified status to %s: %s',
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: %s', 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',
)
log.info('%s', repr(self))
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 showUndoStack(self):
dialog = QtWidgets.QDialog(self.window)
undoView = QtWidgets.QUndoView(self.undoStack)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(undoView)
dialog.setLayout(layout)
dialog.show()
def showFfmpegCommand(self):
from textwrap import wrap
from toolkit.ffmpeg import createFfmpegCommand
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 addComponent(self, compPos, moduleIndex):
'''Creates an undoable action that adds a new component.'''
action = AddComponent(self, compPos, moduleIndex)
self.undoStack.push(action)
def insertComponent(self, index):
'''Triggered by Core to finish initializing a new component.'''
componentList = self.window.listWidget_componentList
stackedWidget = self.window.stackedWidget
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
selected = componentList.selectedItems()
if selected:
action = RemoveComponent(self, selected)
self.undoStack.push(action)
def _removeComponent(self, index):
stackedWidget = self.window.stackedWidget
componentList = self.window.listWidget_componentList
stackedWidget.removeWidget(self.pages[index])
componentList.takeItem(index)
self.core.removeComponent(index)
self.pages.pop(index)
self.changeComponentWidget()
self.drawPreview()
@disableWhenEncoding
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
tag = change
if change == 'top':
change = -componentList.currentRow()
elif change == 'bottom':
change = len(componentList)-componentList.currentRow()-1
else:
tag = 'down' if change == 1 else 'up'
row = componentList.currentRow()
newRow = row + change
if newRow > -1 and newRow < componentList.count():
action = MoveComponent(self, row, newRow, tag)
self.undoStack.push(action)
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
):
with blockSignals(field):
field.setText('')
self.progressBarUpdated(0)
self.progressBarSetText('')
self.undoStack.clear()
@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.addComponent(
0 if insertCompAtTop else index, item
)
)
self.menu.move(parentPosition + QPos)
self.menu.show()