From e92e9d79f95ad67e83074ef318278c3486601eac Mon Sep 17 00:00:00 2001 From: DH4 Date: Fri, 23 Jun 2017 17:38:05 -0500 Subject: QT5 Conversion + Directory Structure --- src/main.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/main.py (limited to 'src/main.py') diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..4bf26db --- /dev/null +++ b/src/main.py @@ -0,0 +1,88 @@ +from PyQt5 import QtGui, uic, QtWidgets +import sys +import os + +import core +import preview_thread +import video_thread + + +def LoadDefaultSettings(self): + self.resolutions = [ + '1920x1080', + '1280x720', + '854x480' + ] + + default = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "AAC", + "outputAudioBitrate": "192", + "outputVideoCodec": "H264", + "outputVideoBitrate": "2500", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "outputContainer": "MP4", + "projectDir": os.path.join(self.dataDir, 'projects'), + } + + for parm, value in default.items(): + #print(parm, self.settings.value(parm)) + if self.settings.value(parm) is None: + self.settings.setValue(parm, value) + +if __name__ == "__main__": + mode = 'gui' + if len(sys.argv) > 2: + mode = 'cmd' + + elif len(sys.argv) == 2: + if sys.argv[1].startswith('-'): + mode = 'cmd' + else: + # opening a project file with gui + proj = sys.argv[1] + else: + # normal gui launch + proj = None + + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName("audio-visualizer") + app.setOrganizationName("audio-visualizer") + + if mode == 'cmd': + from command import * + + main = Command() + + elif mode == 'gui': + from mainwindow import * + import atexit + import signal + + if getattr(sys, 'frozen', False): + # frozen + wd = os.path.dirname(sys.executable) + else: + # unfrozen + wd = os.path.dirname(os.path.realpath(__file__)) + + window = uic.loadUi(os.path.join(wd, "mainwindow.ui")) + # window.adjustSize() + desc = QtWidgets.QDesktopWidget() + dpi = desc.physicalDpiX() + + topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96)) + window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96)) + # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) + + main = MainWindow(window, proj) + + signal.signal(signal.SIGINT, main.cleanUp) + atexit.register(main.cleanUp) + + # applicable to both modes + sys.exit(app.exec_()) -- cgit v1.2.3 From 680214f5180a12f2250d8e266df9375ce99b9f80 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 23 Jun 2017 23:00:24 -0400 Subject: qt5 fixes also pep8 compliance --- src/command.py | 15 +++---- src/components/__base__.py | 14 +++---- src/components/color.py | 15 ++++--- src/components/image.py | 2 +- src/components/video.py | 14 ++++--- src/core.py | 28 +++++++------ src/main.py | 6 +-- src/mainwindow.py | 100 ++++++++++++++++++++++++++------------------- src/presetmanager.py | 55 +++++++++++++------------ src/video_thread.py | 11 ++--- 10 files changed, 143 insertions(+), 117 deletions(-) (limited to 'src/main.py') diff --git a/src/command.py b/src/command.py index 1a1e810..2f71f31 100644 --- a/src/command.py +++ b/src/command.py @@ -22,9 +22,9 @@ class Command(QtCore.QObject): self.parser = argparse.ArgumentParser( description='Create a visualization for an audio file', epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp ' - '-i ~/Music/song.mp3 -o ~/video.mp4 ' - '-c 0 image path=~/Pictures/thisWeeksPicture.jpg ' - '-c 1 video "preset=My Logo" -c 2 vis layout=classic') + '-i ~/Music/song.mp3 -o ~/video.mp4 ' + '-c 0 image path=~/Pictures/thisWeeksPicture.jpg ' + '-c 1 video "preset=My Logo" -c 2 vis layout=classic') self.parser.add_argument( '-i', '--input', metavar='SOUND', help='input audio file') @@ -113,10 +113,11 @@ class Command(QtCore.QObject): if name.capitalize() in compName: return compName - compFileNames = [ \ - os.path.splitext(os.path.basename( - mod.__file__))[0] \ - for mod in self.core.modules \ + compFileNames = [ + os.path.splitext( + os.path.basename(mod.__file__) + )[0] + for mod in self.core.modules ] for i, compFileName in enumerate(compFileNames): if name.lower() in compFileName: diff --git a/src/components/__base__.py b/src/components/__base__.py index a4677b1..a24af40 100644 --- a/src/components/__base__.py +++ b/src/components/__base__.py @@ -39,7 +39,7 @@ class Component(QtCore.QObject): then update self.page widgets using the preset dict. ''' self.currentPreset = presetName \ - if presetName != None else presetDict['preset'] + if presetName is not None else presetDict['preset'] def preFrameRender(self, **kwargs): '''Triggered only before a video is exported (video_thread.py) @@ -66,8 +66,8 @@ class Component(QtCore.QObject): print('Couldn\'t locate preset "%s"' % preset) quit(1) else: - print('Opening "%s" preset on layer %s' % \ - (preset, self.compPos)) + print('Opening "%s" preset on layer %s' % ( + preset, self.compPos)) self.core.openPreset(path, self.compPos, preset) else: print( @@ -88,8 +88,8 @@ class Component(QtCore.QObject): and return this as an RGB string and QPushButton stylesheet. In a subclass apply stylesheet to any color selection widgets ''' - dialog = QtGui.QColorDialog() - dialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) + dialog = QtWidgets.QColorDialog() + dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True) color = dialog.getColor() if color.isValid(): RGBstring = '%s,%s,%s' % ( @@ -142,10 +142,10 @@ class Component(QtCore.QObject): return image ''' + class BadComponentInit(Exception): def __init__(self, arg, name): - string = \ -'''################################ + string = '''################################ Mandatory argument "%s" not specified in %s instance initialization ###################################''' diff --git a/src/components/color.py b/src/components/color.py index 8f9a1d1..2e3902a 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -53,7 +53,7 @@ class Component(__base__.Component): page.spinBox_height.valueChanged.connect(self.update) page.checkBox_trans.stateChanged.connect(self.update) - self.fillLabels = [ \ + self.fillLabels = [ 'Solid', 'Linear Gradient', 'Radial Gradient', @@ -126,8 +126,8 @@ class Component(__base__.Component): r, g, b = self.color1 shapeSize = (self.sizeWidth, self.sizeHeight) # in default state, skip all this logic and return a plain fill - if self.fillType==0 and shapeSize == (width, height) \ - and self.x == 0 and self.y == 0: + if self.fillType == 0 and shapeSize == (width, height) \ + and self.x == 0 and self.y == 0: return Image.new("RGBA", (width, height), (r, g, b, 255)) frame = self.blankFrame(width, height) @@ -143,9 +143,11 @@ class Component(__base__.Component): image = ImageQt(frame) painter = QtGui.QPainter(image) if self.stretch: - w = width; h = height + w = width + h = height else: - w = self.sizeWidth; h = self.sizeWidth + w = self.sizeWidth + h = self.sizeWidth if self.fillType == 1: # Linear Gradient brush = QtGui.QLinearGradient( @@ -170,7 +172,8 @@ class Component(__base__.Component): else: brush.setColorAt(1.0, QColor(*self.color2)) painter.setBrush(brush) - painter.drawRect(self.x, self.y, + painter.drawRect( + self.x, self.y, self.sizeWidth, self.sizeHeight) painter.end() imBytes = image.bits().asstring(image.numBytes()) diff --git a/src/components/image.py b/src/components/image.py index 8ca88d3..3517af6 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -85,7 +85,7 @@ class Component(__base__.Component): def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName( + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Image", imgDir, "Image Files (%s)" % " ".join(self.imageFormats)) if filename: diff --git a/src/components/video.py b/src/components/video.py index 58ce7a3..0090426 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -41,8 +41,8 @@ class Video: '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale=%s:%s' % - scale(self.scale, self.width, self.height, str), + '-filter:v', 'scale=%s:%s' % scale( + self.scale, self.width, self.height, str), '-vcodec', 'rawvideo', '-', ] @@ -180,7 +180,7 @@ class Component(__base__.Component): def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName( + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Video", imgDir, "Video Files (%s)" % " ".join(self.videoFormats) ) @@ -199,8 +199,8 @@ class Component(__base__.Component): '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale=%s:%s' % - scale(self.scale, width, height, str), + '-filter:v', 'scale=%s:%s' % scale( + self.scale, width, height, str), '-vcodec', 'rawvideo', '-', '-ss', '90', '-vframes', '1', @@ -238,6 +238,7 @@ class Component(__base__.Component): def commandHelp(self): print('Load a video:\n path=/filepath/to/video.mp4') + def scale(scale, width, height, returntype=None): width = (float(width) / 100.0) * float(scale) height = (float(height) / 100.0) * float(scale) @@ -248,6 +249,7 @@ def scale(scale, width, height, returntype=None): else: return (width, height) + def finalizeFrame(self, imageData, width, height): if self.distort: try: @@ -265,7 +267,7 @@ def finalizeFrame(self, imageData, width, height): imageData) if self.scale != 100 \ - or self.xPosition != 0 or self.yPosition != 0: + or self.xPosition != 0 or self.yPosition != 0: frame = self.blankFrame(width, height) frame.paste(image, box=(self.xPosition, self.yPosition)) else: diff --git a/src/core.py b/src/core.py index bb5d351..670a3c5 100644 --- a/src/core.py +++ b/src/core.py @@ -179,7 +179,7 @@ class Core(): clearThis = False # add loaded named presets to savedPresets dict - if 'preset' in preset and preset['preset'] != None: + if 'preset' in preset and preset['preset'] is not None: nam = preset['preset'] filepath2 = os.path.join( self.presetDir, name, str(vers), nam) @@ -195,12 +195,12 @@ class Core(): -1, self.moduleIndexFor(name), loader) - if i == None: + if i is None: loader.showMessage(msg="Too many components!") break try: - if 'preset' in preset and preset['preset'] != None: + if 'preset' in preset and preset['preset'] is not None: self.selectedComponents[i].loadPreset( preset ) @@ -210,8 +210,8 @@ class Core(): preset['preset'] ) except KeyError as e: - print('%s missing value %s' % - (self.selectedComponents[i], e)) + print('%s missing value %s' % ( + self.selectedComponents[i], e)) if clearThis: self.clearPreset(i) @@ -221,7 +221,6 @@ class Core(): errcode = 1 data = sys.exc_info() - if errcode == 1: typ, value, _ = data if typ.__name__ == KeyError: @@ -274,11 +273,11 @@ class Core(): i += 1 elif i == 2: lastCompPreset = Core.presetFromString(line) - data[section].append( - (lastCompName, + data[section].append(( + lastCompName, lastCompVers, - lastCompPreset) - ) + lastCompPreset + )) i = 0 return 0, data except: @@ -309,7 +308,9 @@ class Core(): return False, '' def exportPreset(self, exportPath, compName, vers, origName): - internalPath = os.path.join(self.presetDir, compName, str(vers), origName) + internalPath = os.path.join( + self.presetDir, compName, str(vers), origName + ) if not os.path.exists(internalPath): return if os.path.exists(exportPath): @@ -328,7 +329,7 @@ class Core(): return False def createPresetFile( - self, compName, vers, presetName, saveValueStore, filepath=''): + self, compName, vers, presetName, saveValueStore, filepath=''): '''Create a preset file (.avl) at filepath using args. Or if filepath is empty, create an internal preset using args''' if not filepath: @@ -463,7 +464,8 @@ class Core(): @staticmethod def presetToString(dictionary): '''Alphabetizes a dict into OrderedDict & returns string repr''' - return repr(OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))) + return repr( + OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))) @staticmethod def presetFromString(string): diff --git a/src/main.py b/src/main.py index 4bf26db..58fdb46 100644 --- a/src/main.py +++ b/src/main.py @@ -30,7 +30,6 @@ def LoadDefaultSettings(self): } for parm, value in default.items(): - #print(parm, self.settings.value(parm)) if self.settings.value(parm) is None: self.settings.setValue(parm, value) @@ -51,7 +50,7 @@ if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") - app.setOrganizationName("audio-visualizer") + # app.setOrganizationName("audio-visualizer") if mode == 'cmd': from command import * @@ -76,7 +75,8 @@ if __name__ == "__main__": dpi = desc.physicalDpiX() topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96)) - window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96)) + window.resize( + window.width() * (dpi / 96), window.height() * (dpi / 96)) # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = MainWindow(window, proj) diff --git a/src/mainwindow.py b/src/mainwindow.py index a52a0f4..7a9e397 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -116,7 +116,6 @@ class MainWindow(QtWidgets.QMainWindow): codec = window.comboBox_videoCodec.itemText(i) if codec == self.settings.value('outputVideoCodec'): window.comboBox_videoCodec.setCurrentIndex(i) - #print(codec) for i in range(window.comboBox_audioCodec.count()): codec = window.comboBox_audioCodec.itemText(i) @@ -146,10 +145,11 @@ class MainWindow(QtWidgets.QMainWindow): # Make component buttons self.compMenu = QMenu() + self.compActions = [] for i, comp in enumerate(self.core.modules): action = self.compMenu.addAction(comp.Component.__doc__) action.triggered.connect( - lambda item=i: self.core.insertComponent(0, item, self)) + lambda _, item=i: self.core.insertComponent(0, item, self)) self.window.pushButton_addComponent.setMenu(self.compMenu) @@ -160,9 +160,10 @@ class MainWindow(QtWidgets.QMainWindow): self.window.pushButton_removeComponent.clicked.connect( lambda _: self.removeComponent()) - componentList.setContextMenuPolicy( - QtCore.Qt.CustomContextMenu) - componentList.customContextMenuRequested.connect(self.componentContextMenu) + componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + componentList.customContextMenuRequested.connect( + self.componentContextMenu + ) currentRes = str(self.settings.value('outputWidth'))+'x' + \ str(self.settings.value('outputHeight')) @@ -245,19 +246,30 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) - QtWidgets.QShortcut("Ctrl+T", self.window, activated=lambda: - self.window.pushButton_addComponent.click()) - 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, - activated=lambda: self.moveComponent(-1)) - QtWidgets.QShortcut("Ctrl+Down", self.window, - activated=lambda: self.moveComponent(1)) + QtWidgets.QShortcut( + "Ctrl+T", self.window, + activated=lambda: self.window.pushButton_addComponent.click() + ) + 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, + activated=lambda: self.moveComponent(-1) + ) + QtWidgets.QShortcut( + "Ctrl+Down", self.window, + activated=lambda: self.moveComponent(1) + ) QtWidgets.QShortcut("Ctrl+Home", self.window, self.moveComponentTop) QtWidgets.QShortcut("Ctrl+End", self.window, self.moveComponentBottom) QtWidgets.QShortcut("Ctrl+r", self.window, self.removeComponent) @@ -280,7 +292,7 @@ class MainWindow(QtWidgets.QMainWindow): def updateComponentTitle(self, pos, presetStore=False): if type(presetStore) == dict: name = presetStore['preset'] - if name == None or name not in self.core.savedPresets: + if name is None or name not in self.core.savedPresets: modified = False else: modified = (presetStore != self.core.savedPresets[name]) @@ -362,21 +374,22 @@ class MainWindow(QtWidgets.QMainWindow): def openInputFileDialog(self): inputDir = self.settings.value("inputDir", os.path.expanduser("~")) - fileName = QtGui.QFileDialog.getOpenFileName( + fileName, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Open Audio File", inputDir, "Audio Files (%s)" % " ".join(self.core.audioFormats)) - if not fileName == "": + 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 = QtGui.QFileDialog.getSaveFileName( + fileName, _ = QtWidgets.QFileDialog.getSaveFileName( self.window, "Set Output Video File", outputDir, - "Video Files (%s);; All Files (*)" % " ".join(self.core.videoFormats)) + "Video Files (%s);; All Files (*)" % " ".join( + self.core.videoFormats)) if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) @@ -547,13 +560,13 @@ class MainWindow(QtWidgets.QMainWindow): '''Drop event for the component listwidget''' componentList = self.window.listWidget_componentList - modelIndexes = [ \ - componentList.model().index(i) \ - for i in range(componentList.count()) \ + modelIndexes = [ + componentList.model().index(i) + for i in range(componentList.count()) ] - rects = [ \ - componentList.visualRect(modelIndex) \ - for modelIndex in modelIndexes \ + rects = [ + componentList.visualRect(modelIndex) + for modelIndex in modelIndexes ] rowPos = [rect.contains(event.pos()) for rect in rects] @@ -602,9 +615,10 @@ class MainWindow(QtWidgets.QMainWindow): 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), + "Save before %s?" % ( + os.path.basename(self.currentProject)[:-4], + phrase + ), showCancel=True) if ch: success = self.saveProjectChanges() @@ -613,7 +627,7 @@ class MainWindow(QtWidgets.QMainWindow): os.remove(self.autosavePath) def openSaveProjectDialog(self): - filename = QtGui.QFileDialog.getSaveFileName( + filename, _ = QtWidgets.QFileDialog.getSaveFileName( self.window, "Create Project File", self.settings.value("projectDir"), "Project Files (*.avp)") @@ -628,7 +642,7 @@ class MainWindow(QtWidgets.QMainWindow): self.core.createProjectFile(filename) def openOpenProjectDialog(self): - filename = QtGui.QFileDialog.getOpenFileName( + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Open Project File", self.settings.value("projectDir"), "Project Files (*.avp)") @@ -657,17 +671,19 @@ class MainWindow(QtWidgets.QMainWindow): def showMessage(self, **kwargs): parent = kwargs['parent'] if 'parent' in kwargs else self.window - msg = QtGui.QMessageBox(parent) + msg = QtWidgets.QMessageBox(parent) msg.setModal(True) msg.setText(kwargs['msg']) msg.setIcon( - kwargs['icon'] if 'icon' in kwargs else QtGui.QMessageBox.Information) + 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( - QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) else: - msg.setStandardButtons(QtGui.QMessageBox.Ok) + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) ch = msg.exec_() if ch == 1024: return True @@ -687,7 +703,7 @@ class MainWindow(QtWidgets.QMainWindow): return self.presetManager.findPresets() - self.menu = QtGui.QMenu() + self.menu = QMenu() menuItem = self.menu.addAction("Save Preset") menuItem.triggered.connect( self.presetManager.openSavePresetDialog @@ -695,8 +711,10 @@ class MainWindow(QtWidgets.QMainWindow): # submenu for opening presets try: - presets = self.presetManager.presets[str(self.core.selectedComponents[index])] - self.submenu = QtGui.QMenu("Open Preset") + presets = self.presetManager.presets[ + str(self.core.selectedComponents[index]) + ] + self.submenu = QMenu("Open Preset") self.menu.addMenu(self.submenu) for version, presetName in presets: diff --git a/src/presetmanager.py b/src/presetmanager.py index ec3f5cd..97f6e0e 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -1,4 +1,4 @@ -from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5 import QtCore, QtWidgets import string import os @@ -21,13 +21,15 @@ class PresetManager(QtWidgets.QDialog): # window self.lastFilter = '*' - self.presetRows = [] # list of (comp, vers, name) tuples + 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_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) @@ -36,7 +38,8 @@ class PresetManager(QtWidgets.QDialog): self.drawFilterList() self.window.comboBox_filter.currentIndexChanged.connect( lambda: self.drawPresetList( - self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text() + self.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() ) ) @@ -47,7 +50,8 @@ class PresetManager(QtWidgets.QDialog): 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.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() ) ) self.drawPresetList('*') @@ -72,16 +76,14 @@ class PresetManager(QtWidgets.QDialog): 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 \ - } + 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() @@ -96,7 +98,8 @@ class PresetManager(QtWidgets.QDialog): continue for vers, preset in presets: if not presetFilter or presetFilter in preset: - self.window.listWidget_presets.addItem('%s: %s' % (component, preset)) + self.window.listWidget_presets.addItem( + '%s: %s' % (component, preset)) self.presetRows.append((component, vers, preset)) if preset not in presetNames: presetNames.append(preset) @@ -124,11 +127,11 @@ class PresetManager(QtWidgets.QDialog): while True: index = componentList.currentRow() currentPreset = selectedComponents[index].currentPreset - newName, OK = QtGui.QInputDialog.getText( + newName, OK = QtWidgets.QInputDialog.getText( self.parent.window, 'Audio Visualizer', 'New Preset Name:', - QtGui.QLineEdit.Normal, + QtWidgets.QLineEdit.Normal, currentPreset ) if OK: @@ -149,7 +152,7 @@ class PresetManager(QtWidgets.QDialog): break def createNewPreset( - self, compName, vers, filename, saveValueStore, **kwargs): + self, compName, vers, filename, saveValueStore, **kwargs): path = os.path.join(self.presetDir, compName, str(vers), filename) if self.presetExists(path, **kwargs): return @@ -163,7 +166,7 @@ class PresetManager(QtWidgets.QDialog): msg="%s already exists! Overwrite it?" % os.path.basename(path), showCancel=True, - icon=QtGui.QMessageBox.Warning, + icon=QtWidgets.QMessageBox.Warning, parent=window) if not ch: # user clicked cancel @@ -196,7 +199,7 @@ class PresetManager(QtWidgets.QDialog): ch = self.parent.showMessage( msg='Really delete %s?' % name, showCancel=True, - icon=QtGui.QMessageBox.Warning, + icon=QtWidgets.QMessageBox.Warning, parent=self.window ) if not ch: @@ -223,11 +226,11 @@ class PresetManager(QtWidgets.QDialog): while True: index = presetList.currentRow() - newName, OK = QtGui.QInputDialog.getText( + newName, OK = QtWidgets.QInputDialog.getText( self.window, 'Preset Manager', 'Rename Preset:', - QtGui.QLineEdit.Normal, + QtWidgets.QLineEdit.Normal, self.presetRows[index][2] ) if OK: @@ -250,7 +253,7 @@ class PresetManager(QtWidgets.QDialog): break def openImportDialog(self): - filename = QtGui.QFileDialog.getOpenFileName( + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Import Preset File", self.settings.value("presetDir"), "Preset Files (*.avl)") @@ -275,7 +278,7 @@ class PresetManager(QtWidgets.QDialog): def openExportDialog(self): if not self.window.listWidget_presets.selectedItems(): return - filename = QtGui.QFileDialog.getSaveFileName( + filename, _ = QtWidgets.QFileDialog.getSaveFileName( self.window, "Export Preset", self.settings.value("presetDir"), "Preset Files (*.avl)") diff --git a/src/video_thread.py b/src/video_thread.py index 5ea6d21..b45381c 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -121,15 +121,12 @@ class Worker(QtCore.QObject): vencoders = options['video-codecs'][vcodec] aencoders = options['audio-codecs'][acodec] - #print(encoders) for encoder in vencoders: - #print(encoder) if encoder in encoders: vencoder = encoder break for encoder in aencoders: - #print(encoder) if encoder in encoders: aencoder = encoder break @@ -167,10 +164,10 @@ class Worker(QtCore.QObject): numpy.seterr(divide='ignore') # Call preFrameRender on all components - print('Loaded Components:', ", ".join( - ["%s) %s" % (num, str(component)) \ - for num, component in enumerate(reversed(self.components)) - ])) + print('Loaded Components:', ", ".join([ + "%s) %s" % (num, str(component)) + for num, component in enumerate(reversed(self.components)) + ])) self.staticComponents = {} numComps = len(self.components) for compNo, comp in enumerate(self.components): -- cgit v1.2.3 From a2838a0c3898f999e71f76e6e8d5691155438aea Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 25 Jun 2017 10:36:32 -0400 Subject: disable some hotkeys while encoding, more friendly error messages --- src/components/video.py | 28 +++++++----- src/main.py | 9 ++++ src/mainwindow.py | 111 +++++++++++++++++++++++++++++++----------------- src/preview_thread.py | 9 ++-- src/video_thread.py | 9 ++-- 5 files changed, 107 insertions(+), 59 deletions(-) (limited to 'src/main.py') diff --git a/src/components/video.py b/src/components/video.py index 3e87d2e..44f88a5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -86,12 +86,14 @@ class Video: continue except AttributeError as e: self.parent.showMessage( - msg='%s couldn\'t be loaded.' % os.path.basename( + msg='%s couldn\'t be loaded. ' + 'This is a fatal error.' % os.path.basename( self.videoPath ), detail=str(e) ) self.parent.stopVideo() + break self.currentFrame = pipe.stdout.read(self.chunkSize) if len(self.currentFrame) != 0: @@ -258,20 +260,24 @@ def scale(scale, width, height, returntype=None): def finalizeFrame(self, imageData, width, height): - if self.distort: - try: + try: + if self.distort: image = Image.frombytes( 'RGBA', (width, height), imageData) - except ValueError: - print('#### ignored invalid data caused by distortion ####') - image = self.blankFrame(width, height) - else: - image = Image.frombytes( - 'RGBA', - scale(self.scale, width, height, int), - imageData) + else: + image = Image.frombytes( + 'RGBA', + scale(self.scale, width, height, int), + imageData) + + except ValueError: + print( + '### BAD VIDEO SELECTED ###\n' + 'Video will not export with these settings' + ) + return self.blankFrame(width, height) if self.scale != 100 \ or self.xPosition != 0 or self.yPosition != 0: diff --git a/src/main.py b/src/main.py index a8dd562..5b54fc7 100644 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,15 @@ import preview_thread import video_thread +def disableWhenEncoding(func): + def decorator(*args): + if args[0].encoding: + return + else: + return func(*args) + return decorator + + def LoadDefaultSettings(self): self.resolutions = [ '1920x1080', diff --git a/src/mainwindow.py b/src/mainwindow.py index 7fae4ea..76c2b62 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -12,7 +12,7 @@ import core import preview_thread import video_thread from presetmanager import PresetManager -from main import LoadDefaultSettings +from main import LoadDefaultSettings, disableWhenEncoding class PreviewWindow(QtWidgets.QLabel): @@ -54,6 +54,7 @@ class MainWindow(QtWidgets.QMainWindow): self.pages = [] # widgets of component settings self.lastAutosave = time.time() + self.encoding = False # Create data directory, load/create settings self.dataDir = self.core.dataDir @@ -149,16 +150,18 @@ class MainWindow(QtWidgets.QMainWindow): for i, comp in enumerate(self.core.modules): action = self.compMenu.addAction(comp.Component.__doc__) action.triggered.connect( - lambda _, item=i: self.core.insertComponent(0, item, self)) + 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) - + self.changeComponentWidget + ) self.window.pushButton_removeComponent.clicked.connect( - lambda _: self.removeComponent()) + lambda: self.removeComponent() + ) componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) componentList.customContextMenuRequested.connect( @@ -173,7 +176,8 @@ class MainWindow(QtWidgets.QMainWindow): currentRes = i window.comboBox_resolution.setCurrentIndex(currentRes) window.comboBox_resolution.currentIndexChanged.connect( - self.updateResolution) + self.updateResolution + ) self.window.pushButton_listMoveUp.clicked.connect( lambda: self.moveComponent(-1) @@ -185,14 +189,17 @@ class MainWindow(QtWidgets.QMainWindow): # Configure the Projects Menu self.projectMenu = QMenu() self.window.menuButton_newProject = self.projectMenu.addAction( - "New Project") + "New Project" + ) self.window.menuButton_newProject.triggered.connect( - self.createNewProject) - + lambda: self.createNewProject() + ) self.window.menuButton_openProject = self.projectMenu.addAction( - "Open Project") + "Open Project" + ) self.window.menuButton_openProject.triggered.connect( - self.openOpenProjectDialog) + lambda: self.openOpenProjectDialog() + ) action = self.projectMenu.addAction("Save Project") action.triggered.connect(self.saveCurrentProject) @@ -207,6 +214,7 @@ class MainWindow(QtWidgets.QMainWindow): self.openPresetManager ) + self.updateWindowTitle() window.show() if project and project != self.autosavePath: @@ -282,12 +290,15 @@ class MainWindow(QtWidgets.QMainWindow): def updateWindowTitle(self): appName = 'Audio Visualizer' - if self.currentProject: - appName += ' - %s' % \ - os.path.splitext( - os.path.basename(self.currentProject))[0] - if self.autosaveExists(identical=False): - appName += '*' + try: + if self.currentProject: + appName += ' - %s' % \ + os.path.splitext( + os.path.basename(self.currentProject))[0] + if self.autosaveExists(identical=False): + appName += '*' + except AttributeError: + pass self.window.setWindowTitle(appName) @QtCore.pyqtSlot(int, dict) @@ -347,7 +358,7 @@ class MainWindow(QtWidgets.QMainWindow): if not self.currentProject: if os.path.exists(self.autosavePath): os.remove(self.autosavePath) - elif force or time.time() - self.lastAutosave >= 2.0: + elif force or time.time() - self.lastAutosave >= 0.1: self.core.createProjectFile(self.autosavePath) self.lastAutosave = time.time() @@ -393,7 +404,7 @@ class MainWindow(QtWidgets.QMainWindow): "Video Files (%s);; All Files (*)" % " ".join( self.core.videoFormats)) - if not fileName == "": + if fileName: self.settings.setValue("outputDir", os.path.dirname(fileName)) self.window.lineEdit_outputFile.setText(fileName) @@ -404,33 +415,50 @@ class MainWindow(QtWidgets.QMainWindow): def createAudioVisualisation(self): # create output video if mandatory settings are filled in - if self.window.lineEdit_audioFile.text() and \ - self.window.lineEdit_outputFile.text(): - self.canceled = False - self.progressBarUpdated(-1) - self.videoThread = QtCore.QThread(self) - self.videoWorker = video_thread.Worker(self) - self.videoWorker.moveToThread(self.videoThread) - self.videoWorker.videoCreated.connect(self.videoCreated) - 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.videoThread.start() - outputPath = self.window.lineEdit_outputFile.text() + 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) - self.videoTask.emit( - self.window.lineEdit_audioFile.text(), - outputPath, - self.core.selectedComponents) + 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.' + ) + return else: - self.showMessage( - msg="You must select an audio file and output filename.") + 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.videoThread = QtCore.QThread(self) + self.videoWorker = video_thread.Worker(self) + self.videoWorker.moveToThread(self.videoThread) + self.videoWorker.videoCreated.connect(self.videoCreated) + 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.videoThread.start() + self.videoTask.emit( + audioFile, + outputPath, + self.core.selectedComponents) def changeEncodingStatus(self, status): + self.encoding = status if status: self.window.pushButton_createVideo.setEnabled(False) self.window.pushButton_Cancel.setEnabled(True) @@ -598,6 +626,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window.stackedWidget.removeWidget(widget) self.pages = [] + @disableWhenEncoding def createNewProject(self): self.openSaveChangesDialog('starting a new project') @@ -644,6 +673,7 @@ class MainWindow(QtWidgets.QMainWindow): self.core.createProjectFile(filename) self.updateWindowTitle() + @disableWhenEncoding def openOpenProjectDialog(self): filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.window, "Open Project File", @@ -669,6 +699,7 @@ class MainWindow(QtWidgets.QMainWindow): if self.window.listWidget_componentList.count() == 0: self.drawPreview() self.autosave(True) + self.updateWindowTitle() def showMessage(self, **kwargs): parent = kwargs['parent'] if 'parent' in kwargs else self.window diff --git a/src/preview_thread.py b/src/preview_thread.py index ac5751d..769656b 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -50,16 +50,15 @@ class Worker(QtCore.QObject): components = nextPreviewInformation["components"] for component in reversed(components): try: - newFrame = component.previewRender(self) frame = Image.alpha_composite( - frame, newFrame) - except ValueError: + frame, component.previewRender(self) + ) + except ValueError as e: self.parent.showMessage( msg="Bad frame returned by %s's previewRender method. " "This is a fatal error." % str(component), - detail="bad frame: w%s, h%s" % ( - newFrame.width, newFrame.height) + detail=str(e) ) quit(1) diff --git a/src/video_thread.py b/src/video_thread.py index b45381c..9b0bf56 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -2,7 +2,6 @@ from PyQt5 import QtCore, QtGui, uic from PyQt5.QtCore import pyqtSignal, pyqtSlot from PIL import Image, ImageDraw, ImageFont from PIL.ImageQt import ImageQt -import core import numpy import subprocess as sp import sys @@ -13,6 +12,8 @@ import time from copy import copy import signal +import core + class Worker(QtCore.QObject): @@ -87,8 +88,10 @@ class Worker(QtCore.QObject): self.encoding.emit(True) self.components = components self.outputFile = outputFile - self.bgI = 0 # tracked video frame + self.reset() + + self.bgI = 0 # tracked video frame self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) progressBarValue = 0 @@ -171,7 +174,7 @@ class Worker(QtCore.QObject): self.staticComponents = {} numComps = len(self.components) for compNo, comp in enumerate(self.components): - pStr = "Analyzing audio..." + pStr = "Starting components..." self.progressBarSetText.emit(pStr) properties = None properties = comp.preFrameRender( -- cgit v1.2.3 From 252639e9a2ab69e0aceb0caa6ae3ca0a3dfad686 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 25 Jun 2017 18:12:16 -0400 Subject: renamed Original Audio Visualization to Classic Visualizer --- src/components/__base__.py | 5 +++++ src/components/original.py | 6 +++++- src/core.py | 19 +++++++++++++++---- src/main.py | 4 ++-- src/mainwindow.py | 5 +++-- 5 files changed, 30 insertions(+), 9 deletions(-) (limited to 'src/main.py') diff --git a/src/components/__base__.py b/src/components/__base__.py index 84d41c8..9b04157 100644 --- a/src/components/__base__.py +++ b/src/components/__base__.py @@ -144,6 +144,11 @@ class Component(QtCore.QObject): height = int(self.worker.core.settings.value('outputHeight')) image = Image.new("RGBA", (width, height), (0,0,0,0)) return image + + @classmethod + def names(cls): + # Alternative names for renaming a component between project files + return [] ''' diff --git a/src/components/original.py b/src/components/original.py index 0185e0d..8450aa1 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -9,10 +9,14 @@ from copy import copy class Component(__base__.Component): - '''Original Audio Visualization''' + '''Classic Visualizer''' modified = QtCore.pyqtSignal(int, dict) + @classmethod + def names(cls): + return ['Original Audio Visualization'] + def widget(self, parent): self.parent = parent self.visColor = (255, 255, 255) diff --git a/src/core.py b/src/core.py index 47fa01a..b3c5640 100644 --- a/src/core.py +++ b/src/core.py @@ -1,5 +1,4 @@ import sys -import io import os from PyQt5 import QtCore, QtGui, uic from os.path import expanduser @@ -81,8 +80,15 @@ class Core(): import_module('components.%s' % name) for name in findComponents() ] + # store canonical module names and indexes self.moduleIndexes = [i for i in range(len(self.modules))] self.compNames = [mod.Component.__doc__ for mod in self.modules] + self.altCompNames = [] + # store alternative names for modules + for i, mod in enumerate(self.modules): + if hasattr(mod.Component, 'names'): + for name in mod.Component.names(): + self.altCompNames.append((name, i)) def componentListChanged(self): for i, component in enumerate(self.selectedComponents): @@ -132,8 +138,13 @@ class Core(): self.selectedComponents[i].update() def moduleIndexFor(self, compName): - index = self.compNames.index(compName) - return self.moduleIndexes[index] + try: + index = self.compNames.index(compName) + return self.moduleIndexes[index] + except ValueError: + for altName, modI in self.altCompNames: + if altName == compName: + return self.moduleIndexes[modI] def clearPreset(self, compIndex): self.selectedComponents[compIndex].currentPreset = None @@ -247,7 +258,7 @@ class Core(): print('file missing value: %s' % value) return if hasattr(loader, 'createNewProject'): - loader.createNewProject() + loader.createNewProject(prompt=False) import traceback msg = '%s: %s\n\nTraceback:\n' % (typ.__name__, value) msg += "\n".join(traceback.format_tb(tb)) diff --git a/src/main.py b/src/main.py index 5b54fc7..fd32b13 100644 --- a/src/main.py +++ b/src/main.py @@ -8,11 +8,11 @@ import video_thread def disableWhenEncoding(func): - def decorator(*args): + def decorator(*args, **kwargs): if args[0].encoding: return else: - return func(*args) + return func(*args, **kwargs) return decorator diff --git a/src/mainwindow.py b/src/mainwindow.py index 203992b..a39f344 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -644,8 +644,9 @@ class MainWindow(QtWidgets.QMainWindow): field.blockSignals(False) @disableWhenEncoding - def createNewProject(self): - self.openSaveChangesDialog('starting a new project') + def createNewProject(self, prompt=True): + if prompt: + self.openSaveChangesDialog('starting a new project') self.clear() self.currentProject = None -- cgit v1.2.3 From 38557f29f91b8abc68ec3408ce466ee8a5da815e Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 2 Jul 2017 14:19:15 -0400 Subject: rm unneeded imports, work on freezing --- .gitignore | 7 ++++++- freeze.py | 35 +++++++++++++++++++++-------------- src/components/__base__.py | 2 +- src/components/text.py | 7 +++++-- src/core.py | 26 +++++++++++++++++--------- src/main.py | 2 +- 6 files changed, 51 insertions(+), 28 deletions(-) (limited to 'src/main.py') diff --git a/.gitignore b/.gitignore index 0316a98..68dffc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ __pycache__ -settings.ini build/* +env/* .vscode/* *.mkv *.mp4 +*.zip +*.tar +*.tar.* +*.exe +ffmpeg diff --git a/freeze.py b/freeze.py index 48034dc..a81f325 100644 --- a/freeze.py +++ b/freeze.py @@ -1,11 +1,14 @@ from cx_Freeze import setup, Executable import sys +import os # Dependencies are automatically detected, but it might need # fine tuning. +deps = [os.path.join('src', p) for p in os.listdir('src') if p] +deps.append('ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg') + buildOptions = dict( - packages=[], excludes=[ "apport", "apt", @@ -17,17 +20,21 @@ buildOptions = dict( "xmlrpc", "nose" ], - include_files=[ - "mainwindow.ui", - "presetmanager.ui", - "background.png", - "encoder-options.json", - "components/" - ], includes=[ - 'numpy.core._methods', - 'numpy.lib.format' - ] + "encodings", + "json", + "filecmp", + "numpy.core._methods", + "numpy.lib.format", + "PyQt5.QtCore", + "PyQt5.QtGui", + "PyQt5.QtWidgets", + "PyQt5.uic", + "PIL.Image", + "PIL.ImageQt", + "PIL.ImageDraw", + ], + include_files=deps, ) @@ -35,16 +42,16 @@ base = 'Win32GUI' if sys.platform == 'win32' else None executables = [ Executable( - 'main.py', + 'src/main.py', base=base, targetName='audio-visualizer-python' - ) + ), ] setup( name='audio-visualizer-python', - version='1.0', + version='2.0', description='GUI tool to render visualization videos of audio files', options=dict(build_exe=buildOptions), executables=executables diff --git a/src/components/__base__.py b/src/components/__base__.py index 00601e7..b5e7d93 100644 --- a/src/components/__base__.py +++ b/src/components/__base__.py @@ -1,4 +1,4 @@ -from PyQt5 import uic, QtGui, QtCore, QtWidgets +from PyQt5 import uic, QtCore, QtWidgets from PIL import Image import os diff --git a/src/components/text.py b/src/components/text.py index 96421e6..6c5c4eb 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -3,7 +3,7 @@ from PyQt5.QtGui import QPainter, QColor, QFont from PyQt5 import QtGui, QtCore, QtWidgets from PIL.ImageQt import ImageQt import os -import io +import sys from . import __base__ @@ -136,7 +136,10 @@ class Component(__base__.Component): painter = QPainter(image) self.titleFont.setPixelSize(self.fontSize) painter.setFont(self.titleFont) - painter.setPen(QColor(*self.textColor[::-1])) + if sys.byteorder == 'big': + painter.setPen(QColor(*self.textColor)) + else: + painter.setPen(QColor(*self.textColor[::-1])) painter.drawText(x, y, self.title) painter.end() diff --git a/src/core.py b/src/core.py index b3c5640..3fa67db 100644 --- a/src/core.py +++ b/src/core.py @@ -17,7 +17,6 @@ import string class Core(): def __init__(self): - self.FFMPEG_BIN = self.findFfmpeg() self.dataDir = QStandardPaths.writableLocation( QStandardPaths.AppConfigLocation ) @@ -63,6 +62,7 @@ class Core(): '*.xpm', ]) + self.FFMPEG_BIN = self.findFfmpeg() self.findComponents() self.selectedComponents = [] # copies of named presets to detect modification @@ -437,15 +437,23 @@ class Core(): self.encoder_options = json.load(json_file) def findFfmpeg(self): - if sys.platform == "win32": - return "ffmpeg.exe" + if getattr(sys, 'frozen', False): + # The application is frozen + if sys.platform == "win32": + return os.path.join(self.wd, 'ffmpeg.exe') + else: + return os.path.join(self.wd, 'ffmpeg') + else: - try: - with open(os.devnull, "w") as f: - sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f) - return "ffmpeg" - except: - return "avconv" + if sys.platform == "win32": + return "ffmpeg.exe" + else: + try: + with open(os.devnull, "w") as f: + sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f) + return "ffmpeg" + except: + return "avconv" def readAudioFile(self, filename, parent): command = [self.FFMPEG_BIN, '-i', filename] diff --git a/src/main.py b/src/main.py index fd32b13..bae9adf 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,4 @@ -from PyQt5 import QtGui, uic, QtWidgets +from PyQt5 import uic, QtWidgets import sys import os -- cgit v1.2.3 From ba0409829de62b745d6f87749572a416061a42b4 Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 4 Jul 2017 19:52:52 -0400 Subject: moved functions into toolkit, fixed CMD appearing on Windows --- src/command.py | 2 +- src/components/video.py | 5 +-- src/core.py | 65 +++++++++++++----------------------- src/core.pyc | Bin 0 -> 15050 bytes src/main.py | 35 -------------------- src/mainwindow.py | 4 +-- src/presetmanager.py | 5 +-- src/toolkit.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ src/video_thread.py | 3 +- 9 files changed, 119 insertions(+), 85 deletions(-) create mode 100644 src/core.pyc create mode 100644 src/toolkit.py (limited to 'src/main.py') diff --git a/src/command.py b/src/command.py index 3eea1b6..ee0e48d 100644 --- a/src/command.py +++ b/src/command.py @@ -5,7 +5,7 @@ import sys import core import video_thread -from main import LoadDefaultSettings +from toolkit import LoadDefaultSettings class Command(QtCore.QObject): diff --git a/src/components/video.py b/src/components/video.py index 175cf29..19a9106 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -8,6 +8,7 @@ from queue import PriorityQueue from component import Component, BadComponentInit from frame import BlankFrame +from toolkit import openPipe class Video: @@ -72,7 +73,7 @@ class Video: self.frameBuffer.task_done() def fillBuffer(self): - pipe = subprocess.Popen( + pipe = openPipe( self.command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 ) @@ -217,7 +218,7 @@ class Component(Component): '-ss', '90', '-vframes', '1', ] - pipe = subprocess.Popen( + pipe = openPipe( command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 ) diff --git a/src/core.py b/src/core.py index 3fa67db..9ea9666 100644 --- a/src/core.py +++ b/src/core.py @@ -1,21 +1,24 @@ +''' + Home to the Core class which tracks the program state +''' import sys import os from PyQt5 import QtCore, QtGui, uic -from os.path import expanduser import subprocess as sp import numpy -from PIL import Image -from shutil import rmtree -import time -from collections import OrderedDict import json from importlib import import_module from PyQt5.QtCore import QStandardPaths -import string +import toolkit -class Core(): +class Core: + ''' + MainWindow and Command module both use an instance of this class + to store the program state. This object tracks the components, + opens projects and presets, and stores settings/paths to data. + ''' def __init__(self): self.dataDir = QStandardPaths.writableLocation( QStandardPaths.AppConfigLocation @@ -34,7 +37,7 @@ class Core(): ) self.loadEncoderOptions() - self.videoFormats = Core.appendUppercase([ + self.videoFormats = toolkit.appendUppercase([ '*.mp4', '*.mov', '*.mkv', @@ -42,7 +45,7 @@ class Core(): '*.webm', '*.flv', ]) - self.audioFormats = Core.appendUppercase([ + self.audioFormats = toolkit.appendUppercase([ '*.mp3', '*.wav', '*.ogg', @@ -50,7 +53,7 @@ class Core(): '*.flac', '*.aac', ]) - self.imageFormats = Core.appendUppercase([ + self.imageFormats = toolkit.appendUppercase([ '*.png', '*.jpg', '*.tif', @@ -175,7 +178,7 @@ class Core(): return False with open(filepath, 'r') as f: for line in f: - saveValueStore = Core.presetFromString(line.strip()) + saveValueStore = toolkit.presetFromString(line.strip()) break return saveValueStore @@ -307,7 +310,7 @@ class Core(): lastCompVers = str(line) i += 1 elif i == 2: - lastCompPreset = Core.presetFromString(line) + lastCompPreset = toolkit.presetFromString(line) data[section].append(( lastCompName, lastCompVers, @@ -357,7 +360,7 @@ class Core(): with open(internalPath, 'r') as f: internalData = [line for line in f] try: - saveValueStore = Core.presetFromString(internalData[0].strip()) + saveValueStore = toolkit.presetFromString(internalData[0].strip()) self.createPresetFile( compName, vers, origName, saveValueStore, @@ -387,7 +390,7 @@ class Core(): f.write('[Components]\n') f.write('%s\n' % compName) f.write('%s\n' % str(vers)) - f.write(Core.presetToString(saveValueStore)) + f.write(toolkit.presetToString(saveValueStore)) def createProjectFile(self, filepath, window=None): '''Create a project file (.avp) using the current program state''' @@ -411,7 +414,7 @@ class Core(): saveValueStore = comp.savePreset() f.write('%s\n' % str(comp)) f.write('%s\n' % str(comp.version())) - f.write('%s\n' % Core.presetToString(saveValueStore)) + f.write('%s\n' % toolkit.presetToString(saveValueStore)) f.write('\n[Settings]\n') for key in self.settings.allKeys(): @@ -450,7 +453,9 @@ class Core(): else: try: with open(os.devnull, "w") as f: - sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f) + sp.check_call( + ['ffmpeg', '-version'], stdout=f, stderr=f + ) return "ffmpeg" except: return "avconv" @@ -459,10 +464,9 @@ class Core(): command = [self.FFMPEG_BIN, '-i', filename] try: - fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False) + fileInfo = toolkit.checkOutput(command, stderr=sp.STDOUT) except sp.CalledProcessError as ex: fileInfo = ex.output - pass info = fileInfo.decode("utf-8").split('\n') for line in info: @@ -480,7 +484,7 @@ class Core(): '-ar', '44100', # ouput will have 44100 Hz '-ac', '1', # mono (set to '2' for stereo) '-'] - in_pipe = sp.Popen( + in_pipe = toolkit.openPipe( command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8) completeAudioArray = numpy.empty(0, dtype="int16") @@ -525,26 +529,3 @@ class Core(): def reset(self): self.canceled = False - - @staticmethod - def badName(name): - '''Returns whether a name contains non-alphanumeric chars''' - return any([letter in string.punctuation for letter in name]) - - @staticmethod - def presetToString(dictionary): - '''Alphabetizes a dict into OrderedDict & returns string repr''' - return repr( - OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) - ) - - @staticmethod - def presetFromString(string): - '''Turns a string repr of OrderedDict into a regular dict''' - return dict(eval(string)) - - @staticmethod - def appendUppercase(lst): - for form, i in zip(lst, range(len(lst))): - lst.append(form.upper()) - return lst diff --git a/src/core.pyc b/src/core.pyc new file mode 100644 index 0000000..ce68831 Binary files /dev/null and b/src/core.pyc differ diff --git a/src/main.py b/src/main.py index bae9adf..b0ece29 100644 --- a/src/main.py +++ b/src/main.py @@ -7,41 +7,6 @@ import preview_thread import video_thread -def disableWhenEncoding(func): - def decorator(*args, **kwargs): - if args[0].encoding: - return - else: - return func(*args, **kwargs) - return decorator - - -def LoadDefaultSettings(self): - self.resolutions = [ - '1920x1080', - '1280x720', - '854x480' - ] - - default = { - "outputWidth": 1280, - "outputHeight": 720, - "outputFrameRate": 30, - "outputAudioCodec": "AAC", - "outputAudioBitrate": "192", - "outputVideoCodec": "H264", - "outputVideoBitrate": "2500", - "outputVideoFormat": "yuv420p", - "outputPreset": "medium", - "outputFormat": "mp4", - "outputContainer": "MP4", - "projectDir": os.path.join(self.dataDir, 'projects'), - } - - for parm, value in default.items(): - if self.settings.value(parm) is None: - self.settings.setValue(parm, value) - if __name__ == "__main__": mode = 'gui' if len(sys.argv) > 2: diff --git a/src/mainwindow.py b/src/mainwindow.py index 5068108..e8a3221 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -1,6 +1,6 @@ -from queue import Queue from PyQt5 import QtCore, QtGui, uic, QtWidgets from PyQt5.QtWidgets import QMenu, QShortcut +from queue import Queue import sys import os import signal @@ -11,7 +11,7 @@ import core import preview_thread import video_thread from presetmanager import PresetManager -from main import LoadDefaultSettings, disableWhenEncoding +from toolkit import LoadDefaultSettings, disableWhenEncoding class PreviewWindow(QtWidgets.QLabel): diff --git a/src/presetmanager.py b/src/presetmanager.py index 68679ec..805b93e 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -3,6 +3,7 @@ import string import os import core +import toolkit class PresetManager(QtWidgets.QDialog): @@ -147,7 +148,7 @@ class PresetManager(QtWidgets.QDialog): currentPreset ) if OK: - if core.Core.badName(newName): + if toolkit.badName(newName): self.warnMessage(self.parent.window) continue if newName: @@ -252,7 +253,7 @@ class PresetManager(QtWidgets.QDialog): self.presetRows[index][2] ) if OK: - if core.Core.badName(newName): + if toolkit.badName(newName): self.warnMessage() continue if newName: diff --git a/src/toolkit.py b/src/toolkit.py new file mode 100644 index 0000000..8dce645 --- /dev/null +++ b/src/toolkit.py @@ -0,0 +1,85 @@ +''' + Common functions +''' +import string +import os +import sys +import subprocess +from collections import OrderedDict + + +def badName(name): + '''Returns whether a name contains non-alphanumeric chars''' + return any([letter in string.punctuation for letter in name]) + + +def presetToString(dictionary): + '''Alphabetizes a dict into OrderedDict & returns string repr''' + return repr( + OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + ) + + +def presetFromString(string): + '''Turns a string repr of OrderedDict into a regular dict''' + return dict(eval(string)) + + +def appendUppercase(lst): + for form, i in zip(lst, range(len(lst))): + lst.append(form.upper()) + return lst + + +def checkOutput(commandList, **kwargs): + _subprocess(subprocess.check_output) + + +def openPipe(commandList, **kwargs): + _subprocess(subprocess.Popen) + + +def _subprocess(func, commandList, **kwargs): + if not sys.platform == 'win32': + # Stop CMD window from appearing on Windows + # http://code.activestate.com/recipes/409002/ + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + kwargs['startupinfo'] = startupinfo + return func(commandList, shell=False, **kwargs) + + +def disableWhenEncoding(func): + def decorator(*args, **kwargs): + if args[0].encoding: + return + else: + return func(*args, **kwargs) + return decorator + + +def LoadDefaultSettings(self): + self.resolutions = [ + '1920x1080', + '1280x720', + '854x480' + ] + + default = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "AAC", + "outputAudioBitrate": "192", + "outputVideoCodec": "H264", + "outputVideoBitrate": "2500", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "outputContainer": "MP4", + "projectDir": os.path.join(self.dataDir, 'projects'), + } + + for parm, value in default.items(): + if self.settings.value(parm) is None: + self.settings.setValue(parm, value) diff --git a/src/video_thread.py b/src/video_thread.py index 9b0bf56..aed4d60 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -13,6 +13,7 @@ from copy import copy import signal import core +from toolkit import openPipe class Worker(QtCore.QObject): @@ -191,7 +192,7 @@ class Worker(QtCore.QObject): self.progressBarUpdate.emit(100) # Create ffmpeg pipe and queues for frames - self.out_pipe = sp.Popen( + self.out_pipe = openPipe( ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) self.compositeQueue = Queue() self.compositeQueue.maxsize = 20 -- cgit v1.2.3 From bcb8f27c2e4434d2296dcd66bf279b76ee0d0a4f Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 15 Jul 2017 13:13:53 -0400 Subject: use -t on inputs so ffmpeg knows when to stop filters + better feedback in cmd mode --- src/command.py | 20 ++++++++++++++++++++ src/components/sound.py | 5 +++++ src/components/video.py | 38 ++++++++++++++++++++++++++------------ src/core.py | 8 ++++++-- src/main.py | 11 ++++++----- src/video_thread.py | 8 ++++---- 6 files changed, 67 insertions(+), 23 deletions(-) (limited to 'src/main.py') diff --git a/src/command.py b/src/command.py index 41618f8..84d798d 100644 --- a/src/command.py +++ b/src/command.py @@ -7,6 +7,7 @@ from PyQt5 import QtCore import argparse import os import sys +import time import core from toolkit import LoadDefaultSettings @@ -118,8 +119,27 @@ class Command(QtCore.QObject): self, input, output ) self.worker.videoCreated.connect(self.videoCreated) + self.lastProgressUpdate = time.time() + self.worker.progressBarSetText.connect(self.progressBarSetText) self.createVideo.emit() + @QtCore.pyqtSlot(str) + def progressBarSetText(self, value): + if 'Export ' in value: + # Don't duplicate completion/failure messages + return + if not value.startswith('Exporting') \ + and time.time() - self.lastProgressUpdate >= 0.05: + # Show most messages very often + print(value) + elif time.time() - self.lastProgressUpdate >= 2.0: + # Give user time to read ffmpeg's output during the export + print('##### %s' % value) + else: + return + self.lastProgressUpdate = time.time() + + @QtCore.pyqtSlot() def videoCreated(self): quit(0) diff --git a/src/components/sound.py b/src/components/sound.py index fedc32b..4a5714b 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -79,6 +79,11 @@ class Component(Component): if not arg.startswith('preset=') and '=' in arg: key, arg = arg.split('=', 1) if key == 'path': + if '*%s' % os.path.splitext(arg)[1] \ + not in self.core.audioFormats: + print("Not a supported audio format") + quit(1) self.page.lineEdit_sound.setText(arg) return + super().command(arg) diff --git a/src/components/video.py b/src/components/video.py index b3b6a59..0b93293 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -116,6 +116,7 @@ class Component(Component): page = self.loadUi('video.ui') self.videoPath = '' self.badVideo = False + self.badAudio = False self.x = 0 self.y = 0 self.loopVideo = False @@ -161,22 +162,14 @@ class Component(Component): if self.useAudio: props.append('audio') - # test if an audio stream really exists - audioTestCommand = [ - self.core.FFMPEG_BIN, - '-i', self.videoPath, - '-vn', '-f', 'null', '-' - ] - try: - checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError: - self.badAudio = True + self.testAudioStream() + if self.badAudio: return ['error'] return props def error(self): - if hasattr(self, 'badAudio'): + if self.badAudio: return "Could not identify an audio stream in this video." if not self.videoPath: return "There is no video selected." @@ -185,6 +178,20 @@ class Component(Component): if self.badVideo: return "The video selected is corrupt!" + def testAudioStream(self): + # test if an audio stream really exists + audioTestCommand = [ + self.core.FFMPEG_BIN, + '-i', self.videoPath, + '-vn', '-f', 'null', '-' + ] + try: + checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + self.badAudio = True + else: + self.badAudio = False + def audio(self): return (self.videoPath, {'map': '-v'}) @@ -277,7 +284,7 @@ class Component(Component): if not arg.startswith('preset=') and '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): - if os.path.splitext(arg)[1] in self.core.videoFormats: + if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: self.page.lineEdit_video.setText(arg) self.page.spinBox_scale.setValue(100) self.page.checkBox_loop.setChecked(True) @@ -285,10 +292,17 @@ class Component(Component): else: print("Not a supported video format") quit(1) + elif arg == 'audio': + if not self.page.lineEdit_video.text(): + print("'audio' option must follow a video selection") + quit(1) + self.page.checkBox_useAudio.setChecked(True) + return super().command(arg) def commandHelp(self): print('Load a video:\n path=/filepath/to/video.mp4') + print('Using audio:\n path=/filepath/to/video.mp4 audio') def scale(scale, width, height, returntype=None): diff --git a/src/core.py b/src/core.py index 55bf261..4c12209 100644 --- a/src/core.py +++ b/src/core.py @@ -464,10 +464,11 @@ class Core: except sp.CalledProcessError: return "avconv" - def createFfmpegCommand(self, inputFile, outputFile): + def createFfmpegCommand(self, inputFile, outputFile, duration): ''' Constructs the major ffmpeg command used to export the video ''' + duration = str(duration) # Test if user has libfdk_aac encoders = toolkit.checkOutput( @@ -516,10 +517,12 @@ class Core: ), '-pix_fmt', 'rgba', '-r', self.settings.value('outputFrameRate'), + '-t', duration, '-i', '-', # the video input comes from a pipe '-an', # the video input has no sound # INPUT SOUND + '-t', duration, '-i', inputFile ] @@ -532,6 +535,7 @@ class Core: for streamNo, params in enumerate(extraAudio): extraInputFile, params = params ffmpegCommand.extend([ + '-t', duration, '-i', extraInputFile ]) if 'map' in params and params['map'] == '-v': @@ -632,7 +636,7 @@ class Core: completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray completeAudioArray = completeAudioArrayCopy - return completeAudioArray + return (completeAudioArray, duration) def newVideoWorker(self, loader, audioFile, outputPath): self.videoThread = QtCore.QThread(loader) diff --git a/src/main.py b/src/main.py index b0ece29..2216d2a 100644 --- a/src/main.py +++ b/src/main.py @@ -8,13 +8,13 @@ import video_thread if __name__ == "__main__": - mode = 'gui' + mode = 'GUI' if len(sys.argv) > 2: - mode = 'cmd' + mode = 'commandline' elif len(sys.argv) == 2: if sys.argv[1].startswith('-'): - mode = 'cmd' + mode = 'commandline' else: # opening a project file with gui proj = sys.argv[1] @@ -22,16 +22,17 @@ if __name__ == "__main__": # normal gui launch proj = None + print('Starting Audio Visualizer in %s mode' % mode) app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") # app.setOrganizationName("audio-visualizer") - if mode == 'cmd': + if mode == 'commandline': from command import * main = Command() - elif mode == 'gui': + elif mode == 'GUI': from mainwindow import * import atexit import signal diff --git a/src/video_thread.py b/src/video_thread.py index 5295a3b..674765a 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -31,7 +31,6 @@ class Worker(QtCore.QObject): progressBarSetText = pyqtSignal(str) encoding = pyqtSignal(bool) - def __init__(self, parent, inputFile, outputFile, components): QtCore.QObject.__init__(self) self.core = parent.core @@ -135,7 +134,9 @@ class Worker(QtCore.QObject): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ self.progressBarSetText.emit("Loading audio file...") - self.completeAudioArray = self.core.readAudioFile(self.inputFile, self) + self.completeAudioArray, duration = self.core.readAudioFile( + self.inputFile, self + ) self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") @@ -144,7 +145,6 @@ class Worker(QtCore.QObject): for num, component in enumerate(reversed(self.components)) ])) self.staticComponents = {} - numComps = len(self.components) for compNo, comp in enumerate(reversed(self.components)): comp.preFrameRender( worker=self, @@ -194,7 +194,7 @@ class Worker(QtCore.QObject): self.staticComponents[compNo] = None ffmpegCommand = self.core.createFfmpegCommand( - self.inputFile, self.outputFile + self.inputFile, self.outputFile, duration ) print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand)) print('############################') -- cgit v1.2.3 From 17c8a6703a8093d31c6772ba3b8d9ee01adaa0da Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 15 Jul 2017 18:59:22 -0400 Subject: trying to make setup.py work --- setup.py | 53 +++++++++++++++++++++++++++++++++------------------ src/__init__.py | 0 src/__main__.py | 3 +++ src/main.py | 29 +++++++++++++++------------- src/presetmanager.py | 1 - src/preview_thread.py | 1 - src/video_thread.py | 1 - 7 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/__main__.py (limited to 'src/main.py') diff --git a/setup.py b/setup.py index fde3461..4ef6077 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,34 @@ -+from setuptools import setup, find_packages - - -# Dependencies are automatically detected, but it might need +setup(name='audio_visualizer_python', - -# fine tuning. + version='1.0', - -buildOptions = dict(packages = [], excludes = [ + description='a little GUI tool to render visualization \ - - "apport", + videos of audio files', - - "apt", + license='MIT', - - "ctypes", + url='https://github.com/djfun/audio-visualizer-python', - - "curses", + packages=find_packages(), - - "distutils", + package_data={ - - "email", + 'src': ['*'], - - "html", + }, - - "http", + install_requires=['pillow-simd', 'numpy', ''], - - "json", + entry_points={ - - "xmlrpc", + 'gui_scripts': [ - - "nose" + 'audio-visualizer-python = avpython.main:main' - - ], include_files = ["main.ui"]) + ] - - + } - -import sys + ) \ No newline at end of file +from setuptools import setup +import os + + +def package_files(directory): + paths = [] + for (path, directories, filenames) in os.walk(directory): + for filename in filenames: + paths.append(os.path.join('..', path, filename)) + return paths + + +setup( + name='audio_visualizer_python', + version='2.0.0', + description='A little GUI tool to create audio visualization " \ + "videos out of audio files', + license='MIT', + url='https://github.com/djfun/audio-visualizer-python', + packages=[ + 'avpython', + 'avpython.components' + ], + package_dir={'avpython': 'src'}, + package_data={ + 'avpython': package_files('src'), + }, + install_requires=['olefile', 'Pillow-SIMD', 'PyQt5', 'numpy'], + entry_points={ + 'gui_scripts': [ + 'avp = avpython.main:main' + ], + } +) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..a68739e --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,3 @@ +from avpython.main import main + +main() \ No newline at end of file diff --git a/src/main.py b/src/main.py index 2216d2a..317237c 100644 --- a/src/main.py +++ b/src/main.py @@ -2,12 +2,18 @@ from PyQt5 import uic, QtWidgets import sys import os -import core -import preview_thread -import video_thread +def main(): + if getattr(sys, 'frozen', False): + # frozen + wd = os.path.dirname(sys.executable) + else: + # unfrozen + wd = os.path.dirname(os.path.realpath(__file__)) + + # make local imports work everywhere + sys.path.append(wd) -if __name__ == "__main__": mode = 'GUI' if len(sys.argv) > 2: mode = 'commandline' @@ -28,22 +34,15 @@ if __name__ == "__main__": # app.setOrganizationName("audio-visualizer") if mode == 'commandline': - from command import * + from command import Command main = Command() elif mode == 'GUI': - from mainwindow import * + from mainwindow import MainWindow import atexit import signal - if getattr(sys, 'frozen', False): - # frozen - wd = os.path.dirname(sys.executable) - else: - # unfrozen - wd = os.path.dirname(os.path.realpath(__file__)) - window = uic.loadUi(os.path.join(wd, "mainwindow.ui")) # window.adjustSize() desc = QtWidgets.QDesktopWidget() @@ -64,3 +63,7 @@ if __name__ == "__main__": # applicable to both modes sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() diff --git a/src/presetmanager.py b/src/presetmanager.py index 0028203..6e003a1 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtWidgets import string import os -import core import toolkit diff --git a/src/preview_thread.py b/src/preview_thread.py index 4ffb7f6..6c33aff 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtGui, uic from PyQt5.QtCore import pyqtSignal, pyqtSlot from PIL import Image from PIL.ImageQt import ImageQt -import core from queue import Queue, Empty import os diff --git a/src/video_thread.py b/src/video_thread.py index 674765a..60db99f 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -18,7 +18,6 @@ from threading import Thread, Event import time import signal -import core from toolkit import openPipe from frame import Checkerboard -- cgit v1.2.3 From ec0abd190273b7b636c7085d7caed8220ab09172 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 16 Jul 2017 14:06:11 -0400 Subject: apply complex filters to audio streams from components tons of sound options could be given now, + installation using setup.py --- README.md | 21 +++++----- setup.py | 24 ++++++++--- src/component.py | 5 ++- src/components/sound.py | 23 ++++++++++- src/components/sound.ui | 50 +++++++++++++++++++++++ src/components/video.py | 16 +++++++- src/components/video.ui | 75 +++++++++++++++++++++++++++++++---- src/core.py | 103 ++++++++++++++++++++++++++++++++++++++++-------- src/main.py | 2 +- src/toolkit.py | 11 ++++-- 10 files changed, 283 insertions(+), 47 deletions(-) (limited to 'src/main.py') diff --git a/README.md b/README.md index 658a22d..9149b4f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,31 @@ audio-visualizer-python ======================= +**We need a good name that is not as generic as "audio-visualizer-python"!** -This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. The component setup can be saved as a Project and exporting can be automated using commandline options. +This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. Encoding options can be changed with a variety of different output containers. -The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and send me a pull request and/or file an issue on this project. +Projects can be created from the GUI and used in commandline mode for easy automation of video production. Create a template project named `template` with your typical visualizers and watermarks, and add text to the top layer from commandline: +`avp template -c 99 text "title=Episode 371" -i /this/weeks/audio.ogg -o out` -I also need a good name that is not as generic as "audio-visualizer-python"! +For more information use `avp --help` or for help with a particular component use `avp -c 0 componentName help`. + +The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and submit a pull request and/or file an issue on this project. Dependencies ------------ -Python 3, PyQt5, pillow-simd, numpy, and ffmpeg 3.3 +Python 3.4, FFmpeg 3.3, PyQt5, Pillow-SIMD, NumPy -**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. +**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. For help troubleshooting installation problems, the * For any problems with installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html). Installation ------------ ### Manual installation on Ubuntu 16.04 * Install pip: `sudo apt-get install python3-pip` -* Install [prerequisites to compile Pillow](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-linux):`sudo apt-get install python3-dev python3-setuptools libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk` -* Prerequisites on **Fedora**:`sudo dnf install python3-devel redhat-rpm-config libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel` -* Install dependencies from PyPI: `sudo pip3 install pyqt5 numpy pillow-simd` +* If Pillow is installed, it must be removed. Nothing should break because Pillow-SIMD is simply a drop-in replacement with better performance. +* Download audio-visualizer-python from this repository and run `sudo pip3 install .` in this directory * Install `ffmpeg` from the [website](http://ffmpeg.org/) or from a PPA (e.g. [https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3](https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3)). NOTE: `ffmpeg` in the standard repos is too old (v2.8). Old versions and `avconv` may be used but full functionality is only guaranteed with `ffmpeg` 3.3 or higher. -Download audio-visualizer-python from this repository and run it with `python3 main.py`. +Run the program with `avp` or `python3 -m avpython` ### Manual installation on Windows * **Warning:** [Compiling Pillow is difficult on Windows](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-windows) and required for the best experience. diff --git a/setup.py b/setup.py index 4ef6077..71dc51f 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,25 @@ def package_files(directory): setup( name='audio_visualizer_python', - version='2.0.0', - description='A little GUI tool to create audio visualization " \ - "videos out of audio files', + version='2.0.0rc1', + url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui', license='MIT', - url='https://github.com/djfun/audio-visualizer-python', + description='Create audio visualization videos from a GUI or commandline', + long_description="Create customized audio visualization videos and save " + "them as Projects to continue editing later. Different components can " + "be added and layered to add visualizers, images, videos, gradients, " + "text, etc. Use Projects created in the GUI with commandline mode to " + "automate your video production workflow without learning any complex " + "syntax.", + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3 :: Only', + 'Intended Audience :: End Users/Desktop', + 'Topic :: Multimedia :: Video :: Non-Linear Editor', + ], + keywords=['visualizer', 'visualization', 'commandline video', + 'video editor', 'ffmpeg', 'podcast'] packages=[ 'avpython', 'avpython.components' @@ -25,7 +39,7 @@ setup( package_data={ 'avpython': package_files('src'), }, - install_requires=['olefile', 'Pillow-SIMD', 'PyQt5', 'numpy'], + install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'], entry_points={ 'gui_scripts': [ 'avp = avpython.main:main' diff --git a/src/component.py b/src/component.py index 2b297d1..adb170e 100644 --- a/src/component.py +++ b/src/component.py @@ -178,8 +178,9 @@ class Component(QtCore.QObject): The first element can be: - A string (path to audio file), - Or an object that returns audio data through a pipe - The second element must be a dictionary of ffmpeg parameters - to apply to the input stream. + The second element must be a dictionary of ffmpeg filters/options + to apply to the input stream. See the filter docs for ideas: + https://ffmpeg.org/ffmpeg-filters.html \''' @classmethod diff --git a/src/components/sound.py b/src/components/sound.py index 4a5714b..bd7d002 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -17,12 +17,18 @@ class Component(Component): page.lineEdit_sound.textChanged.connect(self.update) page.pushButton_sound.clicked.connect(self.pickSound) + page.checkBox_chorus.stateChanged.connect(self.update) + page.spinBox_delay.valueChanged.connect(self.update) + page.spinBox_volume.valueChanged.connect(self.update) self.page = page return page def update(self): self.sound = self.page.lineEdit_sound.text() + self.delay = self.page.spinBox_delay.value() + self.volume = self.page.spinBox_volume.value() + self.chorus = self.page.checkBox_chorus.isChecked() super().update() def previewRender(self, previewWorker): @@ -46,7 +52,16 @@ class Component(Component): return "The audio file selected no longer exists!" def audio(self): - return (self.sound, {}) + params = {} + if self.delay != 0.0: + params['adelay'] = '=%s' % str(int(self.delay * 1000.00)) + if self.chorus: + params['chorus'] = \ + '=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3' + if self.volume != 1.0: + params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume) + + return (self.sound, params) def pickSound(self): sndDir = self.settings.value("componentDir", os.path.expanduser("~")) @@ -66,10 +81,16 @@ class Component(Component): def loadPreset(self, pr, presetName=None): super().loadPreset(pr, presetName) self.page.lineEdit_sound.setText(pr['sound']) + self.page.checkBox_chorus.setChecked(pr['chorus']) + self.page.spinBox_delay.setValue(pr['delay']) + self.page.spinBox_volume.setValue(pr['volume']) def savePreset(self): return { 'sound': self.sound, + 'chorus': self.chorus, + 'delay': self.delay, + 'volume': self.volume, } def commandHelp(self): diff --git a/src/components/sound.ui b/src/components/sound.ui index 5fc00c1..4c11332 100644 --- a/src/components/sound.ui +++ b/src/components/sound.ui @@ -87,6 +87,29 @@ + + + + Volume + + + + + + + x + + + 10.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + @@ -100,6 +123,33 @@ + + + + Delay + + + + + + + s + + + 9999999.990000000223517 + + + 0.500000000000000 + + + + + + + Chorus + + + diff --git a/src/components/video.py b/src/components/video.py index 0b93293..e1f182c 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -127,6 +127,7 @@ class Component(Component): page.checkBox_distort.stateChanged.connect(self.update) page.checkBox_useAudio.stateChanged.connect(self.update) page.spinBox_scale.valueChanged.connect(self.update) + page.spinBox_volume.valueChanged.connect(self.update) page.spinBox_x.valueChanged.connect(self.update) page.spinBox_y.valueChanged.connect(self.update) @@ -139,9 +140,17 @@ class Component(Component): self.useAudio = self.page.checkBox_useAudio.isChecked() self.distort = self.page.checkBox_distort.isChecked() self.scale = self.page.spinBox_scale.value() + self.volume = self.page.spinBox_volume.value() self.xPosition = self.page.spinBox_x.value() self.yPosition = self.page.spinBox_y.value() + if self.useAudio: + self.page.label_volume.setEnabled(True) + self.page.spinBox_volume.setEnabled(True) + else: + self.page.label_volume.setEnabled(False) + self.page.spinBox_volume.setEnabled(False) + super().update() def previewRender(self, previewWorker): @@ -193,7 +202,10 @@ class Component(Component): self.badAudio = False def audio(self): - return (self.videoPath, {'map': '-v'}) + params = {} + if self.volume != 1.0: + params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume) + return (self.videoPath, params) def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) @@ -222,6 +234,7 @@ class Component(Component): self.page.checkBox_useAudio.setChecked(pr['useAudio']) self.page.checkBox_distort.setChecked(pr['distort']) self.page.spinBox_scale.setValue(pr['scale']) + self.page.spinBox_volume.setValue(pr['volume']) self.page.spinBox_x.setValue(pr['x']) self.page.spinBox_y.setValue(pr['y']) @@ -233,6 +246,7 @@ class Component(Component): 'useAudio': self.useAudio, 'distort': self.distort, 'scale': self.scale, + 'volume': self.volume, 'x': self.xPosition, 'y': self.yPosition, } diff --git a/src/components/video.ui b/src/components/video.ui index 97b7d6f..08d15d3 100644 --- a/src/components/video.ui +++ b/src/components/video.ui @@ -10,6 +10,18 @@ 197 + + + 0 + 0 + + + + + 0 + 197 + + Form @@ -189,13 +201,6 @@ - - - - Use Audio - - - @@ -247,6 +252,62 @@ + + + + + + Use Audio + + + + + + + Volume + + + + + + + + 0 + 0 + + + + x + + + 0.000000000000000 + + + 10.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/core.py b/src/core.py index 4c12209..324b04f 100644 --- a/src/core.py +++ b/src/core.py @@ -468,7 +468,8 @@ class Core: ''' Constructs the major ffmpeg command used to export the video ''' - duration = str(duration) + safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters + duration = "{0:.3f}".format(duration + 0.1) # used by input sources # Test if user has libfdk_aac encoders = toolkit.checkOutput( @@ -526,35 +527,99 @@ class Core: '-i', inputFile ] + # Add extra audio inputs and any needed avfilters + # NOTE: Global filters are currently hard-coded here for debugging use + globalFilters = 0 # increase to add global filters extraAudio = [ comp.audio() for comp in self.selectedComponents if 'audio' in comp.properties() ] - if extraAudio: - unwantedVideoStreams = [] - for streamNo, params in enumerate(extraAudio): + if extraAudio or globalFilters > 0: + # Add -i options for extra input files + extraFilters = {} + for streamNo, params in enumerate(reversed(extraAudio)): extraInputFile, params = params ffmpegCommand.extend([ - '-t', duration, + '-t', safeDuration, '-i', extraInputFile ]) - if 'map' in params and params['map'] == '-v': - # a video stream to remove - unwantedVideoStreams.append(streamNo + 1) + # Construct dataset of extra filters we'll need to add later + for ffmpegFilter in params: + if streamNo + 2 not in extraFilters: + extraFilters[streamNo + 2] = [] + extraFilters[streamNo + 2].append(( + ffmpegFilter, params[ffmpegFilter] + )) + + # Start creating avfilters! + extraFilterCommand = [] + + if globalFilters <= 0: + # Dictionary of last-used tmp labels for a given stream number + tmpInputs = {streamNo: -1 for streamNo in extraFilters} + else: + # Insert blank entries for global filters into extraFilters + # so the per-stream filters know what input to source later + for streamNo in range(len(extraAudio), 0, -1): + if streamNo + 1 not in extraFilters: + extraFilters[streamNo + 1] = [] + # Also filter the primary audio track + extraFilters[1] = [] + tmpInputs = { + streamNo: globalFilters - 1 + for streamNo in extraFilters + } + + # Add the global filters! + # NOTE: list length must = globalFilters, currently hardcoded + if tmpInputs: + extraFilterCommand.extend([ + '[%s:a] ashowinfo [%stmp0]' % ( + str(streamNo), + str(streamNo) + ) + for streamNo in tmpInputs + ]) + + # Now add the per-stream filters! + for streamNo, paramList in extraFilters.items(): + for param in paramList: + source = '[%s:a]' % str(streamNo) \ + if tmpInputs[streamNo] == -1 else \ + '[%stmp%s]' % ( + str(streamNo), str(tmpInputs[streamNo]) + ) + tmpInputs[streamNo] = tmpInputs[streamNo] + 1 + extraFilterCommand.append( + '%s %s%s [%stmp%s]' % ( + source, param[0], param[1], str(streamNo), + str(tmpInputs[streamNo]) + ) + ) - if unwantedVideoStreams: - ffmpegCommand.extend(['-map', '0']) - for streamNo in unwantedVideoStreams: - ffmpegCommand.extend([ - '-map', '-%s:v' % str(streamNo) - ]) + # Join all the filters together and combine into 1 stream + extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \ + if tmpInputs else '' ffmpegCommand.extend([ '-filter_complex', - 'amix=inputs=%s:duration=first:dropout_transition=3' % str( - len(extraAudio) + 1 + extraFilterCommand + + '%s amix=inputs=%s:duration=first [a]' + % ( + "".join([ + '[%stmp%s]' % (str(i), tmpInputs[i]) + if i in extraFilters else '[%s:a]' % str(i) + for i in range(1, len(extraAudio) + 2) + ]), + str(len(extraAudio) + 1) ), ]) + # Only map audio from the filters, and video from the pipe + ffmpegCommand.extend([ + '-map', '0:v', + '-map', '[a]', + ]) + ffmpegCommand.extend([ # OUTPUT '-vcodec', vencoder, @@ -573,7 +638,7 @@ class Core: ffmpegCommand.append(outputFile) return ffmpegCommand - def readAudioFile(self, filename, parent): + def getAudioDuration(self, filename): command = [self.FFMPEG_BIN, '-i', filename] try: @@ -588,6 +653,10 @@ class Core: d = d.split(' ')[3] d = d.split(':') duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) + return duration + + def readAudioFile(self, filename, parent): + duration = self.getAudioDuration(filename) command = [ self.FFMPEG_BIN, diff --git a/src/main.py b/src/main.py index 317237c..6a9a25e 100644 --- a/src/main.py +++ b/src/main.py @@ -12,7 +12,7 @@ def main(): wd = os.path.dirname(os.path.realpath(__file__)) # make local imports work everywhere - sys.path.append(wd) + sys.path.insert(0, wd) mode = 'GUI' if len(sys.argv) > 2: diff --git a/src/toolkit.py b/src/toolkit.py index 589d8e6..5493f37 100644 --- a/src/toolkit.py +++ b/src/toolkit.py @@ -13,11 +13,14 @@ def badName(name): return any([letter in string.punctuation for letter in name]) +def alphabetizeDict(dictionary): + '''Alphabetizes a dict into OrderedDict ''' + return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + + def presetToString(dictionary): - '''Alphabetizes a dict into OrderedDict & returns string repr''' - return repr( - OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) - ) + '''Returns string repr of a preset''' + return repr(alphabetizeDict(dictionary)) def presetFromString(string): -- cgit v1.2.3 From bf0890e7c87c730b8970c1a20c5b6a9a1a55d203 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 23 Jul 2017 01:53:54 -0400 Subject: components auto-connect & track widgets, less autosave spam importing toolkit from live interpreter now works --- setup.py | 2 +- src/__init__.py | 12 +++ src/command.py | 2 - src/component.py | 196 +++++++++++++++++++++++++++++++++------------ src/components/color.py | 137 +++++++++++-------------------- src/components/image.py | 77 +++++------------- src/components/original.py | 59 ++++++-------- src/components/sound.py | 50 +++--------- src/components/text.py | 81 ++++++++----------- src/components/video.py | 98 +++++++---------------- src/core.py | 196 ++++++++++++++++++++++++++++----------------- src/main.py | 23 ++---- src/mainwindow.py | 125 +++++++++++++++++++---------- src/mainwindow.ui | 3 + src/presetmanager.py | 15 ++-- src/preview_thread.py | 17 ++-- src/toolkit/common.py | 56 +++---------- src/toolkit/core.py | 18 ----- src/toolkit/ffmpeg.py | 46 ++++++++--- src/toolkit/frame.py | 4 +- src/video_thread.py | 7 +- 21 files changed, 604 insertions(+), 620 deletions(-) delete mode 100644 src/toolkit/core.py (limited to 'src/main.py') diff --git a/setup.py b/setup.py index a2d8495..d4f226b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc1' +__version__ = '2.0.0.rc2' def package_files(directory): diff --git a/src/__init__.py b/src/__init__.py index 8b13789..2f4cffa 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +1,13 @@ +import sys +import os + +if getattr(sys, 'frozen', False): + # frozen + wd = os.path.dirname(sys.executable) +else: + # unfrozen + wd = os.path.dirname(os.path.realpath(__file__)) + +# make relative imports work when using /src as a package +sys.path.insert(0, wd) diff --git a/src/command.py b/src/command.py index 046a1bf..ca186e5 100644 --- a/src/command.py +++ b/src/command.py @@ -10,7 +10,6 @@ import sys import time from core import Core -from toolkit import loadDefaultSettings class Command(QtCore.QObject): @@ -55,7 +54,6 @@ class Command(QtCore.QObject): self.args = self.parser.parse_args() self.settings = Core.settings - loadDefaultSettings(self) if self.args.projpath: projPath = self.args.projpath diff --git a/src/component.py b/src/component.py index 92cc65c..bec2df5 100644 --- a/src/component.py +++ b/src/component.py @@ -5,8 +5,28 @@ from PyQt5 import uic, QtCore, QtWidgets import os -from core import Core -from toolkit.common import getPresetDir +from presetmanager import getPresetDir + + +def commandWrapper(func): + '''Intercepts each component's command() method to check for global args''' + def decorator(self, arg): + if arg.startswith('preset='): + _, preset = arg.split('=', 1) + path = os.path.join(getPresetDir(self), preset) + if not os.path.exists(path): + print('Couldn\'t locate preset "%s"' % preset) + quit(1) + else: + print('Opening "%s" preset on layer %s' % ( + preset, self.compPos) + ) + self.core.openPreset(path, self.compPos, preset) + # Don't call the component's command() method + return + else: + return func(self, arg) + return decorator class ComponentMetaclass(type(QtCore.QObject)): @@ -16,10 +36,14 @@ class ComponentMetaclass(type(QtCore.QObject)): E.g., takes only major version from version string & decorates methods ''' def __new__(cls, name, parents, attrs): - # print('Creating %s component' % attrs['name']) + if 'ui' not in attrs: + # use module name as ui filename by default + attrs['ui'] = '%s.ui' % os.path.splitext( + attrs['__module__'].split('.')[-1] + )[0] # Turn certain class methods into properties and classmethods - for key in ('error', 'properties', 'audio', 'commandHelp'): + for key in ('error', 'properties', 'audio'): if key not in attrs: continue attrs[key] = property(attrs[key]) @@ -29,6 +53,10 @@ class ComponentMetaclass(type(QtCore.QObject)): continue attrs[key] = classmethod(key) + # Do not apply these mutations to the base class + if parents[0] != QtCore.QObject: + attrs['command'] = commandWrapper(attrs['command']) + # Turn version string into a number try: if 'version' not in attrs: @@ -54,19 +82,24 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' name = 'Component' + # ui = 'nameOfNonDefaultUiFile' version = '1.0.0' - # The 1st number (before dot, aka the major version) is used to determine + # The major version (before the first dot) is used to determine # preset compatibility; the rest is ignored so it can be non-numeric. modified = QtCore.pyqtSignal(int, dict) # ^ Signal used to tell core program that the component state changed, # you shouldn't need to use this directly, it is used by self.update() - def __init__(self, moduleIndex, compPos): + def __init__(self, moduleIndex, compPos, core): super().__init__() - self.currentPreset = None self.moduleIndex = moduleIndex self.compPos = compPos + self.core = core + self.currentPreset = None + + self._trackedWidgets = {} + self._presetNames = {} # Stop lengthy processes in response to this variable self.canceled = False @@ -114,28 +147,103 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' return [] - def commandHelp(self): - '''Help text as string for this component's commandline arguments''' - # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - def update(self): - '''Read widget values from self.page, then call super().update()''' - self.parent.drawPreview() - saveValueStore = self.savePreset() - saveValueStore['preset'] = self.currentPreset - self.modified.emit(self.compPos, saveValueStore) + def widget(self, parent): + ''' + Call super().widget(*args) to create the component widget + which also auto-connects any common widgets (e.g., checkBoxes) + to self.update(). Then in a subclass connect special actions + (e.g., pushButtons to select a file/colour) and initialize + ''' + self.parent = parent + self.settings = parent.settings + self.page = self.loadUi(self.__class__.ui) + + # Connect widget signals + widgets = { + 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), + 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), + 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), + 'comboBox': self.page.findChildren(QtWidgets.QComboBox), + } + widgets['spinBox'].extend( + self.page.findChildren(QtWidgets.QDoubleSpinBox) + ) + for widget in widgets['lineEdit']: + widget.textChanged.connect(self.update) + for widget in widgets['checkBox']: + widget.stateChanged.connect(self.update) + for widget in widgets['spinBox']: + widget.valueChanged.connect(self.update) + for widget in widgets['comboBox']: + widget.currentIndexChanged.connect(self.update) + + def trackWidgets(self, trackDict, presetNames=None): + ''' + Name widgets to track in update(), savePreset(), and loadPreset() + Accepts a dict with attribute names as keys and widgets as values. + Optional: a dict of attribute names to map to preset variable names + ''' + self._trackedWidgets = trackDict + if type(presetNames) is dict: + self._presetNames = presetNames - def loadPreset(self, presetDict, presetName): + def update(self): ''' - Subclasses take (presetDict, presetName=None) as args. - Must use super().loadPreset(presetDict, presetName) first, + Reads all tracked widget values into instance attributes + and tells the MainWindow that the component was modified. + Call at the END of your method if you need to subclass this. + ''' + for attr, widget in self._trackedWidgets.items(): + if type(widget) == QtWidgets.QLineEdit: + setattr(self, attr, widget.text()) + elif type(widget) == QtWidgets.QSpinBox \ + or type(widget) == QtWidgets.QDoubleSpinBox: + setattr(self, attr, widget.value()) + elif type(widget) == QtWidgets.QCheckBox: + setattr(self, attr, widget.isChecked()) + elif type(widget) == QtWidgets.QComboBox: + setattr(self, attr, widget.currentIndex()) + if not self.core.openingProject: + self.parent.drawPreview() + saveValueStore = self.savePreset() + saveValueStore['preset'] = self.currentPreset + self.modified.emit(self.compPos, saveValueStore) + + def loadPreset(self, presetDict, presetName=None): + ''' + Subclasses should take (presetDict, *args) as args. + Must use super().loadPreset(presetDict, *args) first, then update self.page widgets using the preset dict. ''' self.currentPreset = presetName \ if presetName is not None else presetDict['preset'] + for attr, widget in self._trackedWidgets.items(): + val = presetDict[ + attr if attr not in self._presetNames + else self._presetNames[attr] + ] + if type(widget) == QtWidgets.QLineEdit: + widget.setText(val) + elif type(widget) == QtWidgets.QSpinBox \ + or type(widget) == QtWidgets.QDoubleSpinBox: + widget.setValue(val) + elif type(widget) == QtWidgets.QCheckBox: + widget.setChecked(val) + elif type(widget) == QtWidgets.QComboBox: + widget.setCurrentIndex(val) + + def savePreset(self): + saveValueStore = {} + for attr, widget in self._trackedWidgets.items(): + saveValueStore[ + attr if attr not in self._presetNames + else self._presetNames[attr] + ] = getattr(self, attr) + return saveValueStore def preFrameRender(self, **kwargs): ''' @@ -151,34 +259,27 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): for key, value in kwargs.items(): setattr(self, key, value) - def command(self, arg): + def commandHelp(self): + '''Help text as string for this component's commandline arguments''' + + def command(self, arg=''): ''' - Configure a component using argument from the commandline. - Use super().command(arg) at the end of a subclass's method, - if no arguments are found in that method first + Configure a component using an arg from the commandline. This is + never called if global args like 'preset=' are found in the arg. + So simply check for any non-global args in your component and + call super().command() at the end to get a Help message. ''' - if arg.startswith('preset='): - _, preset = arg.split('=', 1) - path = os.path.join(getPresetDir(self), preset) - if not os.path.exists(path): - print('Couldn\'t locate preset "%s"' % preset) - quit(1) - else: - print('Opening "%s" preset on layer %s' % ( - preset, self.compPos) - ) - self.core.openPreset(path, self.compPos, preset) - else: - print( - self.__doc__, 'Usage:\n' - 'Open a preset for this component:\n' - ' "preset=Preset Name"') - print(self.commandHelp) - quit(0) + print( + self.__class__.name, 'Usage:\n' + 'Open a preset for this component:\n' + ' "preset=Preset Name"' + ) + self.commandHelp() + quit(0) def loadUi(self, filename): '''Load a Qt Designer ui file to use for this component's widget''' - return uic.loadUi(os.path.join(Core.componentsPath, filename)) + return uic.loadUi(os.path.join(self.core.componentsPath, filename)) def cancel(self): '''Stop any lengthy process in response to this variable.''' @@ -191,16 +292,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ### Reference methods for creating a new component ### (Inherit from this class and define these) - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - self.page = self.loadUi('example.ui') - # --- connect widget signals here --- - return self.page - def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) - height = int(previewWorker.core.settings.value('outputHeight')) + height = int(self.settings.value('outputHeight')) from toolkit.frame import BlankFrame image = BlankFrame(width, height) return image @@ -217,7 +311,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): class BadComponentInit(Exception): ''' - General purpose exception components can raise to indicate + General purpose exception that components can raise to indicate a Python issue with e.g., dynamic creation of instances or something. Decorative for now, may have future use for logging. ''' diff --git a/src/components/color.py b/src/components/color.py index 03371e7..8257ed9 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -13,18 +13,15 @@ class Component(Component): name = 'Color' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('color.ui') - + def widget(self, *args): self.color1 = (0, 0, 0) self.color2 = (133, 133, 133) self.x = 0 self.y = 0 + super().widget(*args) - page.lineEdit_color1.setText('%s,%s,%s' % self.color1) - page.lineEdit_color2.setText('%s,%s,%s' % self.color2) + self.page.lineEdit_color1.setText('%s,%s,%s' % self.color1) + self.page.lineEdit_color2.setText('%s,%s,%s' % self.color2) btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color1).name() @@ -32,68 +29,55 @@ class Component(Component): btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color2).name() - page.pushButton_color1.setStyleSheet(btnStyle1) - page.pushButton_color2.setStyleSheet(btnStyle2) - page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) - page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) + self.page.pushButton_color1.setStyleSheet(btnStyle1) + self.page.pushButton_color2.setStyleSheet(btnStyle2) + self.page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) + self.page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) # disable color #2 until non-default 'fill' option gets changed - page.lineEdit_color2.setDisabled(True) - page.pushButton_color2.setDisabled(True) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - page.spinBox_width.setValue( + self.page.lineEdit_color2.setDisabled(True) + self.page.pushButton_color2.setDisabled(True) + self.page.spinBox_width.setValue( int(self.settings.value("outputWidth"))) - page.spinBox_height.setValue( + self.page.spinBox_height.setValue( int(self.settings.value("outputHeight"))) - page.lineEdit_color1.textChanged.connect(self.update) - page.lineEdit_color2.textChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - page.spinBox_width.valueChanged.connect(self.update) - page.spinBox_height.valueChanged.connect(self.update) - page.checkBox_trans.stateChanged.connect(self.update) - self.fillLabels = [ 'Solid', 'Linear Gradient', 'Radial Gradient', ] for label in self.fillLabels: - page.comboBox_fill.addItem(label) - page.comboBox_fill.setCurrentIndex(0) - page.comboBox_fill.currentIndexChanged.connect(self.update) - page.comboBox_spread.currentIndexChanged.connect(self.update) - page.spinBox_radialGradient_end.valueChanged.connect(self.update) - page.spinBox_radialGradient_start.valueChanged.connect(self.update) - page.spinBox_radialGradient_spread.valueChanged.connect(self.update) - page.spinBox_linearGradient_end.valueChanged.connect(self.update) - page.spinBox_linearGradient_start.valueChanged.connect(self.update) - page.checkBox_stretch.stateChanged.connect(self.update) - - self.page = page - return page + self.page.comboBox_fill.addItem(label) + self.page.comboBox_fill.setCurrentIndex(0) + + self.trackWidgets( + { + 'x': self.page.spinBox_x, + 'y': self.page.spinBox_y, + 'sizeWidth': self.page.spinBox_width, + 'sizeHeight': self.page.spinBox_height, + 'trans': self.page.checkBox_trans, + 'spread': self.page.comboBox_spread, + 'stretch': self.page.checkBox_stretch, + 'RG_start': self.page.spinBox_radialGradient_start, + 'LG_start': self.page.spinBox_linearGradient_start, + 'RG_end': self.page.spinBox_radialGradient_end, + 'LG_end': self.page.spinBox_linearGradient_end, + 'RG_centre': self.page.spinBox_radialGradient_spread, + 'fillType': self.page.comboBox_fill, + }, presetNames={ + 'sizeWidth': 'width', + 'sizeHeight': 'height', + } + ) def update(self): self.color1 = rgbFromString(self.page.lineEdit_color1.text()) self.color2 = rgbFromString(self.page.lineEdit_color2.text()) - self.x = self.page.spinBox_x.value() - self.y = self.page.spinBox_y.value() - self.sizeWidth = self.page.spinBox_width.value() - self.sizeHeight = self.page.spinBox_height.value() - self.trans = self.page.checkBox_trans.isChecked() - self.spread = self.page.comboBox_spread.currentIndex() - - self.RG_start = self.page.spinBox_radialGradient_start.value() - self.RG_end = self.page.spinBox_radialGradient_end.value() - self.RG_centre = self.page.spinBox_radialGradient_spread.value() - self.stretch = self.page.checkBox_stretch.isChecked() - self.LG_start = self.page.spinBox_linearGradient_start.value() - self.LG_end = self.page.spinBox_linearGradient_end.value() - - self.fillType = self.page.comboBox_fill.currentIndex() - if self.fillType == 0: + + fillType = self.page.comboBox_fill.currentIndex() + if fillType == 0: self.page.lineEdit_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False) self.page.checkBox_trans.setEnabled(False) @@ -105,10 +89,10 @@ class Component(Component): self.page.checkBox_trans.setEnabled(True) self.page.checkBox_stretch.setEnabled(True) self.page.comboBox_spread.setEnabled(True) - if self.trans: + if self.page.checkBox_trans.isChecked(): self.page.lineEdit_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False) - self.page.fillWidget.setCurrentIndex(self.fillType) + self.page.fillWidget.setCurrentIndex(fillType) super().update() @@ -181,25 +165,11 @@ class Component(Component): return image.finalize() - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) - self.page.comboBox_fill.setCurrentIndex(pr['fillType']) self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - self.page.spinBox_width.setValue(pr['width']) - self.page.spinBox_height.setValue(pr['height']) - self.page.checkBox_trans.setChecked(pr['trans']) - - self.page.spinBox_radialGradient_start.setValue(pr['RG_start']) - self.page.spinBox_radialGradient_end.setValue(pr['RG_end']) - self.page.spinBox_radialGradient_spread.setValue(pr['RG_centre']) - self.page.spinBox_linearGradient_start.setValue(pr['LG_start']) - self.page.spinBox_linearGradient_end.setValue(pr['LG_end']) - self.page.checkBox_stretch.setChecked(pr['stretch']) - self.page.comboBox_spread.setCurrentIndex(pr['spread']) btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['color1']).name() @@ -209,23 +179,10 @@ class Component(Component): self.page.pushButton_color2.setStyleSheet(btnStyle2) def savePreset(self): - return { - 'color1': self.color1, - 'color2': self.color2, - 'x': self.x, - 'y': self.y, - 'fillType': self.fillType, - 'width': self.sizeWidth, - 'height': self.sizeHeight, - 'trans': self.trans, - 'stretch': self.stretch, - 'spread': self.spread, - 'RG_start': self.RG_start, - 'RG_end': self.RG_end, - 'RG_centre': self.RG_centre, - 'LG_start': self.LG_start, - 'LG_end': self.LG_end, - } + saveValueStore = super().savePreset() + saveValueStore['color1'] = self.color1 + saveValueStore['color2'] = self.color2 + return saveValueStore def pickColor(self, num): RGBstring, btnStyle = pickColor() @@ -242,7 +199,7 @@ class Component(Component): print('Specify a color:\n color=255,255,255') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'color': self.page.lineEdit_color1.setText(arg) diff --git a/src/components/image.py b/src/components/image.py index 591e03e..a705904 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,7 +2,6 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os -from core import Core from component import Component from toolkit.frame import BlankFrame @@ -11,35 +10,26 @@ class Component(Component): name = 'Image' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('image.ui') - - page.lineEdit_image.textChanged.connect(self.update) - page.pushButton_image.clicked.connect(self.pickImage) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_rotate.valueChanged.connect(self.update) - page.spinBox_color.valueChanged.connect(self.update) - page.checkBox_stretch.stateChanged.connect(self.update) - page.checkBox_mirror.stateChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - - self.page = page - return page - - def update(self): - self.imagePath = self.page.lineEdit_image.text() - self.scale = self.page.spinBox_scale.value() - self.rotate = self.page.spinBox_rotate.value() - self.color = self.page.spinBox_color.value() - self.xPosition = self.page.spinBox_x.value() - self.yPosition = self.page.spinBox_y.value() - self.stretched = self.page.checkBox_stretch.isChecked() - self.mirror = self.page.checkBox_mirror.isChecked() - - super().update() + def widget(self, *args): + super().widget(*args) + self.page.pushButton_image.clicked.connect(self.pickImage) + self.trackWidgets( + { + 'imagePath': self.page.lineEdit_image, + 'scale': self.page.spinBox_scale, + 'rotate': self.page.spinBox_rotate, + 'color': self.page.spinBox_color, + 'xPosition': self.page.spinBox_x, + 'yPosition': self.page.spinBox_y, + 'stretched': self.page.checkBox_stretch, + 'mirror': self.page.checkBox_mirror, + }, + presetNames={ + 'imagePath': 'image', + 'xPosition': 'x', + 'yPosition': 'y', + }, + ) def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -89,41 +79,18 @@ class Component(Component): return frame - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_image.setText(pr['image']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_color.setValue(pr['color']) - self.page.spinBox_rotate.setValue(pr['rotate']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - self.page.checkBox_stretch.setChecked(pr['stretched']) - self.page.checkBox_mirror.setChecked(pr['mirror']) - - def savePreset(self): - return { - 'image': self.imagePath, - 'scale': self.scale, - 'color': self.color, - 'rotate': self.rotate, - 'stretched': self.stretched, - 'mirror': self.mirror, - 'x': self.xPosition, - 'y': self.yPosition, - } - def pickImage(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Image", imgDir, - "Image Files (%s)" % " ".join(Core.imageFormats)) + "Image Files (%s)" % " ".join(self.core.imageFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) self.update() def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): try: diff --git a/src/components/original.py b/src/components/original.py index ae40df3..2bda878 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -18,59 +18,46 @@ class Component(Component): def names(): return ['Original Audio Visualization'] - def widget(self, parent): - self.parent = parent - self.settings = parent.settings + def widget(self, *args): self.visColor = (255, 255, 255) self.scale = 20 self.y = 0 - self.canceled = False - - page = self.loadUi('original.ui') - page.comboBox_visLayout.addItem("Classic") - page.comboBox_visLayout.addItem("Split") - page.comboBox_visLayout.addItem("Bottom") - page.comboBox_visLayout.addItem("Top") - page.comboBox_visLayout.setCurrentIndex(0) - page.comboBox_visLayout.currentIndexChanged.connect(self.update) - page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) - page.pushButton_visColor.clicked.connect(lambda: self.pickColor()) + super().widget(*args) + + self.page.comboBox_visLayout.addItem("Classic") + self.page.comboBox_visLayout.addItem("Split") + self.page.comboBox_visLayout.addItem("Bottom") + self.page.comboBox_visLayout.addItem("Top") + self.page.comboBox_visLayout.setCurrentIndex(0) + + self.page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) + self.page.pushButton_visColor.clicked.connect(lambda: self.pickColor()) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.visColor).name() - page.pushButton_visColor.setStyleSheet(btnStyle) - page.lineEdit_visColor.textChanged.connect(self.update) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) + self.page.pushButton_visColor.setStyleSheet(btnStyle) - self.page = page - return page + self.trackWidgets({ + 'layout': self.page.comboBox_visLayout, + 'scale': self.page.spinBox_scale, + 'y': self.page.spinBox_y, + }) def update(self): - self.layout = self.page.comboBox_visLayout.currentIndex() self.visColor = rgbFromString(self.page.lineEdit_visColor.text()) - self.scale = self.page.spinBox_scale.value() - self.y = self.page.spinBox_y.value() - super().update() - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['visColor']).name() self.page.pushButton_visColor.setStyleSheet(btnStyle) - self.page.comboBox_visLayout.setCurrentIndex(pr['layout']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_y.setValue(pr['y']) def savePreset(self): - return { - 'layout': self.layout, - 'visColor': self.visColor, - 'scale': self.scale, - 'y': self.y, - } + saveValueStore = super().savePreset() + saveValueStore['visColor'] = self.visColor + return saveValueStore def previewRender(self, previewWorker): spectrum = numpy.fromfunction( @@ -206,7 +193,7 @@ class Component(Component): return im def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) try: if key == 'color': diff --git a/src/components/sound.py b/src/components/sound.py index 677a22f..dd3cbab 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -10,26 +10,15 @@ class Component(Component): name = 'Sound' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('sound.ui') - - page.lineEdit_sound.textChanged.connect(self.update) - page.pushButton_sound.clicked.connect(self.pickSound) - page.checkBox_chorus.stateChanged.connect(self.update) - page.spinBox_delay.valueChanged.connect(self.update) - page.spinBox_volume.valueChanged.connect(self.update) - - self.page = page - return page - - def update(self): - self.sound = self.page.lineEdit_sound.text() - self.delay = self.page.spinBox_delay.value() - self.volume = self.page.spinBox_volume.value() - self.chorus = self.page.checkBox_chorus.isChecked() - super().update() + def widget(self, *args): + super().widget(*args) + self.page.pushButton_sound.clicked.connect(self.pickSound) + self.trackWidgets({ + 'sound': self.page.lineEdit_sound, + 'chorus': self.page.checkBox_chorus, + 'delay': self.page.spinBox_delay, + 'volume': self.page.spinBox_volume, + }) def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -67,7 +56,7 @@ class Component(Component): sndDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Sound", sndDir, - "Audio Files (%s)" % " ".join(Core.audioFormats)) + "Audio Files (%s)" % " ".join(self.core.audioFormats)) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) @@ -78,30 +67,15 @@ class Component(Component): height = int(self.settings.value('outputHeight')) return BlankFrame(width, height) - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_sound.setText(pr['sound']) - self.page.checkBox_chorus.setChecked(pr['chorus']) - self.page.spinBox_delay.setValue(pr['delay']) - self.page.spinBox_volume.setValue(pr['volume']) - - def savePreset(self): - return { - 'sound': self.sound, - 'chorus': self.chorus, - 'delay': self.delay, - 'volume': self.volume, - } - def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path': if '*%s' % os.path.splitext(arg)[1] \ - not in Core.audioFormats: + not in self.core.audioFormats: print("Not a supported audio format") quit(1) self.page.lineEdit_sound.setText(arg) diff --git a/src/components/text.py b/src/components/text.py index d511f22..1d64617 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -16,12 +16,10 @@ class Component(Component): super().__init__(*args) self.titleFont = QFont() - def widget(self, parent): - self.parent = parent - self.settings = parent.settings + def widget(self, *args): + super().widget(*args) height = int(self.settings.value('outputHeight')) width = int(self.settings.value('outputWidth')) - self.textColor = (255, 255, 255) self.title = 'Text' self.alignment = 1 @@ -30,40 +28,35 @@ class Component(Component): self.xPosition = width / 2 - fm.width(self.title)/2 self.yPosition = height / 2 * 1.036 - page = self.loadUi('text.ui') - page.comboBox_textAlign.addItem("Left") - page.comboBox_textAlign.addItem("Middle") - page.comboBox_textAlign.addItem("Right") + self.page.comboBox_textAlign.addItem("Left") + self.page.comboBox_textAlign.addItem("Middle") + self.page.comboBox_textAlign.addItem("Right") - page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) - page.pushButton_textColor.clicked.connect(self.pickColor) + self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) + self.page.pushButton_textColor.clicked.connect(self.pickColor) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.textColor).name() - page.pushButton_textColor.setStyleSheet(btnStyle) - - page.lineEdit_title.setText(self.title) - page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) - page.spinBox_fontSize.setValue(int(self.fontSize)) - page.spinBox_xTextAlign.setValue(int(self.xPosition)) - page.spinBox_yTextAlign.setValue(int(self.yPosition)) - - page.fontComboBox_titleFont.currentFontChanged.connect(self.update) - page.lineEdit_title.textChanged.connect(self.update) - page.comboBox_textAlign.currentIndexChanged.connect(self.update) - page.spinBox_xTextAlign.valueChanged.connect(self.update) - page.spinBox_yTextAlign.valueChanged.connect(self.update) - page.spinBox_fontSize.valueChanged.connect(self.update) - page.lineEdit_textColor.textChanged.connect(self.update) - self.page = page - return page + self.page.pushButton_textColor.setStyleSheet(btnStyle) + + self.page.lineEdit_title.setText(self.title) + self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) + self.page.spinBox_fontSize.setValue(int(self.fontSize)) + self.page.spinBox_xTextAlign.setValue(int(self.xPosition)) + self.page.spinBox_yTextAlign.setValue(int(self.yPosition)) + + self.page.fontComboBox_titleFont.currentFontChanged.connect( + self.update + ) + self.trackWidgets({ + 'title': self.page.lineEdit_title, + 'alignment': self.page.comboBox_textAlign, + 'fontSize': self.page.spinBox_fontSize, + 'xPosition': self.page.spinBox_xTextAlign, + 'yPosition': self.page.spinBox_yTextAlign, + }) def update(self): - self.title = self.page.lineEdit_title.text() - self.alignment = self.page.comboBox_textAlign.currentIndex() self.titleFont = self.page.fontComboBox_titleFont.currentFont() - self.fontSize = self.page.spinBox_fontSize.value() - self.xPosition = self.page.spinBox_xTextAlign.value() - self.yPosition = self.page.spinBox_yTextAlign.value() self.textColor = rgbFromString( self.page.lineEdit_textColor.text()) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ @@ -87,32 +80,22 @@ class Component(Component): x = self.xPosition - offset return x, self.yPosition - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) - self.page.lineEdit_title.setText(pr['title']) font = QFont() font.fromString(pr['titleFont']) self.page.fontComboBox_titleFont.setCurrentFont(font) - self.page.spinBox_fontSize.setValue(pr['fontSize']) - self.page.comboBox_textAlign.setCurrentIndex(pr['alignment']) - self.page.spinBox_xTextAlign.setValue(pr['xPosition']) - self.page.spinBox_yTextAlign.setValue(pr['yPosition']) self.page.lineEdit_textColor.setText('%s,%s,%s' % pr['textColor']) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['textColor']).name() self.page.pushButton_textColor.setStyleSheet(btnStyle) def savePreset(self): - return { - 'title': self.title, - 'titleFont': self.titleFont.toString(), - 'alignment': self.alignment, - 'fontSize': self.fontSize, - 'xPosition': self.xPosition, - 'yPosition': self.yPosition, - 'textColor': self.textColor - } + saveValueStore = super().savePreset() + saveValueStore['titleFont'] = self.titleFont.toString() + saveValueStore['textColor'] = self.textColor + return saveValueStore def previewRender(self, previewWorker): width = int(self.settings.value('outputWidth')) @@ -158,7 +141,7 @@ class Component(Component): print('Set custom x, y position:\n x=500 y=500') def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'color': self.page.lineEdit_textColor.setText(arg) diff --git a/src/components/video.py b/src/components/video.py index 8758b12..677e3ee 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -9,6 +9,7 @@ from queue import PriorityQueue from core import Core from component import Component, BadComponentInit from toolkit.frame import BlankFrame +from toolkit.ffmpeg import testAudioStream from toolkit import openPipe, checkOutput @@ -16,7 +17,7 @@ class Video: '''Video Component Frame-Fetcher''' def __init__(self, **kwargs): mandatoryArgs = [ - 'ffmpeg', # path to ffmpeg, usually Core.FFMPEG_BIN + 'ffmpeg', # path to ffmpeg, usually self.core.FFMPEG_BIN 'videoPath', 'width', 'height', @@ -110,47 +111,40 @@ class Component(Component): name = 'Video' version = '1.0.0' - def widget(self, parent): - self.parent = parent - self.settings = parent.settings - page = self.loadUi('video.ui') + def widget(self, *args): self.videoPath = '' self.badVideo = False self.badAudio = False self.x = 0 self.y = 0 self.loopVideo = False - - page.lineEdit_video.textChanged.connect(self.update) - page.pushButton_video.clicked.connect(self.pickVideo) - page.checkBox_loop.stateChanged.connect(self.update) - page.checkBox_distort.stateChanged.connect(self.update) - page.checkBox_useAudio.stateChanged.connect(self.update) - page.spinBox_scale.valueChanged.connect(self.update) - page.spinBox_volume.valueChanged.connect(self.update) - page.spinBox_x.valueChanged.connect(self.update) - page.spinBox_y.valueChanged.connect(self.update) - - self.page = page - return page + super().widget(*args) + self.page.pushButton_video.clicked.connect(self.pickVideo) + self.trackWidgets( + { + 'videoPath': self.page.lineEdit_video, + 'loopVideo': self.page.checkBox_loop, + 'useAudio': self.page.checkBox_useAudio, + 'distort': self.page.checkBox_distort, + 'scale': self.page.spinBox_scale, + 'volume': self.page.spinBox_volume, + 'xPosition': self.page.spinBox_x, + 'yPosition': self.page.spinBox_y, + }, presetNames={ + 'videoPath': 'video', + 'loopVideo': 'loop', + 'xPosition': 'x', + 'yPosition': 'y', + } + ) def update(self): - self.videoPath = self.page.lineEdit_video.text() - self.loopVideo = self.page.checkBox_loop.isChecked() - self.useAudio = self.page.checkBox_useAudio.isChecked() - self.distort = self.page.checkBox_distort.isChecked() - self.scale = self.page.spinBox_scale.value() - self.volume = self.page.spinBox_volume.value() - self.xPosition = self.page.spinBox_x.value() - self.yPosition = self.page.spinBox_y.value() - - if self.useAudio: + if self.page.checkBox_useAudio.isChecked(): self.page.label_volume.setEnabled(True) self.page.spinBox_volume.setEnabled(True) else: self.page.label_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False) - super().update() def previewRender(self, previewWorker): @@ -188,18 +182,7 @@ class Component(Component): return "The video selected is corrupt!" def testAudioStream(self): - # test if an audio stream really exists - audioTestCommand = [ - Core.FFMPEG_BIN, - '-i', self.videoPath, - '-vn', '-f', 'null', '-' - ] - try: - checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) - except subprocess.CalledProcessError: - self.badAudio = True - else: - self.badAudio = False + self.badAudio = testAudioStream(self.videoPath) def audio(self): params = {} @@ -214,7 +197,7 @@ class Component(Component): self.blankFrame_ = BlankFrame(width, height) self.updateChunksize(width, height) self.video = Video( - ffmpeg=Core.FFMPEG_BIN, videoPath=self.videoPath, + ffmpeg=self.core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, loopVideo=self.loopVideo, @@ -227,34 +210,11 @@ class Component(Component): else: return self.blankFrame_ - def loadPreset(self, pr, presetName=None): - super().loadPreset(pr, presetName) - self.page.lineEdit_video.setText(pr['video']) - self.page.checkBox_loop.setChecked(pr['loop']) - self.page.checkBox_useAudio.setChecked(pr['useAudio']) - self.page.checkBox_distort.setChecked(pr['distort']) - self.page.spinBox_scale.setValue(pr['scale']) - self.page.spinBox_volume.setValue(pr['volume']) - self.page.spinBox_x.setValue(pr['x']) - self.page.spinBox_y.setValue(pr['y']) - - def savePreset(self): - return { - 'video': self.videoPath, - 'loop': self.loopVideo, - 'useAudio': self.useAudio, - 'distort': self.distort, - 'scale': self.scale, - 'volume': self.volume, - 'x': self.xPosition, - 'y': self.yPosition, - } - def pickVideo(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( self.page, "Choose Video", - imgDir, "Video Files (%s)" % " ".join(Core.videoFormats) + imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats) ) if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) @@ -266,7 +226,7 @@ class Component(Component): return command = [ - self.parent.core.FFMPEG_BIN, + self.core.FFMPEG_BIN, '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', @@ -294,10 +254,10 @@ class Component(Component): self.chunkSize = 4*width*height def command(self, arg): - if not arg.startswith('preset=') and '=' in arg: + if '=' in arg: key, arg = arg.split('=', 1) if key == 'path' and os.path.exists(arg): - if '*%s' % os.path.splitext(arg)[1] in Core.videoFormats: + if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: self.page.lineEdit_video.setText(arg) self.page.spinBox_scale.setValue(100) self.page.checkBox_loop.setChecked(True) diff --git a/src/core.py b/src/core.py index f6cf5eb..eb6398b 100644 --- a/src/core.py +++ b/src/core.py @@ -1,5 +1,6 @@ ''' Home to the Core class which tracks program state. Used by GUI & commandline + to create a list of components and create a video thread to export. ''' from PyQt5 import QtCore, QtGui, uic import sys @@ -8,7 +9,6 @@ import json from importlib import import_module import toolkit -from toolkit.ffmpeg import findFfmpeg import video_thread @@ -16,82 +16,21 @@ class Core: ''' MainWindow and Command module both use an instance of this class to store the core program state. This object tracks the components, - talks to the components and handles opening/creating project files - and presets. The class also stores constants as class variables. + talks to the components, handles opening/creating project files + and presets, and creates the video thread to export. + This class also stores constants as class variables. ''' - @classmethod - def storeSettings(cls): - '''Store settings/paths to directories as class variables.''' - if getattr(sys, 'frozen', False): - # frozen - wd = os.path.dirname(sys.executable) - else: - wd = os.path.dirname(os.path.realpath(__file__)) - - dataDir = QtCore.QStandardPaths.writableLocation( - QtCore.QStandardPaths.AppConfigLocation - ) - with open(os.path.join(wd, 'encoder-options.json')) as json_file: - encoderOptions = json.load(json_file) - - settings = { - 'wd': wd, - 'dataDir': dataDir, - 'settings': QtCore.QSettings( - os.path.join(dataDir, 'settings.ini'), - QtCore.QSettings.IniFormat), - 'presetDir': os.path.join(dataDir, 'presets'), - 'componentsPath': os.path.join(wd, 'components'), - 'encoderOptions': encoderOptions, - 'FFMPEG_BIN': findFfmpeg(), - 'canceled': False, - } - - settings['videoFormats'] = toolkit.appendUppercase([ - '*.mp4', - '*.mov', - '*.mkv', - '*.avi', - '*.webm', - '*.flv', - ]) - settings['audioFormats'] = toolkit.appendUppercase([ - '*.mp3', - '*.wav', - '*.ogg', - '*.fla', - '*.flac', - '*.aac', - ]) - settings['imageFormats'] = toolkit.appendUppercase([ - '*.png', - '*.jpg', - '*.tif', - '*.tiff', - '*.gif', - '*.bmp', - '*.ico', - '*.xbm', - '*.xpm', - ]) - - # Register all settings as class variables - for classvar, val in settings.items(): - setattr(cls, classvar, val) - # Make settings accessible to the toolkit package - toolkit.init(settings) - def __init__(self): - Core.storeSettings() - self.findComponents() self.selectedComponents = [] self.savedPresets = {} # copies of presets to detect modification + self.openingProject = False def findComponents(self): + '''Imports all the component modules''' def findComponents(): - for f in sorted(os.listdir(Core.componentsPath)): + for f in os.listdir(Core.componentsPath): name, ext = os.path.splitext(f) if name.startswith("__"): continue @@ -104,8 +43,13 @@ class Core: # store canonical module names and indexes self.moduleIndexes = [i for i in range(len(self.modules))] self.compNames = [mod.Component.name for mod in self.modules] - self.altCompNames = [] + # alphabetize modules by Component name + sortedModules = sorted(zip(self.compNames, self.modules)) + self.compNames = [y[0] for y in sortedModules] + self.modules = [y[1] for y in sortedModules] + # store alternative names for modules + self.altCompNames = [] for i, mod in enumerate(self.modules): if hasattr(mod.Component, 'names'): for name in mod.Component.names(): @@ -116,14 +60,17 @@ class Core: component.compPos = i def insertComponent(self, compPos, moduleIndex, loader): - '''Creates a new component''' + ''' + Creates a new component using these args: + (compPos, moduleIndex in self.modules, MWindow/Command/Core obj) + ''' if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None component = self.modules[moduleIndex].Component( - moduleIndex, compPos + moduleIndex, compPos, self ) self.selectedComponents.insert( compPos, @@ -206,6 +153,7 @@ class Core: errcode, data = self.parseAvFile(filepath) if errcode == 0: + self.openingProject = True try: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: @@ -239,7 +187,8 @@ class Core: i = self.insertComponent( -1, self.moduleIndexFor(name), - loader) + loader + ) if i is None: loader.showMessage(msg="Too many components!") break @@ -284,6 +233,7 @@ class Core: showCancel=False, icon='Warning', detail=msg) + self.openingProject = False def parseAvFile(self, filepath): '''Parses an avp (project) or avl (preset package) file. @@ -467,8 +417,106 @@ class Core: def cancel(self): Core.canceled = True - toolkit.cancel() def reset(self): Core.canceled = False - toolkit.reset() + + @classmethod + def storeSettings(cls): + '''Store settings/paths to directories as class variables''' + from __init__ import wd + from toolkit.ffmpeg import findFfmpeg + + cls.wd = wd + dataDir = QtCore.QStandardPaths.writableLocation( + QtCore.QStandardPaths.AppConfigLocation + ) + with open(os.path.join(wd, 'encoder-options.json')) as json_file: + encoderOptions = json.load(json_file) + + settings = { + 'dataDir': dataDir, + 'settings': QtCore.QSettings( + os.path.join(dataDir, 'settings.ini'), + QtCore.QSettings.IniFormat), + 'presetDir': os.path.join(dataDir, 'presets'), + 'componentsPath': os.path.join(wd, 'components'), + 'encoderOptions': encoderOptions, + 'resolutions': [ + '1920x1080', + '1280x720', + '854x480', + ], + 'windowHasFocus': False, + 'FFMPEG_BIN': findFfmpeg(), + 'canceled': False, + } + + settings['videoFormats'] = toolkit.appendUppercase([ + '*.mp4', + '*.mov', + '*.mkv', + '*.avi', + '*.webm', + '*.flv', + ]) + settings['audioFormats'] = toolkit.appendUppercase([ + '*.mp3', + '*.wav', + '*.ogg', + '*.fla', + '*.flac', + '*.aac', + ]) + settings['imageFormats'] = toolkit.appendUppercase([ + '*.png', + '*.jpg', + '*.tif', + '*.tiff', + '*.gif', + '*.bmp', + '*.ico', + '*.xbm', + '*.xpm', + ]) + + # Register all settings as class variables + for classvar, val in settings.items(): + setattr(cls, classvar, val) + + cls.loadDefaultSettings() + + @classmethod + def loadDefaultSettings(cls): + defaultSettings = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "AAC", + "outputAudioBitrate": "192", + "outputVideoCodec": "H264", + "outputVideoBitrate": "2500", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "outputContainer": "MP4", + "projectDir": os.path.join(cls.dataDir, 'projects'), + "pref_insertCompAtTop": True, + } + + for parm, value in defaultSettings.items(): + if cls.settings.value(parm) is None: + cls.settings.setValue(parm, value) + + # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to + # store True as 'true' but interprets a manually-added 'true' as str.) + for key in cls.settings.allKeys(): + if not key.startswith('pref_'): + continue + val = cls.settings.value(key) + if val in ('true', 'false'): + cls.settings.setValue(key, True if val == 'true' else False) + + +# always store settings in class variables even if a Core object is not created +Core.storeSettings() diff --git a/src/main.py b/src/main.py index 6a9a25e..977da3b 100644 --- a/src/main.py +++ b/src/main.py @@ -2,22 +2,17 @@ from PyQt5 import uic, QtWidgets import sys import os +from __init__ import wd -def main(): - if getattr(sys, 'frozen', False): - # frozen - wd = os.path.dirname(sys.executable) - else: - # unfrozen - wd = os.path.dirname(os.path.realpath(__file__)) - # make local imports work everywhere - sys.path.insert(0, wd) +def main(): + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName("audio-visualizer") + # Determine mode mode = 'GUI' if len(sys.argv) > 2: mode = 'commandline' - elif len(sys.argv) == 2: if sys.argv[1].startswith('-'): mode = 'commandline' @@ -28,11 +23,7 @@ def main(): # normal gui launch proj = None - print('Starting Audio Visualizer in %s mode' % mode) - app = QtWidgets.QApplication(sys.argv) - app.setApplicationName("audio-visualizer") - # app.setOrganizationName("audio-visualizer") - + # Launch program if mode == 'commandline': from command import Command @@ -61,9 +52,7 @@ def main(): signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) - # applicable to both modes sys.exit(app.exec_()) - if __name__ == "__main__": main() diff --git a/src/mainwindow.py b/src/mainwindow.py index 2d598ae..f333513 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -17,7 +17,7 @@ import time from core import Core import preview_thread from presetmanager import PresetManager -from toolkit import loadDefaultSettings, disableWhenEncoding, checkOutput +from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput class PreviewWindow(QtWidgets.QLabel): @@ -25,6 +25,7 @@ class PreviewWindow(QtWidgets.QLabel): Paints the preview QLabel and maintains the aspect ratio when the window is resized. ''' + def __init__(self, parent, img): super(PreviewWindow, self).__init__() self.parent = parent @@ -49,6 +50,14 @@ class PreviewWindow(QtWidgets.QLabel): self.pixmap = QtGui.QPixmap(img) self.repaint() + @QtCore.pyqtSlot(str) + def threadError(self, msg): + self.parent.showMessage( + msg=msg, + icon='Warning', + parent=self + ) + class MainWindow(QtWidgets.QMainWindow): ''' @@ -66,13 +75,16 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = Core() - self.pages = [] # widgets of component settings + # widgets of component settings + self.pages = [] self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + self.autosaveCooldown = 0.2 self.encoding = False # Create data directory, load/create settings @@ -80,7 +92,6 @@ class MainWindow(QtWidgets.QMainWindow): self.presetDir = Core.presetDir self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.settings = Core.settings - loadDefaultSettings(self) self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'presetmanager.ui')), self) @@ -92,13 +103,17 @@ class MainWindow(QtWidgets.QMainWindow): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) - # Make queues/timers for the preview thread + # Create the preview window and its thread, queues, and timers + self.previewWindow = PreviewWindow(self, os.path.join( + Core.wd, "background.png")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + 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.previewWorker.error.connect(self.cleanUp) self.previewThread.start() self.timer = QtCore.QTimer(self) @@ -106,6 +121,7 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.start(500) # Begin decorating the window and connecting events + self.window.installEventFilter(self) componentList = self.window.listWidget_componentList if sys.platform == 'darwin': @@ -168,14 +184,9 @@ class MainWindow(QtWidgets.QMainWindow): window.spinBox_vBitrate.setValue(vBitrate) window.spinBox_aBitrate.setValue(aBitrate) - window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) - self.previewWindow = PreviewWindow(self, os.path.join( - Core.wd, "background.png")) - window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - # Make component buttons self.compMenu = QMenu() for i, comp in enumerate(self.core.modules): @@ -204,7 +215,7 @@ class MainWindow(QtWidgets.QMainWindow): currentRes = str(self.settings.value('outputWidth'))+'x' + \ str(self.settings.value('outputHeight')) - for i, res in enumerate(self.resolutions): + for i, res in enumerate(Core.resolutions): window.comboBox_resolution.addItem(res) if res == currentRes: currentRes = i @@ -375,6 +386,7 @@ class MainWindow(QtWidgets.QMainWindow): self.previewThread.quit() self.previewThread.wait() + @disableWhenOpeningProject def updateWindowTitle(self): appName = 'Audio Visualizer' try: @@ -442,13 +454,29 @@ class MainWindow(QtWidgets.QMainWindow): 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 >= 0.2: + 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) def autosaveExists(self, identical=True): '''Determines if creating the autosave should be blocked.''' @@ -602,15 +630,20 @@ class MainWindow(QtWidgets.QMainWindow): def updateResolution(self): resIndex = int(self.window.comboBox_resolution.currentIndex()) - res = self.resolutions[resIndex].split('x') + res = Core.resolutions[resIndex].split('x') self.settings.setValue('outputWidth', res[0]) self.settings.setValue('outputHeight', res[1]) self.drawPreview() - def drawPreview(self, force=False): + 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() - self.autosave(force) + if force or 'autosave' in kwargs: + if force or kwargs['autosave']: + self.autosave(True) + else: + self.autosave() self.updateWindowTitle() @QtCore.pyqtSlot(QtGui.QImage) @@ -685,9 +718,13 @@ class MainWindow(QtWidgets.QMainWindow): stackedWidget.insertWidget(newRow, page) componentList.setCurrentRow(newRow) stackedWidget.setCurrentIndex(newRow) - self.drawPreview() + self.drawPreview(True) - def getComponentListRects(self): + 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 = [ @@ -698,20 +735,23 @@ class MainWindow(QtWidgets.QMainWindow): componentList.visualRect(modelIndex) for modelIndex in modelIndexes ] - return rects + mousePos = [rect.contains(position) for rect in rects] + if not any(mousePos): + # Not clicking a component + mousePos = -1 + else: + mousePos = mousePos.index(True) + return mousePos @disableWhenEncoding def dragComponent(self, event): '''Used as Qt drop event for the component listwidget''' componentList = self.window.listWidget_componentList - rects = self.getComponentListRects() - - rowPos = [rect.contains(event.pos()) for rect in rects] - if not any(rowPos): - return - - i = rowPos.index(True) - change = (componentList.currentRow() - i) * -1 + 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): @@ -814,9 +854,7 @@ class MainWindow(QtWidgets.QMainWindow): self.settings.setValue("projectDir", os.path.dirname(filepath)) # actually load the project using core method self.core.openProject(self, filepath) - if self.window.listWidget_componentList.count() == 0: - self.drawPreview() - self.autosave(True) + self.drawPreview(autosave=False) self.updateWindowTitle() def showMessage(self, **kwargs): @@ -843,20 +881,11 @@ class MainWindow(QtWidgets.QMainWindow): def componentContextMenu(self, QPos): '''Appears when right-clicking the component list''' componentList = self.window.listWidget_componentList - index = componentList.currentRow() - self.menu = QMenu() parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) - rects = self.getComponentListRects() - rowPos = [rect.contains(QPos) for rect in rects] - if not any(rowPos): - # Insert components at the top if clicking nothing - rowPos = 0 - else: - rowPos = rowPos.index(True) - - if index == rowPos: + index = self.getComponentListMousePos(QPos) + if index > -1: # Show preset menu if clicking a component self.presetManager.findPresets() menuItem = self.menu.addAction("Save Preset") @@ -891,13 +920,23 @@ class MainWindow(QtWidgets.QMainWindow): # "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( - rowPos, item, self + 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 index b491323..b43d375 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -22,6 +22,9 @@ 0 + + Qt::StrongFocus + MainWindow diff --git a/src/presetmanager.py b/src/presetmanager.py index 64e2203..643e180 100644 --- a/src/presetmanager.py +++ b/src/presetmanager.py @@ -6,7 +6,8 @@ from PyQt5 import QtCore, QtWidgets import string import os -import toolkit +from toolkit import badName +from core import Core class PresetManager(QtWidgets.QDialog): @@ -151,7 +152,7 @@ class PresetManager(QtWidgets.QDialog): currentPreset ) if OK: - if toolkit.badName(newName): + if badName(newName): self.warnMessage(self.parent.window) continue if newName: @@ -236,7 +237,6 @@ class PresetManager(QtWidgets.QDialog): os.remove(filepath) def warnMessage(self, window=None): - print(window) self.parent.showMessage( msg='Preset names must contain only letters, ' 'numbers, and spaces.', @@ -272,7 +272,7 @@ class PresetManager(QtWidgets.QDialog): self.presetRows[index][2] ) if OK: - if toolkit.badName(newName): + if badName(newName): self.warnMessage() continue if newName: @@ -289,7 +289,7 @@ class PresetManager(QtWidgets.QDialog): self.findPresets() self.drawPresetList() for i, comp in enumerate(self.core.selectedComponents): - if toolkit.getPresetDir(comp) == path \ + if getPresetDir(comp) == path \ and comp.currentPreset == oldName: self.core.openPreset(newPath, i, newName) self.parent.updateComponentTitle(i, False) @@ -338,3 +338,8 @@ class PresetManager(QtWidgets.QDialog): def clearPresetListSelection(self): self.window.listWidget_presets.setCurrentRow(-1) + + +def getPresetDir(comp): + '''Get the preset subdir for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) diff --git a/src/preview_thread.py b/src/preview_thread.py index 3fc73b3..9917e4b 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -10,12 +10,13 @@ from queue import Queue, Empty import os from toolkit.frame import Checkerboard +from toolkit import disableWhenOpeningProject class Worker(QtCore.QObject): imageCreated = pyqtSignal(QtGui.QImage) - error = pyqtSignal() + error = pyqtSignal(str) def __init__(self, parent=None, queue=None): QtCore.QObject.__init__(self) @@ -30,6 +31,7 @@ class Worker(QtCore.QObject): height = int(self.settings.value('outputHeight')) self.background = Checkerboard(width, height) + @disableWhenOpeningProject @pyqtSlot(list) def createPreviewImage(self, components): dic = { @@ -48,7 +50,6 @@ class Worker(QtCore.QObject): self.queue.get(block=False) except Empty: continue - if self.background.width != width \ or self.background.height != height: self.background = Checkerboard(width, height) @@ -65,20 +66,12 @@ class Worker(QtCore.QObject): except ValueError as e: errMsg = "Bad frame returned by %s's preview renderer. " \ - "%s. New frame size was %s*%s; should be %s*%s. " \ - "This is a fatal error." % ( + "%s. New frame size was %s*%s; should be %s*%s." % ( str(component), str(e).capitalize(), newFrame.width, newFrame.height, width, height ) - print(errMsg) - self.parent.showMessage( - msg=errMsg, - detail=str(e), - icon='Warning', - parent=None # MainWindow is in a different thread - ) - self.error.emit() + self.error.emit(errMsg) break except RuntimeError as e: print(e) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 763d582..5fe601f 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -8,13 +8,6 @@ import sys import subprocess from collections import OrderedDict -from toolkit.core import * - - -def getPresetDir(comp): - '''Get the preset subdirectory for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) - def badName(name): '''Returns whether a name contains non-alphanumeric chars''' @@ -66,14 +59,20 @@ def openPipe(commandList, **kwargs): def disableWhenEncoding(func): - ''' Blocks calls to a function while the video is being exported - in MainWindow. - ''' - def decorator(*args, **kwargs): - if args[0].encoding: + def decorator(self, *args, **kwargs): + if self.encoding: return else: - return func(*args, **kwargs) + return func(self, *args, **kwargs) + return decorator + + +def disableWhenOpeningProject(func): + def decorator(self, *args, **kwargs): + if self.core.openingProject: + return + else: + return func(self, *args, **kwargs) return decorator @@ -108,34 +107,3 @@ def rgbFromString(string): return tup except: return (255, 255, 255) - - -def loadDefaultSettings(self): - ''' - Runs once at each program start-up. Fills in default settings - for any settings not found in settings.ini - ''' - self.resolutions = [ - '1920x1080', - '1280x720', - '854x480' - ] - - default = { - "outputWidth": 1280, - "outputHeight": 720, - "outputFrameRate": 30, - "outputAudioCodec": "AAC", - "outputAudioBitrate": "192", - "outputVideoCodec": "H264", - "outputVideoBitrate": "2500", - "outputVideoFormat": "yuv420p", - "outputPreset": "medium", - "outputFormat": "mp4", - "outputContainer": "MP4", - "projectDir": os.path.join(self.dataDir, 'projects'), - } - - for parm, value in default.items(): - if self.settings.value(parm) is None: - self.settings.setValue(parm, value) diff --git a/src/toolkit/core.py b/src/toolkit/core.py deleted file mode 100644 index a96a684..0000000 --- a/src/toolkit/core.py +++ /dev/null @@ -1,18 +0,0 @@ -class Core: - '''A very complicated class for tracking settings''' - - -def init(settings): - global Core - for classvar, val in settings.items(): - setattr(Core, classvar, val) - - -def cancel(): - global Core - Core.canceled = True - - -def reset(): - global Core - Core.canceled = False diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index cc59a6c..30dc0b3 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -4,18 +4,19 @@ import numpy import sys import os -import subprocess as sp +import subprocess -from toolkit.common import Core, checkOutput, openPipe +import core +from toolkit.common import checkOutput, openPipe def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen if sys.platform == "win32": - return os.path.join(Core.wd, 'ffmpeg.exe') + return os.path.join(core.Core.wd, 'ffmpeg.exe') else: - return os.path.join(Core.wd, 'ffmpeg') + return os.path.join(core.Core.wd, 'ffmpeg') else: if sys.platform == "win32": @@ -27,7 +28,7 @@ def findFfmpeg(): ['ffmpeg', '-version'], stderr=f ) return "ffmpeg" - except sp.CalledProcessError: + except subprocess.CalledProcessError: return "avconv" @@ -37,9 +38,9 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): ''' if duration == -1: duration = getAudioDuration(inputFile) - safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters duration = "{0:.3f}".format(duration + 0.1) # used by input sources + Core = core.Core # Test if user has libfdk_aac encoders = checkOutput( @@ -213,12 +214,28 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): return ffmpegCommand +def testAudioStream(filename): + '''Test if an audio stream definitely exists''' + audioTestCommand = [ + core.Core.FFMPEG_BIN, + '-i', filename, + '-vn', '-f', 'null', '-' + ] + try: + checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + return True + else: + return False + + def getAudioDuration(filename): - command = [Core.FFMPEG_BIN, '-i', filename] + '''Try to get duration of audio file as float, or False if not possible''' + command = [core.Core.FFMPEG_BIN, '-i', filename] try: - fileInfo = checkOutput(command, stderr=sp.STDOUT) - except sp.CalledProcessError as ex: + fileInfo = checkOutput(command, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as ex: fileInfo = ex.output info = fileInfo.decode("utf-8").split('\n') @@ -236,13 +253,17 @@ def getAudioDuration(filename): def readAudioFile(filename, parent): + ''' + Creates the completeAudioArray given to components + and used to draw the classic visualizer. + ''' duration = getAudioDuration(filename) if not duration: print('Audio file doesn\'t exist or unreadable.') return command = [ - Core.FFMPEG_BIN, + core.Core.FFMPEG_BIN, '-i', filename, '-f', 's16le', '-acodec', 'pcm_s16le', @@ -250,7 +271,8 @@ def readAudioFile(filename, parent): '-ac', '1', # mono (set to '2' for stereo) '-'] in_pipe = openPipe( - command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8 + command, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 ) completeAudioArray = numpy.empty(0, dtype="int16") @@ -258,7 +280,7 @@ def readAudioFile(filename, parent): progress = 0 lastPercent = None while True: - if Core.canceled: + if core.Core.canceled: return # read 2 seconds of audio progress += 4 diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 83fd59e..ca2a054 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -7,7 +7,7 @@ from PIL.ImageQt import ImageQt import sys import os -from toolkit.common import Core +import core class FramePainter(QtGui.QPainter): @@ -57,7 +57,7 @@ def Checkerboard(width, height): ''' image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( - os.path.join(Core.wd, "background.png")), + os.path.join(core.Core.wd, "background.png")), (0, 0) ) image = image.resize((width, height)) diff --git a/src/video_thread.py b/src/video_thread.py index 8517b92..7fe3e02 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -18,6 +18,7 @@ from threading import Thread, Event import time import signal +import core from toolkit import openPipe from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard @@ -104,7 +105,8 @@ class Worker(QtCore.QObject): while not self.stopped: audioI, frame = self.previewQueue.get() - if time.time() - self.lastPreview >= 0.06 or audioI == 0: + if core.Core.windowHasFocus \ + and time.time() - self.lastPreview >= 0.06 or audioI == 0: image = Image.alpha_composite(background.copy(), frame) self.imageCreated.emit(QtGui.QImage(ImageQt(image))) self.lastPreview = time.time() @@ -231,7 +233,8 @@ class Worker(QtCore.QObject): self.lastPreview = 0.0 self.previewDispatch = Thread( - target=self.previewDispatch, name="Render Dispatch Thread") + target=self.previewDispatch, name="Render Dispatch Thread" + ) self.previewDispatch.daemon = True self.previewDispatch.start() -- cgit v1.2.3 From c517140a51256169cdcff0a4c2d5973d5f367259 Mon Sep 17 00:00:00 2001 From: rikai Date: Sun, 23 Jul 2017 20:14:10 -0700 Subject: Fixes opening behind other windows on OS X Just a quick fix, this will raise the window to the front after it's created.--- src/main.py | 1 + 1 file changed, 1 insertion(+) (limited to 'src/main.py') diff --git a/src/main.py b/src/main.py index 6a9a25e..8d5a769 100644 --- a/src/main.py +++ b/src/main.py @@ -57,6 +57,7 @@ def main(): # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = MainWindow(window, proj) + window.raise_() signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) -- cgit v1.2.3 From 1c4afc96d69789f16284c067ffd7098dc7b2ca70 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 16:04:41 -0400 Subject: using the builtin logging module --- src/component.py | 14 ++++++--- src/components/spectrum.py | 16 ++++++---- src/components/video.py | 19 +++++++++--- src/components/waveform.py | 18 +++++++++--- src/core.py | 73 +++++++++++++++++++++++++++++++++++++++++----- src/main.py | 6 ++++ src/mainwindow.py | 57 +++++++++++++++++++++++++++--------- src/preview_thread.py | 9 ++++-- src/toolkit/ffmpeg.py | 19 +++++++----- src/toolkit/frame.py | 6 ++++ src/video_thread.py | 24 ++++++++++----- 11 files changed, 206 insertions(+), 55 deletions(-) (limited to 'src/main.py') diff --git a/src/component.py b/src/component.py index 5b6f9a7..a1e24db 100644 --- a/src/component.py +++ b/src/component.py @@ -8,6 +8,7 @@ import os import sys import math import time +import logging from toolkit.frame import BlankFrame from toolkit import ( @@ -15,6 +16,9 @@ from toolkit import ( ) +log = logging.getLogger('AVP.ComponentHandler') + + class ComponentMetaclass(type(QtCore.QObject)): ''' Checks the validity of each Component class and mutates some attrs. @@ -135,17 +139,17 @@ class ComponentMetaclass(type(QtCore.QObject)): # Turn version string into a number try: if 'version' not in attrs: - print( + log.error( 'No version attribute in %s. Defaulting to 1' % attrs['name']) attrs['version'] = 1 else: attrs['version'] = int(attrs['version'].split('.')[0]) except ValueError: - print('%s component has an invalid version string:\n%s' % ( + log.critical('%s component has an invalid version string:\n%s' % ( attrs['name'], str(attrs['version']))) except KeyError: - print('%s component has no version string.' % attrs['name']) + log.critical('%s component has no version string.' % attrs['name']) else: return super().__new__(cls, name, parents, attrs) quit(1) @@ -546,6 +550,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): and oldRelativeVal != newRelativeVal: # Float changed without pixel value changing, which # means the pixel value needs to be updated + log.debug('Updating %s #%s\'s relative widget: %s' % ( + self.name, self.compPos, attr)) self._trackedWidgets[attr].blockSignals(True) self.updateRelativeWidgetMaximum(attr) pixelVal = self.pixelValForAttr(attr, oldRelativeVal) @@ -576,7 +582,7 @@ class ComponentError(RuntimeError): msg = str(sys.exc_info()[1]) else: msg = 'Unknown error.' - print("##### ComponentError by %s's %s: %s" % ( + log.error("ComponentError by %s's %s: %s" % ( caller.name, name, msg)) # Don't create multiple windows for quickly repeated messages diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 666e20a..32763c0 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -4,6 +4,7 @@ import os import math import subprocess import time +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -13,6 +14,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger('AVP.Components.Spectrum') + + class Component(Component): name = 'Spectrum' version = '1.0.0' @@ -68,6 +72,7 @@ class Component(Component): if not changedSize \ and not self.changedOptions \ and self.previewFrame is not None: + log.debug('Comp #%s is reusing old preview frame' % self.compPos) return self.previewFrame frame = self.getPreviewFrame() @@ -131,13 +136,14 @@ class Component(Component): '-frames:v', '1', ]) logFilename = os.path.join( - self.core.dataDir, 'preview_%s.log' % str(self.compPos)) - with open(logFilename, 'w') as log: - log.write(" ".join(command) + '\n\n') - with open(logFilename, 'a') as log: + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: pipe = openPipe( command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=log, bufsize=10**8 + stderr=logf, bufsize=10**8 ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/components/video.py b/src/components/video.py index b6bdd52..a189f60 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -3,6 +3,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os import math import subprocess +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -10,6 +11,9 @@ from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo from toolkit import checkOutput +log = logging.getLogger('AVP.Components.Video') + + class Component(Component): name = 'Video' version = '1.0.0' @@ -134,10 +138,17 @@ class Component(Component): '-ss', '90', '-frames:v', '1', ]) - pipe = openPipe( - command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 - ) + + logFilename = os.path.join( + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=logf, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/components/waveform.py b/src/components/waveform.py index 71cbcac..1517be2 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -4,6 +4,7 @@ from PyQt5.QtGui import QColor import os import math import subprocess +import logging from component import Component from toolkit.frame import BlankFrame, scale @@ -13,6 +14,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger('AVP.Components.Waveform') + + class Component(Component): name = 'Waveform' version = '1.0.0' @@ -106,10 +110,16 @@ class Component(Component): '-codec:v', 'rawvideo', '-', '-frames:v', '1', ]) - pipe = openPipe( - command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 - ) + logFilename = os.path.join( + self.core.logDir, 'preview_%s.log' % str(self.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as logf: + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=logf, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) diff --git a/src/core.py b/src/core.py index 61905eb..4023542 100644 --- a/src/core.py +++ b/src/core.py @@ -7,11 +7,17 @@ import sys import os import json from importlib import import_module +import logging import toolkit import video_thread +log = logging.getLogger('AVP.Core') +STDOUT_LOGLVL = logging.WARNING +FILE_LOGLVL = logging.DEBUG + + class Core: ''' MainWindow and Command module both use an instance of this class @@ -35,6 +41,7 @@ class Core: continue elif ext == '.py': yield name + log.debug('Importing component modules') self.modules = [ import_module('components.%s' % name) for name in findComponents() @@ -67,7 +74,7 @@ class Core: compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None - + log.debug('Inserting Component from module #%s' % moduleIndex) component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) @@ -104,7 +111,7 @@ class Core: self.componentListChanged() def updateComponent(self, i): - # print('updating %s' % self.selectedComponents[i]) + log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i))) self.selectedComponents[i].update() def moduleIndexFor(self, compName): @@ -125,12 +132,17 @@ class Core: if not saveValueStore: return False try: - self.selectedComponents[compIndex].loadPreset( + comp = self.selectedComponents[compIndex] + comp.loadPreset( saveValueStore, presetName ) except KeyError as e: - print('preset missing value: %s' % e) + log.warning( + '%s #%s\'s preset is missing value: %s' % ( + comp.name, str(compIndex), str(e) + ) + ) self.savedPresets[presetName] = dict(saveValueStore) return True @@ -206,7 +218,7 @@ class Core: preset['preset'] ) except KeyError as e: - print('%s missing value: %s' % ( + log.warning('%s missing value: %s' % ( self.selectedComponents[i], e) ) @@ -224,7 +236,7 @@ class Core: typ, value, tb = data if typ.__name__ == 'KeyError': # probably just an old version, still loadable - print('file missing value: %s' % value) + log.warning('Project file missing value: %s' % value) return if hasattr(loader, 'createNewProject'): loader.createNewProject(prompt=False) @@ -244,6 +256,7 @@ class Core: Returns dictionary with section names as the keys, each one contains a list of tuples: (compName, version, compPresetDict) ''' + log.debug('Parsing av file: %s' % filepath) validSections = ( 'Components', 'Settings', @@ -362,6 +375,7 @@ class Core: def createProjectFile(self, filepath, window=None): '''Create a project file (.avp) using the current program state''' + log.info('Creating %s' % filepath) settingsKeys = [ 'componentDir', 'inputDir', @@ -374,9 +388,8 @@ class Core: filepath += '.avp' if os.path.exists(filepath): os.remove(filepath) - with open(filepath, 'w') as f: - print('creating %s' % filepath) + with open(filepath, 'w') as f: f.write('[Components]\n') for comp in self.selectedComponents: saveValueStore = comp.savePreset() @@ -443,6 +456,7 @@ class Core: 'settings': QtCore.QSettings( os.path.join(dataDir, 'settings.ini'), QtCore.QSettings.IniFormat), + 'logDir': os.path.join(dataDir, 'log'), 'presetDir': os.path.join(dataDir, 'presets'), 'componentsPath': os.path.join(wd, 'components'), 'encoderOptions': encoderOptions, @@ -489,6 +503,13 @@ class Core: setattr(cls, classvar, val) cls.loadDefaultSettings() + if not os.path.exists(cls.dataDir): + os.makedirs(cls.dataDir) + for neededDirectory in ( + cls.presetDir, cls.logDir, cls.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + cls.makeLogger() @classmethod def loadDefaultSettings(cls): @@ -522,6 +543,42 @@ class Core: if val in ('true', 'false'): cls.settings.setValue(key, True if val == 'true' else False) + @staticmethod + def makeLogger(): + logFilename = os.path.join(Core.logDir, 'avp_debug.log') + libLogFilename = os.path.join(Core.logDir, 'global_debug.log') + # delete old logs + for log in (logFilename, libLogFilename): + if os.path.exists(log): + os.remove(log) + + # create file handlers to capture every log message somewhere + logFile = logging.FileHandler(logFilename) + logFile.setLevel(FILE_LOGLVL) + libLogFile = logging.FileHandler(libLogFilename) + libLogFile.setLevel(FILE_LOGLVL) + + # send some critical log messages to stdout as well + logStream = logging.StreamHandler() + logStream.setLevel(STDOUT_LOGLVL) + + # create formatters and put everything together + fileFormatter = logging.Formatter( + '[%(asctime)s] <%(name)s> %(levelname)s: %(message)s' + ) + streamFormatter = logging.Formatter( + '<%(name)s> %(message)s' + ) + logFile.setFormatter(fileFormatter) + libLogFile.setFormatter(fileFormatter) + logStream.setFormatter(streamFormatter) + log = logging.getLogger('AVP') + log.setLevel(FILE_LOGLVL) + log.addHandler(logFile) + log.addHandler(logStream) + libLog = logging.getLogger() + libLog.setLevel(FILE_LOGLVL) + libLog.addHandler(libLogFile) # always store settings in class variables even if a Core object is not created Core.storeSettings() diff --git a/src/main.py b/src/main.py index 421a09f..3a6fbe7 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,14 @@ from PyQt5 import uic, QtWidgets import sys import os +import logging from __init__ import wd +log = logging.getLogger('AVP.Entrypoint') + + def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") @@ -28,6 +32,7 @@ def main(): from command import Command main = Command() + log.debug("Finished creating command object") elif mode == 'GUI': from mainwindow import MainWindow @@ -48,6 +53,7 @@ def main(): # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = MainWindow(window, proj) + log.debug("Finished creating main window") window.raise_() signal.signal(signal.SIGINT, main.cleanUp) diff --git a/src/mainwindow.py b/src/mainwindow.py index 789a6e7..114015c 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -13,6 +13,7 @@ import os import signal import filecmp import time +import logging from core import Core import preview_thread @@ -20,11 +21,15 @@ from presetmanager import PresetManager from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput +log = logging.getLogger('AVP.MainWindow') + + class PreviewWindow(QtWidgets.QLabel): ''' Paints the preview QLabel and maintains the aspect ratio when the window is resized. ''' + log = logging.getLogger('AVP.MainWindow.Preview') def __init__(self, parent, img): super(PreviewWindow, self).__init__() @@ -58,11 +63,15 @@ class PreviewWindow(QtWidgets.QLabel): if i >= 0: component = self.parent.core.selectedComponents[i] if not hasattr(component, 'previewClickEvent'): + self.log.info('Ignored click event') return pos = (event.x(), event.y()) size = (self.width(), self.height()) + butt = event.button() + self.log.info('Click event for #%s: %s button %s' % ( + i, pos, butt)) component.previewClickEvent( - pos, size, event.button() + pos, size, butt ) self.parent.core.updateComponent(i) @@ -91,9 +100,10 @@ class MainWindow(QtWidgets.QMainWindow): def __init__(self, window, project): QtWidgets.QMainWindow.__init__(self) - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = Core() + log.debug( + 'Main thread id: {}'.format(QtCore.QThread.currentThreadId())) # widgets of component settings self.pages = [] @@ -103,27 +113,23 @@ class MainWindow(QtWidgets.QMainWindow): self.autosaveCooldown = 0.2 self.encoding = False - # Create data directory, load/create settings + # 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) - if not os.path.exists(self.dataDir): - os.makedirs(self.dataDir) - for neededDirectory in ( - self.presetDir, self.settings.value("projectDir")): - if not os.path.exists(neededDirectory): - os.mkdir(neededDirectory) - # 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) @@ -132,6 +138,7 @@ class MainWindow(QtWidgets.QMainWindow): 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) @@ -141,6 +148,8 @@ class MainWindow(QtWidgets.QMainWindow): 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) @@ -276,6 +285,7 @@ class MainWindow(QtWidgets.QMainWindow): ) self.updateWindowTitle() + log.debug('Showing main window') window.show() if project and project != self.autosavePath: @@ -398,6 +408,7 @@ class MainWindow(QtWidgets.QMainWindow): @QtCore.pyqtSlot() def cleanUp(self, *args): + log.info('Ending the preview thread') self.timer.stop() self.previewThread.quit() self.previewThread.wait() @@ -414,11 +425,12 @@ class MainWindow(QtWidgets.QMainWindow): 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) == dict: + if type(presetStore) is dict: name = presetStore['preset'] if name is None or name not in self.core.savedPresets: modified = False @@ -428,11 +440,20 @@ class MainWindow(QtWidgets.QMainWindow): modified = bool(presetStore) if pos < 0: pos = len(self.core.selectedComponents)-1 - title = str(self.core.selectedComponents[pos]) + 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): @@ -493,6 +514,8 @@ class MainWindow(QtWidgets.QMainWindow): 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.''' @@ -500,9 +523,14 @@ class MainWindow(QtWidgets.QMainWindow): 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: - print('project file couldn\'t be located:', self.currentProject) + log.error( + 'Project file couldn\'t be located:', self.currentProject) return identical return False @@ -543,7 +571,7 @@ class MainWindow(QtWidgets.QMainWindow): self.window.lineEdit_outputFile.setText(fileName) def stopVideo(self): - print('stop') + log.info('Export cancelled') self.videoWorker.cancel() self.canceled = True @@ -773,6 +801,7 @@ class MainWindow(QtWidgets.QMainWindow): mousePos = -1 else: mousePos = mousePos.index(True) + log.debug('Click component list row %s' % mousePos) return mousePos @disableWhenEncoding diff --git a/src/preview_thread.py b/src/preview_thread.py index bb22f0c..9615884 100644 --- a/src/preview_thread.py +++ b/src/preview_thread.py @@ -8,11 +8,15 @@ 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) @@ -55,7 +59,7 @@ class Worker(QtCore.QObject): self.background = Checkerboard(width, height) frame = self.background.copy() - + log.debug('Creating new preview frame') components = nextPreviewInformation["components"] for component in reversed(components): try: @@ -73,10 +77,11 @@ class Worker(QtCore.QObject): newFrame.width, newFrame.height, width, height ) + log.critical(errMsg) self.error.emit(errMsg) break except RuntimeError as e: - print(e) + log.error(str(e)) else: self.frame = ImageQt(frame) self.imageCreated.emit(QtGui.QImage(self.frame)) diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 3421049..6ab445c 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -8,12 +8,16 @@ import subprocess import threading import signal from queue import PriorityQueue +import logging import core from toolkit.common import checkOutput, pipeWrapper from component import ComponentError +log = logging.getLogger('AVP.Toolkit.Ffmpeg') + + class FfmpegVideo: '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.''' @@ -88,13 +92,14 @@ class FfmpegVideo: def fillBuffer(self): logFilename = os.path.join( - core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos)) - with open(logFilename, 'w') as log: - log.write(" ".join(self.command) + '\n\n') - with open(logFilename, 'a') as log: + core.Core.logDir, 'render_%s.log' % str(self.component.compPos)) + log.debug('Creating ffmpeg process (log at %s)' % logFilename) + with open(logFilename, 'w') as logf: + logf.write(" ".join(self.command) + '\n\n') + with open(logFilename, 'a') as logf: self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=log, bufsize=10**8 + stderr=logf, bufsize=10**8 ) while True: if self.parent.canceled: @@ -375,7 +380,7 @@ def getAudioDuration(filename): try: info = fileInfo.decode("utf-8").split('\n') except UnicodeDecodeError as e: - print('Unicode error:', str(e)) + log.error('Unicode error:', str(e)) return False for line in info: @@ -398,7 +403,7 @@ def readAudioFile(filename, videoWorker): ''' duration = getAudioDuration(filename) if not duration: - print('Audio file doesn\'t exist or unreadable.') + log.error('Audio file doesn\'t exist or unreadable.') return command = [ diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 7e83d58..02f9229 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -7,10 +7,14 @@ from PIL.ImageQt import ImageQt import sys import os import math +import logging import core +log = logging.getLogger('AVP.Toolkit.Frame') + + class FramePainter(QtGui.QPainter): ''' A QPainter for a blank frame, which can be converted into a @@ -79,6 +83,7 @@ def FloodFrame(width, height, RgbaTuple): @defaultSize def BlankFrame(width, height): '''The base frame used by each component to start drawing.''' + log.debug('Creating new %s*%s blank frame' % (width, height)) return FloodFrame(width, height, (0, 0, 0, 0)) @@ -88,6 +93,7 @@ def Checkerboard(width, height): A checkerboard to represent transparency to the user. TODO: Would be cool to generate this image with numpy instead. ''' + log.debug('Creating new %s*%s checkerboard' % (width, height)) image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image.paste(Image.open( os.path.join(core.Core.wd, "background.png")), diff --git a/src/video_thread.py b/src/video_thread.py index 5963def..e7e4136 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -17,6 +17,7 @@ from queue import Queue, PriorityQueue from threading import Thread, Event import time import signal +import logging from component import ComponentError from toolkit.frame import Checkerboard @@ -26,6 +27,9 @@ from toolkit.ffmpeg import ( ) +log = logging.getLogger("AVP.VideoThread") + + class Worker(QtCore.QObject): imageCreated = pyqtSignal(['QImage']) @@ -92,7 +96,7 @@ class Worker(QtCore.QObject): by a renderNode later. All indices are multiples of self.sampleSize sampleSize * frameNo = audioI, AKA audio data starting at frameNo ''' - print('Dispatching Frames for Compositing...') + log.debug('Dispatching Frames for Compositing...') for audioI in range(0, len(self.completeAudioArray), self.sampleSize): self.compositeQueue.put(audioI) @@ -156,10 +160,12 @@ class Worker(QtCore.QObject): self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") canceledByComponent = False - print('Loaded Components:', ", ".join([ + initText = ", ".join([ "%s) %s" % (num, str(component)) for num, component in enumerate(reversed(self.components)) - ])) + ]) + print('Loaded Components:', initText) + log.info('Calling preFrameRender for %s' % initText) self.staticComponents = {} for compNo, comp in enumerate(reversed(self.components)): try: @@ -191,6 +197,7 @@ class Worker(QtCore.QObject): compError[0] ) ) + log.critical(errMsg) comp._error.emit(errMsg, compError[1]) break if 'static' in compProps: @@ -199,7 +206,7 @@ class Worker(QtCore.QObject): if self.canceled: if canceledByComponent: - print('Export cancelled by component #%s (%s): %s' % ( + log.critical('Export cancelled by component #%s (%s): %s' % ( compNo, comp.name, 'No message.' if comp.error() is None else ( @@ -224,8 +231,11 @@ class Worker(QtCore.QObject): ffmpegCommand = createFfmpegCommand( self.inputFile, self.outputFile, self.components, duration ) - print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand)) + cmd = " ".join(ffmpegCommand) + print('###### FFMPEG COMMAND ######\n%s' % cmd) print('############################') + log.info('Opening pipe to ffmpeg') + log.info(cmd) self.out_pipe = openPipe( ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout ) @@ -298,9 +308,9 @@ class Worker(QtCore.QObject): try: self.out_pipe.stdin.close() except BrokenPipeError: - print('Broken pipe to ffmpeg!') + log.error('Broken pipe to ffmpeg!') if self.out_pipe.stderr is not None: - print(self.out_pipe.stderr.read()) + log.error(self.out_pipe.stderr.read()) self.out_pipe.stderr.close() self.error = True self.out_pipe.wait() -- cgit v1.2.3 From 733c005eeaf5d3ff15e0f60d320f5c03472bad60 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 18:41:45 -0400 Subject: undoable removeComponent action --- src/command.py | 1 + src/component.py | 3 +-- src/core.py | 36 ++++++++++++++++++++++++------------ src/gui/actions.py | 37 +++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.py | 28 +++++++++++++++------------- src/gui/presetmanager.py | 7 +------ src/main.py | 4 ++-- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/gui/actions.py (limited to 'src/main.py') diff --git a/src/command.py b/src/command.py index 18f7408..4116c5a 100644 --- a/src/command.py +++ b/src/command.py @@ -19,6 +19,7 @@ class Command(QtCore.QObject): def __init__(self): QtCore.QObject.__init__(self) self.core = Core() + Core.mode = 'commandline' self.dataDir = self.core.dataDir self.canceled = False diff --git a/src/component.py b/src/component.py index cf3085c..0e5144c 100644 --- a/src/component.py +++ b/src/component.py @@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)): '''Intercepts the command() method to check for global args''' def commandWrapper(self, arg): if arg.startswith('preset='): - from presetmanager import getPresetDir _, preset = arg.split('=', 1) - path = os.path.join(getPresetDir(self), preset) + path = os.path.join(self.core.getPresetDir(self), preset) if not os.path.exists(path): print('Couldn\'t locate preset "%s"' % preset) quit(1) diff --git a/src/core.py b/src/core.py index 4dfb210..20b9c1d 100644 --- a/src/core.py +++ b/src/core.py @@ -64,31 +64,39 @@ class Core: for i, component in enumerate(self.selectedComponents): component.compPos = i - def insertComponent(self, compPos, moduleIndex, loader): + def insertComponent(self, compPos, component, loader): ''' Creates a new component using these args: - (compPos, moduleIndex in self.modules, MWindow/Command/Core obj) + (compPos, component obj or moduleIndex, MWindow/Command/Core obj) ''' if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None - log.debug('Inserting Component from module #%s' % moduleIndex) - component = self.modules[moduleIndex].Component( - moduleIndex, compPos, self + if type(component) is int: + # create component using module index in self.modules + moduleIndex = int(component) + log.debug('Creating new component from module #%s' % moduleIndex) + component = self.modules[moduleIndex].Component( + moduleIndex, compPos, self + ) + # init component's widget for loading/saving presets + component.widget(loader) + else: + moduleIndex = -1 + log.debug( + 'Inserting previously-created %s component' % component.name) + + component._error.connect( + loader.videoThreadError ) self.selectedComponents.insert( compPos, component ) self.componentListChanged() - self.selectedComponents[compPos]._error.connect( - loader.videoThreadError - ) - - # init component's widget for loading/saving presets - self.selectedComponents[compPos].widget(loader) - self.updateComponent(compPos) + if moduleIndex > -1: + self.updateComponent(compPos) if hasattr(loader, 'insertComponent'): loader.insertComponent(compPos) @@ -156,6 +164,10 @@ class Core: break return saveValueStore + def getPresetDir(self, comp): + '''Get the preset subdir for a particular version of a component''' + return os.path.join(Core.presetDir, str(comp), str(comp.version)) + def openProject(self, loader, filepath): ''' loader is the object calling this method which must have its own showMessage(**kwargs) method for displaying errors. diff --git a/src/gui/actions.py b/src/gui/actions.py new file mode 100644 index 0000000..5cf64e1 --- /dev/null +++ b/src/gui/actions.py @@ -0,0 +1,37 @@ +''' + QCommand classes for every undoable user action performed in the MainWindow +''' +from PyQt5.QtWidgets import QUndoCommand + + +class RemoveComponent(QUndoCommand): + def __init__(self, parent, selectedRows): + super().__init__('Remove component') + self.parent = parent + componentList = self.parent.window.listWidget_componentList + self.selectedRows = [ + componentList.row(selected) for selected in selectedRows + ] + self.components = [ + parent.core.selectedComponents[i] for i in self.selectedRows + ] + + def redo(self): + stackedWidget = self.parent.window.stackedWidget + componentList = self.parent.window.listWidget_componentList + for index in self.selectedRows: + stackedWidget.removeWidget(self.parent.pages[index]) + componentList.takeItem(index) + self.parent.core.removeComponent(index) + self.parent.pages.pop(index) + self.parent.changeComponentWidget() + self.parent.drawPreview() + + def undo(self): + componentList = self.parent.window.listWidget_componentList + for index, comp in zip(self.selectedRows, self.components): + self.parent.core.insertComponent( + index, comp, self.parent + ) + self.parent.drawPreview() + diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index af6e190..2edb750 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,9 +16,10 @@ import time import logging from core import Core -import preview_thread -from preview_win import PreviewWindow -from presetmanager import PresetManager +import gui.preview_thread as preview_thread +from gui.preview_win import PreviewWindow +from gui.presetmanager import PresetManager +from gui.actions import * from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput @@ -43,9 +44,12 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMainWindow.__init__(self) self.window = window self.core = Core() + Core.mode = 'GUI' log.debug( 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) + self.undoStack = QtWidgets.QUndoStack(self) + # widgets of component settings self.pages = [] self.lastAutosave = time.time() @@ -62,7 +66,7 @@ class MainWindow(QtWidgets.QMainWindow): self.presetManager = PresetManager( uic.loadUi( - os.path.join(Core.wd, 'presetmanager.ui')), self) + os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) # Create the preview window and its thread, queues, and timers log.debug('Creating preview window') @@ -298,6 +302,9 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) + QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo) + QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo) + QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo) # Hotkeys for component list for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): @@ -685,15 +692,10 @@ class MainWindow(QtWidgets.QMainWindow): def removeComponent(self): componentList = self.window.listWidget_componentList - - for selected in componentList.selectedItems(): - index = componentList.row(selected) - self.window.stackedWidget.removeWidget(self.pages[index]) - componentList.takeItem(index) - self.core.removeComponent(index) - self.pages.pop(index) - self.changeComponentWidget() - self.drawPreview() + selected = componentList.selectedItems() + if selected: + action = RemoveComponent(self, selected) + self.undoStack.push(action) @disableWhenEncoding def moveComponent(self, change): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index b1eeb34..1cc0887 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -302,7 +302,7 @@ class PresetManager(QtWidgets.QDialog): self.findPresets() self.drawPresetList() for i, comp in enumerate(self.core.selectedComponents): - if getPresetDir(comp) == path \ + if self.core.getPresetDir(comp) == path \ and comp.currentPreset == oldName: self.core.openPreset(newPath, i, newName) self.parent.updateComponentTitle(i, False) @@ -351,8 +351,3 @@ class PresetManager(QtWidgets.QDialog): def clearPresetListSelection(self): self.window.listWidget_presets.setCurrentRow(-1) - - -def getPresetDir(comp): - '''Get the preset subdir for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) diff --git a/src/main.py b/src/main.py index 3a6fbe7..c1278da 100644 --- a/src/main.py +++ b/src/main.py @@ -35,11 +35,11 @@ def main(): log.debug("Finished creating command object") elif mode == 'GUI': - from mainwindow import MainWindow + from gui.mainwindow import MainWindow import atexit import signal - window = uic.loadUi(os.path.join(wd, "mainwindow.ui")) + window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() desc = QtWidgets.QDesktopWidget() dpi = desc.physicalDpiX() -- cgit v1.2.3 From c07f2426ceeada205fdacbfba66329179a74a1dc Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 19 Aug 2017 18:32:12 -0400 Subject: fixed issues with undoing relative widgets --- src/component.py | 198 +++++++++++++++++++++++++++++++++------------ src/components/color.py | 2 - src/components/image.py | 2 - src/components/life.py | 1 - src/components/sound.py | 1 - src/components/spectrum.py | 4 +- src/components/text.py | 1 - src/components/video.py | 2 - src/components/waveform.py | 2 +- src/core.py | 11 +-- src/gui/actions.py | 11 ++- src/gui/mainwindow.py | 4 +- src/gui/presetmanager.py | 4 + src/gui/preview_thread.py | 2 +- src/gui/preview_win.py | 2 +- src/main.py | 2 +- src/toolkit/common.py | 47 +++++++++-- 17 files changed, 215 insertions(+), 81 deletions(-) (limited to 'src/main.py') diff --git a/src/component.py b/src/component.py index 1fe9237..ba86422 100644 --- a/src/component.py +++ b/src/component.py @@ -9,6 +9,7 @@ import sys import math import time import logging +from copy import copy from toolkit.frame import BlankFrame from toolkit import ( @@ -113,14 +114,20 @@ class ComponentMetaclass(type(QtCore.QObject)): def presetWrapper(self, *args): with openingPreset(self): - return func(self, *args) + try: + return func(self, *args) + except Exception: + try: + raise ComponentError(self, 'preset loader') + except ComponentError: + return return presetWrapper def updateWrapper(func): ''' - For undoable updates triggered by the user, - call _userUpdate() after the subclass's update() method. - For non-user updates, call _autoUpdate() + Calls _preUpdate before every subclass update(). + Afterwards, for non-user updates, calls _autoUpdate(). + For undoable updates triggered by the user, calls _userUpdate() ''' class wrap: def __init__(self, comp, auto): @@ -128,24 +135,57 @@ class ComponentMetaclass(type(QtCore.QObject)): self.auto = auto def __enter__(self): - pass + self.comp._preUpdate() def __exit__(self, *args): if self.auto or self.comp.openingPreset \ or not hasattr(self.comp.parent, 'undoStack'): + log.verbose('Automatic update') self.comp._autoUpdate() else: + log.verbose('User update') self.comp._userUpdate() def updateWrapper(self, **kwargs): - auto = False - if 'auto' in kwargs: - auto = kwargs['auto'] - + auto = kwargs['auto'] if 'auto' in kwargs else False with wrap(self, auto): - return func(self) + try: + return func(self) + except Exception: + try: + raise ComponentError(self, 'update method') + except ComponentError: + return return updateWrapper + def widgetWrapper(func): + '''Connects all widgets to update method after the subclass's method''' + class wrap: + def __init__(self, comp): + self.comp = comp + + def __enter__(self): + pass + + def __exit__(self, *args): + for widgetList in self.comp._allWidgets.values(): + for widget in widgetList: + log.verbose('Connecting %s' % str( + widget.__class__.__name__)) + connectWidget(widget, self.comp.update) + + def widgetWrapper(self, *args, **kwargs): + auto = kwargs['auto'] if 'auto' in kwargs else False + with wrap(self): + try: + return func(self, *args, **kwargs) + except Exception: + try: + raise ComponentError(self, 'widget creation') + except ComponentError: + return + return widgetWrapper + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -153,13 +193,12 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs['__module__'].split('.')[-1] )[0] - # if parents[0] == QtCore.QObject: else: decorate = ( 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', 'frameRender', 'command', - 'loadPreset', 'update' + 'loadPreset', 'update', 'widget', ) # Auto-decorate methods @@ -184,6 +223,8 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = cls.loadPresetWrapper(attrs[key]) elif key == 'update': attrs[key] = cls.updateWrapper(attrs[key]) + elif key == 'widget' and parents[0] != QtCore.QObject: + attrs[key] = cls.widgetWrapper(attrs[key]) # Turn version string into a number try: @@ -224,23 +265,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.moduleIndex = moduleIndex self.compPos = compPos self.core = core - self.currentPreset = None - self.openingPreset = False + # STATUS VARIABLES + self.currentPreset = None + self._allWidgets = {} self._trackedWidgets = {} self._presetNames = {} self._commandArgs = {} self._colorWidgets = {} self._colorFuncs = {} self._relativeWidgets = {} - # pixel values stored as floats + # Pixel values stored as floats self._relativeValues = {} - # maximum values of spinBoxes at 1080p (Core.resolutions[0]) + # Maximum values of spinBoxes at 1080p (Core.resolutions[0]) self._relativeMaximums = {} + # LOCKING VARIABLES + self.openingPreset = False self._lockedProperties = None self._lockedError = None self._lockedSize = None + # If set to a dict, values are used as basis to update relative widgets + self.oldAttrs = None # Stop lengthy processes in response to this variable self.canceled = False @@ -338,21 +384,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings + log.verbose('Creating UI for %s #%s\'s widget' % ( + self.name, self.compPos + )) self.page = self.loadUi(self.__class__.ui) - # Connect widget signals - widgets = { + # Find all normal widgets which will be connected after subclass method + self._allWidgets = { 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), 'comboBox': self.page.findChildren(QtWidgets.QComboBox), } - widgets['spinBox'].extend( + self._allWidgets['spinBox'].extend( self.page.findChildren(QtWidgets.QDoubleSpinBox) ) - for widgetList in widgets.values(): - for widget in widgetList: - connectWidget(widget, self.update) def update(self): ''' @@ -427,10 +473,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # "Private" Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + def _preUpdate(self): + '''Happens before subclass update()''' + for attr in self._relativeWidgets: + self.updateRelativeWidget(attr) + def _userUpdate(self): - '''An undoable component update triggered by the user''' + '''Happens after subclass update() for an undoable update by user.''' oldWidgetVals = { - attr: getattr(self, attr) + attr: copy(getattr(self, attr)) for attr in self._trackedWidgets } newWidgetVals = { @@ -443,13 +494,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): for attr, val in newWidgetVals.items() if val != oldWidgetVals[attr] } - if modifiedWidgets: action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets) self.parent.undoStack.push(action) def _autoUpdate(self): - '''An internal component update that is not undoable''' + '''Happens after subclass update() for an internal component update.''' newWidgetVals = { attr: getWidgetValue(widget) for attr, widget in self._trackedWidgets.items() @@ -459,12 +509,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setAttrs(self, attrDict): ''' - Sets attrs (linked to trackedWidgets) in this preset to + Sets attrs (linked to trackedWidgets) in this component to the values in the attrDict. Mutates certain widget values if needed ''' for attr, val in attrDict.items(): if attr in self._colorWidgets: - # Color Widgets: text stored as tuple & update the button color + # Color Widgets must have a tuple & have a button to update if type(val) is tuple: rgbTuple = val else: @@ -475,15 +525,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._colorWidgets[attr].setStyleSheet(btnStyle) setattr(self, attr, rgbTuple) - elif attr in self._relativeWidgets: - # Relative widgets: number scales to fit export resolution - self.updateRelativeWidget(attr) - setattr(self, attr, val) - else: # Normal tracked widget setattr(self, attr, val) + def setWidgetValues(self, attrDict): + ''' + Sets widgets defined by keys in trackedWidgets in this preset to + the values in the attrDict. + ''' + affectedWidgets = [ + self._trackedWidgets[attr] for attr in attrDict + ] + with blockSignals(affectedWidgets): + for attr, val in attrDict.items(): + widget = self._trackedWidgets[attr] + if attr in self._colorWidgets: + val = '%s,%s,%s' % val + setWidgetValue(widget, val) + def _sendUpdateSignal(self): if not self.core.openingProject: self.parent.drawPreview() @@ -499,6 +559,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): Optional args: 'presetNames': preset variable names to replace attr names 'commandArgs': arg keywords that differ from attr names + 'colorWidgets': identify attr as RGB tuple & update button CSS + 'relativeWidgets': change value proportionally to resolution NOTE: Any kwarg key set to None will selectively disable tracking. ''' @@ -542,6 +604,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._relativeMaximums[attr] = \ self._trackedWidgets[attr].maximum() self.updateRelativeWidgetMaximum(attr) + self._preUpdate() + self._autoUpdate() def pickColor(self, textWidget, button): '''Use color picker to get color input from the user.''' @@ -627,12 +691,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setRelativeWidget(self, attr, floatVal): '''Set a relative widget using a float''' pixelVal = self.pixelValForAttr(attr, floatVal) - self._trackedWidgets[attr].setValue(pixelVal) + with blockSignals(self._allWidgets): + self._trackedWidgets[attr].setValue(pixelVal) + self.update(auto=True) + + def getOldAttr(self, attr): + ''' + Returns previous state of this attr. Used to determine whether + a relative widget must be updated. Required because undoing/redoing + can make determining the 'previous' value tricky. + ''' + if self.oldAttrs is not None: + log.verbose('Using nonstandard oldAttr for %s' % attr) + return self.oldAttrs[attr] + else: + return getattr(self, attr) def updateRelativeWidget(self, attr): + '''Called by _preUpdate() for each relativeWidget before each update''' try: - oldUserValue = getattr(self, attr) - except AttributeError: + oldUserValue = self.getOldAttr(attr) + except (AttributeError, KeyError): + log.info('Using visible values as basis for relative widgets') oldUserValue = self._trackedWidgets[attr].value() newUserValue = self._trackedWidgets[attr].value() newRelativeVal = self.floatValForAttr(attr, newUserValue) @@ -645,11 +725,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # means the pixel value needs to be updated log.debug('Updating %s #%s\'s relative widget: %s' % ( self.name, self.compPos, attr)) - self._trackedWidgets[attr].blockSignals(True) - self.updateRelativeWidgetMaximum(attr) - pixelVal = self.pixelValForAttr(attr, oldRelativeVal) - self._trackedWidgets[attr].setValue(pixelVal) - self._trackedWidgets[attr].blockSignals(False) + with blockSignals(self._trackedWidgets[attr]): + self.updateRelativeWidgetMaximum(attr) + pixelVal = self.pixelValForAttr(attr, oldRelativeVal) + self._trackedWidgets[attr].setValue(pixelVal) if attr not in self._relativeValues \ or oldUserValue != newUserValue: @@ -725,14 +804,22 @@ class ComponentUpdate(QtWidgets.QUndoCommand): parent.name, parent.compPos ) ) + self.undone = False self.parent = parent self.oldWidgetVals = { - attr: val + attr: copy(val) for attr, val in oldWidgetVals.items() if attr in modifiedVals } self.modifiedVals = modifiedVals + # Because relative widgets change themselves every update based on + # their previous value, we must store ALL their values in case of undo + self.redoRelativeWidgetVals = { + attr: copy(getattr(self.parent, attr)) + for attr in self.parent._relativeWidgets + } + # Determine if this update is mergeable self.id_ = -1 if len(self.modifiedVals) == 1: @@ -755,15 +842,26 @@ class ComponentUpdate(QtWidgets.QUndoCommand): return True def redo(self): + if self.undone: + log.debug('Redoing component update') + self.parent.setWidgetValues(self.modifiedVals) self.parent.setAttrs(self.modifiedVals) - self.parent._sendUpdateSignal() + if self.undone: + self.parent.oldAttrs = self.redoRelativeWidgetVals + self.parent.update(auto=True) + self.parent.oldAttrs = None + else: + self.undoRelativeWidgetVals = { + attr: copy(getattr(self.parent, attr)) + for attr in self.parent._relativeWidgets + } + self.parent._sendUpdateSignal() def undo(self): + log.debug('Undoing component update') + self.undone = True + self.parent.oldAttrs = self.undoRelativeWidgetVals + self.parent.setWidgetValues(self.oldWidgetVals) self.parent.setAttrs(self.oldWidgetVals) - with blockSignals(self.parent): - for attr, val in self.oldWidgetVals.items(): - widget = self.parent._trackedWidgets[attr] - if attr in self.parent._colorWidgets: - val = '%s,%s,%s' % val - setWidgetValue(widget, val) - self.parent._sendUpdateSignal() + self.parent.update(auto=True) + self.parent.oldAttrs = None diff --git a/src/components/color.py b/src/components/color.py index d09cee8..a55aa10 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -82,8 +82,6 @@ class Component(Component): self.page.pushButton_color2.setEnabled(False) self.page.fillWidget.setCurrentIndex(fillType) - super().update() - def previewRender(self): return self.drawFrame(self.width, self.height) diff --git a/src/components/image.py b/src/components/image.py index 63bee1a..c57b69c 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -84,7 +84,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def command(self, arg): if '=' in arg: @@ -123,4 +122,3 @@ class Component(Component): else: scaleBox.setVisible(True) stretchScaleBox.setVisible(False) - super().update() diff --git a/src/components/life.py b/src/components/life.py index 2383d30..76d2c5f 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def shiftGrid(self, d): def newGrid(Xchange, Ychange): diff --git a/src/components/sound.py b/src/components/sound.py index 26ecf93..b86f40c 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) - self.update() def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 89130a2..2b98dc2 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -76,8 +76,6 @@ class Component(Component): else: self.page.checkBox_mono.setEnabled(True) - super().update() - def previewRender(self): changedSize = self.updateChunksize() if not changedSize \ @@ -138,7 +136,7 @@ class Component(Component): '-r', self.settings.value("outputFrameRate"), '-ss', "{0:.3f}".format(startPt), '-i', - os.path.join(self.core.wd, 'background.png') + self.core.junkStream if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', diff --git a/src/components/text.py b/src/components/text.py index d3afd5c..92f0599 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -68,7 +68,6 @@ class Component(Component): self.page.spinBox_shadY.setHidden(True) self.page.label_shadBlur.setHidden(True) self.page.spinBox_shadBlur.setHidden(True) - super().update() def centerXY(self): self.setRelativeWidget('xPosition', 0.5) diff --git a/src/components/video.py b/src/components/video.py index a189f60..9c0d608 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -52,7 +52,6 @@ class Component(Component): else: self.page.label_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False) - super().update() def previewRender(self): self.updateChunksize() @@ -119,7 +118,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) - self.update() def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): diff --git a/src/components/waveform.py b/src/components/waveform.py index 0743e55..5c02bbf 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -98,7 +98,7 @@ class Component(Component): '-r', self.settings.value("outputFrameRate"), '-ss', "{0:.3f}".format(startPt), '-i', - os.path.join(self.core.wd, 'background.png') + self.core.junkStream if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', diff --git a/src/core.py b/src/core.py index d9499f7..169716c 100644 --- a/src/core.py +++ b/src/core.py @@ -13,7 +13,7 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.WARNING +STDOUT_LOGLVL = logging.VERBOSE FILE_LOGLVL = logging.DEBUG @@ -81,10 +81,7 @@ class Core: component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) - # init component's widget for loading/saving presets component.widget(loader) - # use autoUpdate() method before update() this 1 time to set attrs - component._autoUpdate() else: moduleIndex = -1 log.debug( @@ -186,9 +183,8 @@ class Core: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: widget = eval('loader.window.%s' % widget) - widget.blockSignals(True) - toolkit.setWidgetValue(widget, value) - widget.blockSignals(False) + with toolkit.blockSignals(widget): + toolkit.setWidgetValue(widget, value) for key, value in data['Settings']: Core.settings.setValue(key, value) @@ -474,6 +470,7 @@ class Core: 'logDir': os.path.join(dataDir, 'log'), 'presetDir': os.path.join(dataDir, 'presets'), 'componentsPath': os.path.join(wd, 'components'), + 'junkStream': os.path.join(wd, 'gui', 'background.png'), 'encoderOptions': encoderOptions, 'resolutions': [ '1920x1080', diff --git a/src/gui/actions.py b/src/gui/actions.py index 0fe97f2..1444569 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -20,11 +20,20 @@ class AddComponent(QUndoCommand): self.parent = parent self.moduleI = moduleI self.compI = compI + self.comp = None def redo(self): - self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) + if self.comp is None: + self.parent.core.insertComponent( + self.compI, self.moduleI, self.parent) + else: + # inserting previously-created component + self.parent.core.insertComponent( + self.compI, self.comp, self.parent) + def undo(self): + self.comp = self.parent.core.selectedComponents[self.compI] self.parent._removeComponent(self.compI) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 8000b3b..76c53af 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -25,7 +25,7 @@ from toolkit import ( ) -log = logging.getLogger('AVP.MainWindow') +log = logging.getLogger('AVP.Gui.MainWindow') class MainWindow(QtWidgets.QMainWindow): @@ -76,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow): # Create the preview window and its thread, queues, and timers log.debug('Creating preview window') self.previewWindow = PreviewWindow(self, os.path.join( - Core.wd, "background.png")) + Core.wd, 'gui', "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) log.debug('Starting preview thread') diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index dce5333..befa7cd 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -5,12 +5,16 @@ from PyQt5 import QtCore, QtWidgets import string import os +import logging from toolkit import badName from core import Core from gui.actions import * +log = logging.getLogger('AVP.Gui.PresetManager') + + class PresetManager(QtWidgets.QDialog): def __init__(self, window, parent): super().__init__(parent.window) diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index 9615884..33a9e7a 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -14,7 +14,7 @@ from toolkit.frame import Checkerboard from toolkit import disableWhenOpeningProject -log = logging.getLogger("AVP.PreviewThread") +log = logging.getLogger("AVP.Gui.PreviewThread") class Worker(QtCore.QObject): diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 40c19c6..c6b9a32 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -7,7 +7,7 @@ class PreviewWindow(QtWidgets.QLabel): Paints the preview QLabel in MainWindow and maintains the aspect ratio when the window is resized. ''' - log = logging.getLogger('AVP.PreviewWindow') + log = logging.getLogger('AVP.Gui.PreviewWindow') def __init__(self, parent, img): super(PreviewWindow, self).__init__() diff --git a/src/main.py b/src/main.py index c1278da..6d18af3 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,7 @@ import logging from __init__ import wd -log = logging.getLogger('AVP.Entrypoint') +log = logging.getLogger('AVP.Main') def main(): diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 51ad023..74143e8 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,19 +6,53 @@ import string import os import sys import subprocess +import logging +from copy import copy from collections import OrderedDict +log = logging.getLogger('AVP.Toolkit.Common') + + class blockSignals: - '''A context manager to temporarily block a Qt widget from updating''' - def __init__(self, widget): - self.widget = widget + ''' + Context manager to temporarily block list of QtWidgets from updating, + and guarantee restoring the previous state afterwards. + ''' + def __init__(self, widgets): + if type(widgets) is dict: + self.widgets = concatDictVals(widgets) + else: + self.widgets = ( + widgets if hasattr(widgets, '__iter__') + else [widgets] + ) def __enter__(self): - self.widget.blockSignals(True) + log.verbose('Blocking signals for %s' % ", ".join([ + str(w.__class__.__name__) for w in self.widgets + ])) + self.oldStates = [w.signalsBlocked() for w in self.widgets] + for w in self.widgets: + w.blockSignals(True) def __exit__(self, *args): - self.widget.blockSignals(False) + log.verbose('Resetting blockSignals to %s' % sum(self.oldStates)) + for w, state in zip(self.widgets, self.oldStates): + w.blockSignals(state) + + +def concatDictVals(d): + '''Concatenates all values in given dict into one list.''' + key, value = d.popitem() + d[key] = value + final = copy(value) + if type(final) is not list: + final = [final] + final.extend([val for val in d.values()]) + else: + value.extend([item for val in d.values() for item in val]) + return final def badName(name): @@ -119,12 +153,14 @@ def connectWidget(widget, func): elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) else: + log.warning('Failed to connect %s ' % str(widget.__class__.__name__)) return False return True def setWidgetValue(widget, val): '''Generic setValue method for use with any typical QtWidget''' + log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val)) if type(widget) == QtWidgets.QLineEdit: widget.setText(val) elif type(widget) == QtWidgets.QSpinBox \ @@ -135,6 +171,7 @@ def setWidgetValue(widget, val): elif type(widget) == QtWidgets.QComboBox: widget.setCurrentIndex(val) else: + log.warning('Failed to set %s ' % str(widget.__class__.__name__)) return False return True -- cgit v1.2.3 From 85d3b779d07ad92b0f540ea52185777c3c3f5e48 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 26 Aug 2017 21:23:44 -0400 Subject: fixed too-large Color sizes, fixed a redoing bug, rm pointless things and now Ctrl+Alt+Shift+A gives a bunch of debug info --- src/component.py | 30 +++++++++++------------ src/components/color.py | 2 +- src/components/color.ui | 4 ++-- src/components/text.py | 13 ++++++---- src/core.py | 8 +++++-- src/gui/mainwindow.py | 63 +++++++++++++++++++++++++++++-------------------- src/gui/preview_win.py | 1 + src/main.py | 5 ---- src/toolkit/ffmpeg.py | 2 +- src/toolkit/frame.py | 3 --- 10 files changed, 72 insertions(+), 59 deletions(-) (limited to 'src/main.py') diff --git a/src/component.py b/src/component.py index 35fc717..de4b6a7 100644 --- a/src/component.py +++ b/src/component.py @@ -41,10 +41,8 @@ class ComponentMetaclass(type(QtCore.QObject)): def renderWrapper(self, *args, **kwargs): try: log.verbose( - '### %s #%s renders%s frame %s###', + '### %s #%s renders a preview frame ###', self.__class__.name, str(self.compPos), - '' if args else ' a preview', - '' if not args else '%s ' % args[0], ) return func(self, *args, **kwargs) except Exception as e: @@ -198,8 +196,8 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', - 'loadPreset', 'update', 'widget', + 'loadPreset', 'command', + 'update', 'widget', ) # Auto-decorate methods @@ -212,7 +210,7 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = property(attrs[key]) elif key == 'command': attrs[key] = cls.commandWrapper(attrs[key]) - elif key in ('previewRender', 'frameRender'): + elif key == 'previewRender': attrs[key] = cls.renderWrapper(attrs[key]) elif key == 'preFrameRender': attrs[key] = cls.initializationWrapper(attrs[key]) @@ -298,16 +296,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): return self.__class__.name def __repr__(self): + import pprint try: preset = self.savePreset() except Exception as e: preset = '%s occurred while saving preset' % str(e) return ( - 'Component(%s, %s, Core)\n' - 'Name: %s v%s\n Preset: %s' % ( + 'Component(module %s, pos %s) (%s)\n' + 'Name: %s v%s\nPreset: %s' % ( self.moduleIndex, self.compPos, - self.__class__.name, str(self.__class__.version), preset + object.__repr__(self), + self.__class__.name, str(self.__class__.version), + pprint.pformat(preset) ) ) @@ -886,12 +887,11 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def redo(self): if self.undone: log.debug('Redoing component update') - self.parent.oldAttrs = self.relativeWidgetValsAfterUndo - self.setWidgetValues(self.modifiedVals) - self.parent.update(auto=True) - self.parent.oldAttrs = None - else: - self.parent.setAttrs(self.modifiedVals) + self.parent.oldAttrs = self.relativeWidgetValsAfterUndo + self.setWidgetValues(self.modifiedVals) + self.parent.update(auto=True) + self.parent.oldAttrs = None + if not self.undone: self.relativeWidgetValsAfterRedo = { attr: copy(getattr(self.parent, attr)) for attr in self.parent._relativeWidgets diff --git a/src/components/color.py b/src/components/color.py index a55aa10..7d4f86d 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -102,7 +102,7 @@ class Component(Component): # Return a solid image at x, y if self.fillType == 0: frame = BlankFrame(width, height) - image = Image.new("RGBA", shapeSize, (r, g, b, 255)) + image = FloodFrame(self.sizeWidth, self.sizeHeight, (r, g, b, 255)) frame.paste(image, box=(self.x, self.y)) return frame diff --git a/src/components/color.ui b/src/components/color.ui index 1865e60..c1713fb 100644 --- a/src/components/color.ui +++ b/src/components/color.ui @@ -204,7 +204,7 @@ 0 - 999999999 + 19200 0 @@ -239,7 +239,7 @@ - 999999999 + 10800 diff --git a/src/components/text.py b/src/components/text.py index 92f0599..32a108e 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -2,10 +2,13 @@ from PIL import ImageEnhance, ImageFilter, ImageChops from PyQt5.QtGui import QColor, QFont from PyQt5 import QtGui, QtCore, QtWidgets import os +import logging from component import Component from toolkit.frame import FramePainter, PaintColor +log = logging.getLogger('AVP.Components.Text') + class Component(Component): name = 'Title Text' @@ -76,16 +79,15 @@ class Component(Component): def getXY(self): '''Returns true x, y after considering alignment settings''' fm = QtGui.QFontMetrics(self.titleFont) - if self.alignment == 0: # Left - x = int(self.xPosition) + x = self.pixelValForAttr('xPosition') if self.alignment == 1: # Middle offset = int(fm.width(self.title)/2) - x = self.xPosition - offset - + x -= offset if self.alignment == 2: # Right offset = fm.width(self.title) - x = self.xPosition - offset + x -= offset + return x, self.yPosition def loadPreset(self, pr, *args): @@ -137,6 +139,7 @@ class Component(Component): image = FramePainter(width, height) x, y = self.getXY() + log.debug('Text position translates to %s, %s', x, y) if self.stroke > 0: outliner = QtGui.QPainterPathStroker() outliner.setWidth(self.stroke) diff --git a/src/core.py b/src/core.py index 784f3b8..b9e2335 100644 --- a/src/core.py +++ b/src/core.py @@ -14,7 +14,7 @@ import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.VERBOSE -FILE_LOGLVL = logging.VERBOSE +FILE_LOGLVL = logging.DEBUG class Core: @@ -32,6 +32,11 @@ class Core: self.savedPresets = {} # copies of presets to detect modification self.openingProject = False + def __repr__(self): + return "\n=~=~=~=\n".join( + [repr(comp) for comp in self.selectedComponents] + ) + def importComponents(self): def findComponents(): for f in os.listdir(Core.componentsPath): @@ -482,7 +487,6 @@ class Core: '854x480', ], 'FFMPEG_BIN': findFfmpeg(), - 'windowHasFocus': False, 'canceled': False, } diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 3b204b7..d7fde5c 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -11,6 +11,7 @@ from queue import Queue import sys import os import signal +import atexit import filecmp import time import logging @@ -49,6 +50,13 @@ class MainWindow(QtWidgets.QMainWindow): self.window = window self.core = Core() Core.mode = 'GUI' + # widgets of component settings + self.pages = [] + self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + self.autosaveCooldown = 0.2 + self.encoding = False # Find settings created by Core object self.dataDir = Core.dataDir @@ -56,19 +64,16 @@ class MainWindow(QtWidgets.QMainWindow): self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.settings = Core.settings + # Register clean-up functions + signal.signal(signal.SIGINT, self.terminate) + atexit.register(self.cleanUp) + # Create stack of undoable user actions self.undoStack = QtWidgets.QUndoStack(self) undoLimit = self.settings.value("pref_undoLimit") self.undoStack.setUndoLimit(undoLimit) - # widgets of component settings - self.pages = [] - self.lastAutosave = time.time() - # list of previous five autosave times, used to reduce update spam - self.autosaveTimes = [] - self.autosaveCooldown = 0.2 - self.encoding = False - + # Create Preset Manager self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) @@ -97,7 +102,6 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.start(timeout) # Begin decorating the window and connecting events - self.window.installEventFilter(self) componentList = self.window.listWidget_componentList style = window.pushButton_undo.style() @@ -391,24 +395,41 @@ class MainWindow(QtWidgets.QMainWindow): activated=lambda: self.moveComponent('bottom') ) - # Debug Hotkeys QtWidgets.QShortcut( - "Ctrl+Alt+Shift+R", self.window, self.drawPreview + "Ctrl+Shift+F", self.window, self.showFfmpegCommand ) QtWidgets.QShortcut( - "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand + "Ctrl+Shift+U", self.window, self.showUndoStack ) - QtWidgets.QShortcut( - "Ctrl+Alt+Shift+U", self.window, self.showUndoStack + + if log.isEnabledFor(logging.DEBUG): + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+R", self.window, self.drawPreview + ) + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self)) + ) + + def __repr__(self): + return ( + '\n%s\n' + '#####\n' + 'Preview thread is %s\n' % ( + repr(self.core), + 'live' if self.previewThread.isRunning() else 'dead', + ) ) - @QtCore.pyqtSlot() def cleanUp(self, *args): log.info('Ending the preview thread') self.timer.stop() self.previewThread.quit() self.previewThread.wait() + def terminate(self, *args): + self.cleanUp() + sys.exit(0) + @disableWhenOpeningProject def updateWindowTitle(self): appName = 'Audio Visualizer' @@ -542,7 +563,7 @@ class MainWindow(QtWidgets.QMainWindow): return True except FileNotFoundError: log.error( - 'Project file couldn\'t be located:', self.currentProject) + 'Project file couldn\'t be located: %s', self.currentProject) return identical return False @@ -639,6 +660,7 @@ class MainWindow(QtWidgets.QMainWindow): detail=detail, icon='Critical', ) + log.info('%s', repr(self)) def changeEncodingStatus(self, status): self.encoding = status @@ -1017,12 +1039,3 @@ class MainWindow(QtWidgets.QMainWindow): self.menu.move(parentPosition + QPos) self.menu.show() - - def eventFilter(self, object, event): - if event.type() == QtCore.QEvent.WindowActivate \ - or event.type() == QtCore.QEvent.FocusIn: - Core.windowHasFocus = True - elif event.type() == QtCore.QEvent.WindowDeactivate \ - or event.type() == QtCore.QEvent.FocusOut: - Core.windowHasFocus = False - return False diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index c6b9a32..49a22eb 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -60,3 +60,4 @@ class PreviewWindow(QtWidgets.QLabel): icon='Critical', parent=self ) + log.info('%', repr(self.parent)) diff --git a/src/main.py b/src/main.py index 6d18af3..f767de1 100644 --- a/src/main.py +++ b/src/main.py @@ -36,8 +36,6 @@ def main(): elif mode == 'GUI': from gui.mainwindow import MainWindow - import atexit - import signal window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() @@ -56,9 +54,6 @@ def main(): log.debug("Finished creating main window") window.raise_() - signal.signal(signal.SIGINT, main.cleanUp) - atexit.register(main.cleanUp) - sys.exit(app.exec_()) if __name__ == "__main__": diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index f007f90..a77831e 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -157,7 +157,7 @@ def findFfmpeg(): ['ffmpeg', '-version'], stderr=f ) return "ffmpeg" - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): return "avconv" diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 2104978..aefb55f 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -21,7 +21,6 @@ class FramePainter(QtGui.QPainter): Pillow image with finalize() ''' def __init__(self, width, height): - log.verbose('Creating new FramePainter') image = BlankFrame(width, height) self.image = QtGui.QImage(ImageQt(image)) super().__init__(self.image) @@ -78,8 +77,6 @@ def defaultSize(framefunc): def FloodFrame(width, height, RgbaTuple): - log.verbose('Creating new %s*%s %s flood frame' % ( - width, height, RgbaTuple)) return Image.new("RGBA", (width, height), RgbaTuple) -- cgit v1.2.3 From 765a35119f258f352718a556fbea4af708236900 Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 13 Apr 2022 16:04:32 -0400 Subject: cast floats to ints when calling resize(), setX(), and setY() (argument types changed in newer version) --- src/gui/preview_win.py | 4 ++-- src/main.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'src/main.py') diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 3db420c..27e0a59 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -25,8 +25,8 @@ class PreviewWindow(QtWidgets.QLabel): 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) + point.setX(int((size.width() - scaledPix.width())/2)) + point.setY(int((size.height() - scaledPix.height())/2)) painter.drawPixmap(point, scaledPix) def changePixmap(self, img): diff --git a/src/main.py b/src/main.py index f767de1..126e4a8 100644 --- a/src/main.py +++ b/src/main.py @@ -44,9 +44,10 @@ def main(): topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96)) window.resize( - window.width() * - (dpi / 96), window.height() * - (dpi / 96) + int(window.width() * + (dpi / 96)), + int(window.height() * + (dpi / 96)) ) # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) -- cgit v1.2.3 From 05d2ebc3c69f5a876d602004f69202c5ba8b09f7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 22 Apr 2022 17:09:50 -0400 Subject: make pip-installable as a package --- MANIFEST.in | 7 ++++++ setup.py | 61 ++++++++++++++++++++++++++-------------------- src/__init__.py | 6 ++--- src/__main__.py | 4 +-- src/component.py | 4 +-- src/components/color.py | 4 +-- src/components/image.py | 4 +-- src/components/life.py | 4 +-- src/components/original.py | 4 +-- src/components/sound.py | 4 +-- src/components/spectrum.py | 8 +++--- src/components/text.py | 4 +-- src/components/video.py | 8 +++--- src/components/waveform.py | 8 +++--- src/core.py | 12 ++++----- src/gui/actions.py | 2 +- src/gui/mainwindow.py | 13 +++++----- src/gui/presetmanager.py | 6 ++--- src/gui/preview_thread.py | 4 +-- src/main.py | 11 ++++----- src/toolkit/__init__.py | 2 +- src/toolkit/ffmpeg.py | 8 +++--- src/toolkit/frame.py | 2 +- src/video_thread.py | 8 +++--- 24 files changed, 106 insertions(+), 92 deletions(-) create mode 100644 MANIFEST.in (limited to 'src/main.py') diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2b2d794 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +recursive-include src/tests +include src/components/*.ui +include src/gui/*.ui +include src/gui/background.png +include src/encoder-options.json +global-exclude src/components/__template__.ui +global-exclude *.py[cod] diff --git a/setup.py b/setup.py index cdf4c4a..5e01229 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,39 @@ -from setuptools import setup -import os +from setuptools import setup, find_packages +from importlib import import_module +from os import path +import re -__version__ = '2.0.0rc5' +def getTextFromFile(filename, fallback): + try: + with open( + path.join(path.abspath(path.dirname(__file__)), filename), encoding="utf-8" + ) as f: + output = f.read() + except Exception: + output = fallback + return output -def package_files(directory): - paths = [] - for (path, directories, filenames) in os.walk(directory): - for filename in filenames: - paths.append(os.path.join('..', path, filename)) - return paths +PACKAGE_NAME = 'avp' +SOURCE_DIRECTORY = 'src' +SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}') +PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline' + + +avp = import_module(SOURCE_DIRECTORY) +source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*']) +proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages] setup( name='audio_visualizer_python', - version=__version__, + version=avp.__version__, url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui', license='MIT', - description='Create audio visualization videos from a GUI or commandline', - long_description="Create customized audio visualization videos and save " - "them as Projects to continue editing later. Different components can " - "be added and layered to add visualizers, images, videos, gradients, " - "text, etc. Use Projects created in the GUI with commandline mode to " - "automate your video production workflow without any complex syntax.", + description=PACKAGE_DESCRIPTION, + author=getTextFromFile('AUTHORS', 'djfun, tassaron'), + long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION), classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', @@ -35,19 +45,18 @@ setup( 'visualizer', 'visualization', 'commandline video', 'video editor', 'ffmpeg', 'podcast' ], - packages=[ - 'avpython', - 'avpython.toolkit', - 'avpython.components' + packages=proj_packages, + package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, + include_package_data=True, + install_requires=[ + 'Pillow-SIMD', + 'PyQt5', + 'numpy', + 'pytest' ], - package_dir={'avpython': 'src'}, - package_data={ - 'avpython': package_files('src'), - }, - install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'], entry_points={ 'gui_scripts': [ - 'avp = avpython.main:main' + f'avp = {PACKAGE_NAME}.main:main' ], } ) diff --git a/src/__init__.py b/src/__init__.py index 73f174a..08131ce 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,9 @@ import os import logging +__version__ = '2.0.0rc6' + + class Logger(logging.getLoggerClass()): ''' Custom Logger class to handle custom VERBOSE log level. @@ -31,6 +34,3 @@ if getattr(sys, 'frozen', False): else: # unfrozen wd = os.path.dirname(os.path.realpath(__file__)) - -# make relative imports work when using /src as a package -sys.path.insert(0, wd) diff --git a/src/__main__.py b/src/__main__.py index 3babeae..3206bc8 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,5 +1,5 @@ -# Allows for launching with python3 -m avpython +# Allows for launching with python3 -m avp -from avpython.main import main +from .main import main main() diff --git a/src/component.py b/src/component.py index f3ee188..33c7657 100644 --- a/src/component.py +++ b/src/component.py @@ -11,8 +11,8 @@ import time import logging from copy import copy -from toolkit.frame import BlankFrame -from toolkit import ( +from .toolkit.frame import BlankFrame +from .toolkit import ( getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals ) diff --git a/src/components/color.py b/src/components/color.py index 7d4f86d..6336194 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -4,8 +4,8 @@ from PyQt5.QtGui import QColor from PIL.ImageQt import ImageQt import os -from component import Component -from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor class Component(Component): diff --git a/src/components/image.py b/src/components/image.py index dd363bf..42f9564 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,8 +2,8 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/life.py b/src/components/life.py index 7a610eb..94704bc 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -4,8 +4,8 @@ from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter import os import math -from component import Component -from toolkit.frame import BlankFrame, scale +from ..component import Component +from ..toolkit.frame import BlankFrame, scale class Component(Component): diff --git a/src/components/original.py b/src/components/original.py index f886374..80228fe 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -6,8 +6,8 @@ import os import time from copy import copy -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/sound.py b/src/components/sound.py index 18d2a65..118ea23 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -1,8 +1,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 6675f5b..d1f8fb6 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -6,10 +6,10 @@ import subprocess import time import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput, connectWidget -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput, connectWidget +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/components/text.py b/src/components/text.py index 32a108e..e8c5a9c 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -4,8 +4,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os import logging -from component import Component -from toolkit.frame import FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import FramePainter, PaintColor log = logging.getLogger('AVP.Components.Text') diff --git a/src/components/video.py b/src/components/video.py index 8ad21b5..070940d 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -5,10 +5,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo -from toolkit import checkOutput +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from ..toolkit import checkOutput log = logging.getLogger('AVP.Components.Video') diff --git a/src/components/waveform.py b/src/components/waveform.py index cbfc47f..1a6035f 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -6,10 +6,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/core.py b/src/core.py index d7445c9..bc6f9b4 100644 --- a/src/core.py +++ b/src/core.py @@ -9,12 +9,12 @@ import json from importlib import import_module import logging -import toolkit +from . import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.WARNING -FILE_LOGLVL = None +FILE_LOGLVL = logging.ERROR class Core: @@ -47,7 +47,7 @@ class Core: yield name log.debug('Importing component modules') self.modules = [ - import_module('components.%s' % name) + import_module('.components.%s' % name, __package__) for name in findComponents() ] # store canonical module names and indexes @@ -426,7 +426,7 @@ class Core: def newVideoWorker(self, loader, audioFile, outputPath): '''loader is MainWindow or Command object which must own the thread''' - import video_thread + from . import video_thread self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents @@ -450,8 +450,8 @@ class Core: @classmethod def storeSettings(cls): '''Store settings/paths to directories as class variables''' - from __init__ import wd - from toolkit.ffmpeg import findFfmpeg + from .__init__ import wd + from .toolkit.ffmpeg import findFfmpeg cls.wd = wd dataDir = QtCore.QStandardPaths.writableLocation( diff --git a/src/gui/actions.py b/src/gui/actions.py index 8e867b9..eb7b953 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QUndoCommand import os from copy import copy -from core import Core +from ..core import Core # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 75534c2..da8370d 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,12 +16,12 @@ 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 ( +from ..core import Core +from . import preview_thread +from .preview_win import PreviewWindow +from .presetmanager import PresetManager +from .actions import * +from ..toolkit import ( disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals ) @@ -65,7 +65,6 @@ class MainWindow(QtWidgets.QMainWindow): self.settings = Core.settings # Register clean-up functions - signal.signal(signal.SIGINT, self.terminate) atexit.register(self.cleanUp) # Create stack of undoable user actions diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 2445760..1e47a7f 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -7,9 +7,9 @@ import string import os import logging -from toolkit import badName -from core import Core -from gui.actions import * +from ..toolkit import badName +from ..core import Core +from .actions import * log = logging.getLogger('AVP.Gui.PresetManager') diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index d3e0581..7829476 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -10,8 +10,8 @@ from queue import Queue, Empty import os import logging -from toolkit.frame import Checkerboard -from toolkit import disableWhenOpeningProject +from ..toolkit.frame import Checkerboard +from ..toolkit import disableWhenOpeningProject log = logging.getLogger("AVP.Gui.PreviewThread") diff --git a/src/main.py b/src/main.py index 126e4a8..5fabda3 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ import sys import os import logging -from __init__ import wd +from .__init__ import wd log = logging.getLogger('AVP.Main') @@ -12,6 +12,7 @@ log = logging.getLogger('AVP.Main') def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") + proj = None # Determine mode mode = 'GUI' @@ -23,19 +24,17 @@ def main(): else: # opening a project file with gui proj = sys.argv[1] - else: - # normal gui launch - proj = None # Launch program if mode == 'commandline': - from command import Command + from .command import Command main = Command() + main.parseArgs() log.debug("Finished creating command object") elif mode == 'GUI': - from gui.mainwindow import MainWindow + from .gui.mainwindow import MainWindow window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() diff --git a/src/toolkit/__init__.py b/src/toolkit/__init__.py index 3fca275..55e5f84 100644 --- a/src/toolkit/__init__.py +++ b/src/toolkit/__init__.py @@ -1 +1 @@ -from toolkit.common import * +from .common import * diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 419d491..3298c04 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -10,8 +10,8 @@ import signal from queue import PriorityQueue import logging -import core -from toolkit.common import checkOutput, pipeWrapper +from .. import core +from .common import checkOutput, pipeWrapper log = logging.getLogger('AVP.Toolkit.Ffmpeg') @@ -90,7 +90,7 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): - from component import ComponentError + from ..component import ComponentError if core.Core.logEnabled: logFilename = os.path.join( core.Core.logDir, 'render_%s.log' % str(self.component.compPos) @@ -144,7 +144,7 @@ def openPipe(commandList, **kwargs): def closePipe(pipe): pipe.stdout.close() - pipe.send_signal(signal.SIGINT) + pipe.send_signal(signal.SIGTERM) def findFfmpeg(): diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 0e200b5..f2511fe 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -9,7 +9,7 @@ import os import math import logging -import core +from .. import core log = logging.getLogger('AVP.Toolkit.Frame') diff --git a/src/video_thread.py b/src/video_thread.py index 0a39f28..31331a3 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,9 @@ import time import signal import logging -from component import ComponentError -from toolkit.frame import Checkerboard -from toolkit.ffmpeg import ( +from .component import ComponentError +from .toolkit.frame import Checkerboard +from .toolkit.ffmpeg import ( openPipe, readAudioFile, getAudioDuration, createFfmpegCommand ) @@ -400,7 +400,7 @@ class Worker(QtCore.QObject): comp.cancel() try: - self.out_pipe.send_signal(signal.SIGINT) + self.out_pipe.send_signal(signal.SIGTERM) except Exception: pass -- cgit v1.2.3 From 17b4cba6d1a5f24b4de3b53f79b93dd409e28ccd Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 26 Apr 2022 13:10:29 -0400 Subject: tests for commandline argument parsing --- src/command.py | 6 +++++- src/main.py | 12 +++++------ src/tests/__init__.py | 4 ++++ src/tests/test_commandline_parser.py | 39 ++++++++++++++++++++++++++++++++++++ src/tests/test_export_classic.py | 5 ----- 5 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 src/tests/test_commandline_parser.py delete mode 100644 src/tests/test_export_classic.py (limited to 'src/main.py') diff --git a/src/command.py b/src/command.py index 0aab0f7..db72de7 100644 --- a/src/command.py +++ b/src/command.py @@ -133,14 +133,18 @@ class Command(QtCore.QObject): if 'audioFile' in key: input = value self.createAudioVisualisation(input, output) + return "commandline" elif self.args.input and self.args.output: self.createAudioVisualisation(self.args.input, self.args.output) + return "commandline" - elif 'help' not in sys.argv: + elif 'help' not in sys.argv and self.args.projpath is None and '--debug' not in sys.argv: self.parser.print_help() quit(1) + return "GUI" + def createAudioVisualisation(self, input, output): self.core.selectedComponents = list( reversed(self.core.selectedComponents)) diff --git a/src/main.py b/src/main.py index 5fabda3..39fa997 100644 --- a/src/main.py +++ b/src/main.py @@ -30,25 +30,25 @@ def main(): from .command import Command main = Command() - main.parseArgs() + mode = main.parseArgs() log.debug("Finished creating command object") - elif mode == 'GUI': + # Both branches here may occur in one execution: + # Commandline parsing could change mode back to GUI + if mode == 'GUI': from .gui.mainwindow import MainWindow window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) - # window.adjustSize() desc = QtWidgets.QDesktopWidget() dpi = desc.physicalDpiX() - - topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96)) + log.info("Detected screen DPI: %s", dpi) + window.resize( int(window.width() * (dpi / 96)), int(window.height() * (dpi / 96)) ) - # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) main = MainWindow(window, proj) log.debug("Finished creating main window") diff --git a/src/tests/__init__.py b/src/tests/__init__.py index f2b2ff1..062dca7 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -16,6 +16,10 @@ def command(): return Command() +def getTestData(filename): + return os.path.join(Core.wd, 'tests', 'data', filename) + + def run(logFile): """Run Pytest, which then imports and runs all tests in this module.""" with open(logFile, "w") as f: diff --git a/src/tests/test_commandline_parser.py b/src/tests/test_commandline_parser.py new file mode 100644 index 0000000..d672441 --- /dev/null +++ b/src/tests/test_commandline_parser.py @@ -0,0 +1,39 @@ +import sys +import pytest +from .__init__ import command + + +def test_commandline_help(command): + sys.argv = ['', '--help'] + with pytest.raises(SystemExit): + command.parseArgs() + + +def test_commandline_help_if_bad_args(command): + sys.argv = ['', '--junk'] + with pytest.raises(SystemExit): + command.parseArgs() + + +def test_commandline_launches_gui_if_debug(command): + sys.argv = ['', '--debug'] + mode = command.parseArgs() + assert mode == "GUI" + + +def test_commandline_launches_gui_if_debug_with_project(command): + sys.argv = ['', 'test', '--debug'] + mode = command.parseArgs() + assert mode == "GUI" + + +def test_commandline_export_creates_audio_visualization(command): + didCallFunction = False + def captureFunction(*args): + nonlocal didCallFunction + didCallFunction = True + + sys.argv = ['', '-c', '0', 'classic', '-i', '_', '-o', '_'] + command.createAudioVisualisation = captureFunction + command.parseArgs() + assert didCallFunction diff --git a/src/tests/test_export_classic.py b/src/tests/test_export_classic.py deleted file mode 100644 index a6d3e8c..0000000 --- a/src/tests/test_export_classic.py +++ /dev/null @@ -1,5 +0,0 @@ -from .__init__ import command - - -def test_export_classic_visualizer_default(command): - assert command -- cgit v1.2.3 From a4dff0b3e0aa817822c1a490a423192a8cbf97eb Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 28 Apr 2022 19:48:01 -0400 Subject: remove punctuation from project names at commandline stop someone shooting themself in the foot by doing `avp /?` on Windows --- src/main.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/main.py') diff --git a/src/main.py b/src/main.py index 39fa997..ec4b8bc 100644 --- a/src/main.py +++ b/src/main.py @@ -2,6 +2,8 @@ from PyQt5 import uic, QtWidgets import sys import os import logging +import re +import string from .__init__ import wd @@ -10,11 +12,8 @@ log = logging.getLogger('AVP.Main') def main(): - app = QtWidgets.QApplication(sys.argv) - app.setApplicationName("audio-visualizer") + # Determine primary mode proj = None - - # Determine mode mode = 'GUI' if len(sys.argv) > 2: mode = 'commandline' @@ -22,9 +21,14 @@ def main(): if sys.argv[1].startswith('-'): mode = 'commandline' else: + # remove unsafe punctuation characters such as \/?*&^%$# + sys.argv[1] = re.sub(f'[{re.escape(string.punctuation)}]', '', sys.argv[1]) # opening a project file with gui proj = sys.argv[1] + # Create Qt Application + app = QtWidgets.QApplication(sys.argv) + app.setApplicationName("audio-visualizer") # Launch program if mode == 'commandline': from .command import Command @@ -56,5 +60,6 @@ def main(): sys.exit(app.exec_()) + if __name__ == "__main__": main() -- cgit v1.2.3 From c2c3f0aa5adf3127b84b3d50da9e1aa655c8a824 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 29 Apr 2022 21:15:17 -0400 Subject: remove extra window properties from window objects instead of windows with properties which are windows, windows now have the UI added directly to them using an argument of `uic.loadUi` Also, DPI scaling moved to MainWindow __init__ --- src/components/spectrum.py | 6 +- src/components/video.py | 4 +- src/components/waveform.py | 6 +- src/core.py | 2 +- src/gui/actions.py | 8 +- src/gui/mainwindow.py | 351 +++++++++++++++++++++++---------------------- src/gui/presetmanager.py | 88 ++++++------ src/gui/preview_win.py | 2 +- src/main.py | 18 +-- 9 files changed, 242 insertions(+), 243 deletions(-) (limited to 'src/main.py') diff --git a/src/components/spectrum.py b/src/components/spectrum.py index d1f8fb6..91f2afb 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -30,9 +30,9 @@ class Component(Component): self.previewSize = (214, 120) self.previewPipe = None - if hasattr(self.parent, 'window'): + if hasattr(self.parent, 'lineEdit_audioFile'): # update preview when audio file changes (if genericPreview is off) - self.parent.window.lineEdit_audioFile.textChanged.connect( + self.parent.lineEdit_audioFile.textChanged.connect( self.update ) @@ -123,7 +123,7 @@ class Component(Component): genericPreview = self.settings.value("pref_genericPreview") startPt = 0 if not genericPreview: - inputFile = self.parent.window.lineEdit_audioFile.text() + inputFile = self.parent.lineEdit_audioFile.text() if not inputFile or not os.path.exists(inputFile): return duration = getAudioDuration(inputFile) diff --git a/src/components/video.py b/src/components/video.py index 070940d..9fffc26 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -63,8 +63,8 @@ class Component(Component): def properties(self): props = [] - if hasattr(self.parent, 'window'): - outputFile = self.parent.window.lineEdit_outputFile.text() + if hasattr(self.parent, 'lineEdit_outputFile'): + outputFile = self.parent.lineEdit_outputFile.text() else: outputFile = str(self.parent.args.output) diff --git a/src/components/waveform.py b/src/components/waveform.py index 1a6035f..227f711 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -27,8 +27,8 @@ class Component(Component): self.page.lineEdit_color.setText('255,255,255') - if hasattr(self.parent, 'window'): - self.parent.window.lineEdit_audioFile.textChanged.connect( + if hasattr(self.parent, 'lineEdit_audioFile'): + self.parent.lineEdit_audioFile.textChanged.connect( self.update ) @@ -82,7 +82,7 @@ class Component(Component): genericPreview = self.settings.value("pref_genericPreview") startPt = 0 if not genericPreview: - inputFile = self.parent.window.lineEdit_audioFile.text() + inputFile = self.parent.lineEdit_audioFile.text() if not inputFile or not os.path.exists(inputFile): return duration = getAudioDuration(inputFile) diff --git a/src/core.py b/src/core.py index 42fd1c3..225d8e0 100644 --- a/src/core.py +++ b/src/core.py @@ -181,7 +181,7 @@ class Core: try: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: - widget = eval('loader.window.%s' % widget) + widget = eval('loader.%s' % widget) with toolkit.blockSignals(widget): toolkit.setWidgetValue(widget, value) diff --git a/src/gui/actions.py b/src/gui/actions.py index eb7b953..afb980a 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -41,7 +41,7 @@ class RemoveComponent(QUndoCommand): def __init__(self, parent, selectedRows): super().__init__('remove component') self.parent = parent - componentList = self.parent.window.listWidget_componentList + componentList = self.parent.listWidget_componentList self.selectedRows = [ componentList.row(selected) for selected in selectedRows ] @@ -53,7 +53,7 @@ class RemoveComponent(QUndoCommand): self.parent._removeComponent(self.selectedRows[0]) def undo(self): - componentList = self.parent.window.listWidget_componentList + componentList = self.parent.listWidget_componentList for index, comp in zip(self.selectedRows, self.components): self.parent.core.insertComponent( index, comp, self.parent @@ -78,7 +78,7 @@ class MoveComponent(QUndoCommand): return True def do(self, rowa, rowb): - componentList = self.parent.window.listWidget_componentList + componentList = self.parent.listWidget_componentList page = self.parent.pages.pop(rowa) self.parent.pages.insert(rowb, page) @@ -86,7 +86,7 @@ class MoveComponent(QUndoCommand): item = componentList.takeItem(rowa) componentList.insertItem(rowb, item) - stackedWidget = self.parent.window.stackedWidget + stackedWidget = self.parent.stackedWidget widget = stackedWidget.removeWidget(page) stackedWidget.insertWidget(rowb, page) componentList.setCurrentRow(rowb) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 463d028..c31eec9 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -4,13 +4,12 @@ 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 PyQt5 import QtCore, QtGui, QtWidgets, uic +import PyQt5.QtWidgets as QtWidgets from PIL import Image from queue import Queue import sys import os -import signal import atexit import filecmp import time @@ -43,11 +42,22 @@ class MainWindow(QtWidgets.QMainWindow): newTask = QtCore.pyqtSignal(list) # for the preview window processTask = QtCore.pyqtSignal() - def __init__(self, window, project): - QtWidgets.QMainWindow.__init__(self) + def __init__(self, project): + super().__init__() log.debug( 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) - self.window = window + uic.loadUi(os.path.join(Core.wd, "gui", "mainwindow.ui"), self) + desk = QtWidgets.QDesktopWidget() + dpi = desk.physicalDpiX() + log.info("Detected screen DPI: %s", dpi) + + self.resize( + int(self.width() * + (dpi / 96)), + int(self.height() * + (dpi / 96)) + ) + self.core = Core() Core.mode = 'GUI' # widgets of component settings @@ -73,15 +83,13 @@ class MainWindow(QtWidgets.QMainWindow): self.undoStack.setUndoLimit(undoLimit) # Create Preset Manager - self.presetManager = PresetManager( - uic.loadUi( - os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) + self.presetManager = PresetManager(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) + self.verticalLayout_previewWrapper.addWidget(self.previewWindow) log.debug('Starting preview thread') self.previewQueue = Queue() @@ -105,7 +113,7 @@ class MainWindow(QtWidgets.QMainWindow): self.timer.start(timeout) # Begin decorating the window and connecting events - componentList = self.window.listWidget_componentList + componentList = self.listWidget_componentList # Undo Feature def toggleUndoButtonEnabled(*_): @@ -116,15 +124,15 @@ class MainWindow(QtWidgets.QMainWindow): # program is probably in midst of exiting pass - style = window.pushButton_undo.style() - undoButton = window.pushButton_undo + style = self.pushButton_undo.style() + undoButton = self.pushButton_undo undoButton.setIcon( style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack) ) undoButton.clicked.connect(self.undoStack.undo) undoButton.setEnabled(False) self.undoStack.cleanChanged.connect(toggleUndoButtonEnabled) - self.undoMenu = QMenu() + self.undoMenu = QtWidgets.QMenu() self.undoMenu.addAction( self.undoStack.createUndoAction(self) ) @@ -138,93 +146,93 @@ class MainWindow(QtWidgets.QMainWindow): undoButton.setMenu(self.undoMenu) # end of Undo Feature - style = window.pushButton_listMoveUp.style() - window.pushButton_listMoveUp.setIcon( + style = self.pushButton_listMoveUp.style() + self.pushButton_listMoveUp.setIcon( style.standardIcon(QtWidgets.QStyle.SP_ArrowUp) ) - style = window.pushButton_listMoveDown.style() - window.pushButton_listMoveDown.setIcon( + style = self.pushButton_listMoveDown.style() + self.pushButton_listMoveDown.setIcon( style.standardIcon(QtWidgets.QStyle.SP_ArrowDown) ) - style = window.pushButton_removeComponent.style() - window.pushButton_removeComponent.setIcon( + style = self.pushButton_removeComponent.style() + self.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) + self.progressBar_createVideo.setTextVisible(False) else: - window.progressLabel.setHidden(True) + self.progressLabel.setHidden(True) - window.toolButton_selectAudioFile.clicked.connect( + self.toolButton_selectAudioFile.clicked.connect( self.openInputFileDialog) - window.toolButton_selectOutputFile.clicked.connect( + self.toolButton_selectOutputFile.clicked.connect( self.openOutputFileDialog) def changedField(): self.autosave() self.updateWindowTitle() - window.lineEdit_audioFile.textChanged.connect(changedField) - window.lineEdit_outputFile.textChanged.connect(changedField) + self.lineEdit_audioFile.textChanged.connect(changedField) + self.lineEdit_outputFile.textChanged.connect(changedField) - window.progressBar_createVideo.setValue(0) + self.progressBar_createVideo.setValue(0) - window.pushButton_createVideo.clicked.connect( + self.pushButton_createVideo.clicked.connect( self.createAudioVisualisation) - window.pushButton_Cancel.clicked.connect(self.stopVideo) + self.pushButton_Cancel.clicked.connect(self.stopVideo) for i, container in enumerate(Core.encoderOptions['containers']): - window.comboBox_videoContainer.addItem(container['name']) + self.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.comboBox_videoContainer.setCurrentIndex(selectedContainer) + self.comboBox_videoContainer.currentIndexChanged.connect( self.updateCodecs ) self.updateCodecs() - for i in range(window.comboBox_videoCodec.count()): - codec = window.comboBox_videoCodec.itemText(i) + for i in range(self.comboBox_videoCodec.count()): + codec = self.comboBox_videoCodec.itemText(i) if codec == self.settings.value('outputVideoCodec'): - window.comboBox_videoCodec.setCurrentIndex(i) + self.comboBox_videoCodec.setCurrentIndex(i) - for i in range(window.comboBox_audioCodec.count()): - codec = window.comboBox_audioCodec.itemText(i) + for i in range(self.comboBox_audioCodec.count()): + codec = self.comboBox_audioCodec.itemText(i) if codec == self.settings.value('outputAudioCodec'): - window.comboBox_audioCodec.setCurrentIndex(i) + self.comboBox_audioCodec.setCurrentIndex(i) - window.comboBox_videoCodec.currentIndexChanged.connect( + self.comboBox_videoCodec.currentIndexChanged.connect( self.updateCodecSettings ) - window.comboBox_audioCodec.currentIndexChanged.connect( + self.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) + self.spinBox_vBitrate.setValue(vBitrate) + self.spinBox_aBitrate.setValue(aBitrate) + self.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) + self.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) # Make component buttons - self.compMenu = QMenu() + self.compMenu = QtWidgets.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) + self.pushButton_addComponent.setMenu(self.compMenu) componentList.dropEvent = self.dragComponent componentList.itemSelectionChanged.connect( @@ -233,7 +241,7 @@ class MainWindow(QtWidgets.QMainWindow): componentList.itemSelectionChanged.connect( self.presetManager.clearPresetListSelection ) - self.window.pushButton_removeComponent.clicked.connect( + self.pushButton_removeComponent.clicked.connect( lambda: self.removeComponent() ) @@ -245,33 +253,33 @@ class MainWindow(QtWidgets.QMainWindow): currentRes = str(self.settings.value('outputWidth'))+'x' + \ str(self.settings.value('outputHeight')) for i, res in enumerate(Core.resolutions): - window.comboBox_resolution.addItem(res) + self.comboBox_resolution.addItem(res) if res == currentRes: currentRes = i - window.comboBox_resolution.setCurrentIndex(currentRes) - window.comboBox_resolution.currentIndexChanged.connect( + self.comboBox_resolution.setCurrentIndex(currentRes) + self.comboBox_resolution.currentIndexChanged.connect( self.updateResolution ) - self.window.pushButton_listMoveUp.clicked.connect( + self.pushButton_listMoveUp.clicked.connect( lambda: self.moveComponent(-1) ) - self.window.pushButton_listMoveDown.clicked.connect( + self.pushButton_listMoveDown.clicked.connect( lambda: self.moveComponent(1) ) # Configure the Projects Menu - self.projectMenu = QMenu() - self.window.menuButton_newProject = self.projectMenu.addAction( + self.projectMenu = QtWidgets.QMenu() + self.menuButton_newProject = self.projectMenu.addAction( "New Project" ) - self.window.menuButton_newProject.triggered.connect( + self.menuButton_newProject.triggered.connect( lambda: self.createNewProject() ) - self.window.menuButton_openProject = self.projectMenu.addAction( + self.menuButton_openProject = self.projectMenu.addAction( "Open Project" ) - self.window.menuButton_openProject.triggered.connect( + self.menuButton_openProject.triggered.connect( lambda: self.openOpenProjectDialog() ) @@ -281,16 +289,16 @@ class MainWindow(QtWidgets.QMainWindow): action = self.projectMenu.addAction("Save Project As") action.triggered.connect(self.openSaveProjectDialog) - self.window.pushButton_projects.setMenu(self.projectMenu) + self.pushButton_projects.setMenu(self.projectMenu) # Configure the Presets Button - self.window.pushButton_presets.clicked.connect( + self.pushButton_presets.clicked.connect( self.openPresetManager ) self.updateWindowTitle() log.debug('Showing main window') - window.show() + self.show() if project and project != self.autosavePath: if not project.endswith('.avp'): @@ -358,77 +366,80 @@ class MainWindow(QtWidgets.QMainWindow): 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+S", self, self.saveCurrentProject) + QtWidgets.QShortcut("Ctrl+A", self, self.openSaveProjectDialog) + QtWidgets.QShortcut("Ctrl+O", self, self.openOpenProjectDialog) + QtWidgets.QShortcut("Ctrl+N", self, 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 undo/redo + QtWidgets.QShortcut("Ctrl+Z", self, self.undoStack.undo) + QtWidgets.QShortcut("Ctrl+Y", self, self.undoStack.redo) + QtWidgets.QShortcut("Ctrl+Shift+Z", self, 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() + inskey, self, + activated=lambda: self.pushButton_addComponent.click() ) for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete): QtWidgets.QShortcut( - delkey, self.window.listWidget_componentList, + delkey, self.listWidget_componentList, self.removeComponent ) QtWidgets.QShortcut( - "Ctrl+Space", self.window, - activated=lambda: self.window.listWidget_componentList.setFocus() + "Ctrl+Space", self, + activated=lambda: self.listWidget_componentList.setFocus() ) QtWidgets.QShortcut( - "Ctrl+Shift+S", self.window, + "Ctrl+Shift+S", self, self.presetManager.openSavePresetDialog ) QtWidgets.QShortcut( - "Ctrl+Shift+C", self.window, self.presetManager.clearPreset + "Ctrl+Shift+C", self, self.presetManager.clearPreset ) QtWidgets.QShortcut( - "Ctrl+Up", self.window.listWidget_componentList, + "Ctrl+Up", self.listWidget_componentList, activated=lambda: self.moveComponent(-1) ) QtWidgets.QShortcut( - "Ctrl+Down", self.window.listWidget_componentList, + "Ctrl+Down", self.listWidget_componentList, activated=lambda: self.moveComponent(1) ) QtWidgets.QShortcut( - "Ctrl+Home", self.window.listWidget_componentList, + "Ctrl+Home", self.listWidget_componentList, activated=lambda: self.moveComponent('top') ) QtWidgets.QShortcut( - "Ctrl+End", self.window.listWidget_componentList, + "Ctrl+End", self.listWidget_componentList, activated=lambda: self.moveComponent('bottom') ) QtWidgets.QShortcut( - "Ctrl+Shift+F", self.window, self.showFfmpegCommand + "Ctrl+Shift+F", self, self.showFfmpegCommand ) QtWidgets.QShortcut( - "Ctrl+Shift+U", self.window, self.showUndoStack + "Ctrl+Shift+U", self, self.showUndoStack ) if log.isEnabledFor(logging.DEBUG): QtWidgets.QShortcut( - "Ctrl+Alt+Shift+R", self.window, self.drawPreview + "Ctrl+Alt+Shift+R", self, self.drawPreview ) QtWidgets.QShortcut( - "Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self)) + "Ctrl+Alt+Shift+A", self, lambda: log.debug(repr(self)) ) def __repr__(self): return ( + '%s\n' '\n%s\n' '#####\n' 'Preview thread is %s\n' % ( - repr(self.core), - 'live' if self.previewThread.isRunning() else 'dead', + super().__repr__(), + "core not initialized" if not hasattr(self, "core") else repr(self.core), + 'live' if hasattr(self, "previewThread") and self.previewThread.isRunning() else 'dead', ) ) @@ -456,7 +467,7 @@ class MainWindow(QtWidgets.QMainWindow): except AttributeError: pass log.verbose(f'Window title is "{appName}"') - self.window.setWindowTitle(appName) + self.setWindowTitle(appName) @QtCore.pyqtSlot(int, dict) def updateComponentTitle(self, pos, presetStore=False): @@ -492,12 +503,12 @@ class MainWindow(QtWidgets.QMainWindow): 'Setting %s #%s\'s title: %s', name, pos, title ) - self.window.listWidget_componentList.item(pos).setText(title) + self.listWidget_componentList.item(pos).setText(title) def updateCodecs(self): - containerWidget = self.window.comboBox_videoContainer - vCodecWidget = self.window.comboBox_videoCodec - aCodecWidget = self.window.comboBox_audioCodec + containerWidget = self.comboBox_videoContainer + vCodecWidget = self.comboBox_videoCodec + aCodecWidget = self.comboBox_audioCodec index = containerWidget.currentIndex() name = containerWidget.itemText(index) self.settings.setValue('outputContainer', name) @@ -514,10 +525,10 @@ class MainWindow(QtWidgets.QMainWindow): 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 + vCodecWidget = self.comboBox_videoCodec + vBitrateWidget = self.spinBox_vBitrate + aBitrateWidget = self.spinBox_aBitrate + aCodecWidget = self.comboBox_audioCodec currentVideoCodec = vCodecWidget.currentIndex() currentVideoCodec = vCodecWidget.itemText(currentVideoCodec) currentVideoBitrate = vBitrateWidget.value() @@ -535,7 +546,7 @@ class MainWindow(QtWidgets.QMainWindow): 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.core.createProjectFile(self.autosavePath, self) self.lastAutosave = time.time() if len(self.autosaveTimes) >= 5: # Do some math to reduce autosave spam. This gives a smooth @@ -588,25 +599,25 @@ class MainWindow(QtWidgets.QMainWindow): inputDir = self.settings.value("inputDir", os.path.expanduser("~")) fileName, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Open Audio File", + self, "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) + self.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", + self, "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) + self.lineEdit_outputFile.setText(fileName) def stopVideo(self): log.info('Export cancelled') @@ -615,8 +626,8 @@ class MainWindow(QtWidgets.QMainWindow): def createAudioVisualisation(self): # create output video if mandatory settings are filled in - audioFile = self.window.lineEdit_audioFile.text() - outputPath = self.window.lineEdit_outputFile.text() + audioFile = self.lineEdit_audioFile.text() + outputPath = self.lineEdit_outputFile.text() if audioFile and outputPath and self.core.selectedComponents: if not os.path.dirname(outputPath): @@ -670,62 +681,62 @@ class MainWindow(QtWidgets.QMainWindow): 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) + self.pushButton_createVideo.setEnabled(False) + self.pushButton_Cancel.setEnabled(True) + self.comboBox_resolution.setEnabled(False) + self.stackedWidget.setEnabled(False) + self.tab_encoderSettings.setEnabled(False) + self.label_audioFile.setEnabled(False) + self.toolButton_selectAudioFile.setEnabled(False) + self.label_outputFile.setEnabled(False) + self.toolButton_selectOutputFile.setEnabled(False) + self.lineEdit_audioFile.setEnabled(False) + self.lineEdit_outputFile.setEnabled(False) + self.pushButton_addComponent.setEnabled(False) + self.pushButton_removeComponent.setEnabled(False) + self.pushButton_listMoveDown.setEnabled(False) + self.pushButton_listMoveUp.setEnabled(False) + self.menuButton_newProject.setEnabled(False) + self.menuButton_openProject.setEnabled(False) if sys.platform == 'darwin': - self.window.progressLabel.setHidden(False) + self.progressLabel.setHidden(False) else: - self.window.listWidget_componentList.setEnabled(False) + self.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.pushButton_createVideo.setEnabled(True) + self.pushButton_Cancel.setEnabled(False) + self.comboBox_resolution.setEnabled(True) + self.stackedWidget.setEnabled(True) + self.tab_encoderSettings.setEnabled(True) + self.label_audioFile.setEnabled(True) + self.toolButton_selectAudioFile.setEnabled(True) + self.lineEdit_audioFile.setEnabled(True) + self.label_outputFile.setEnabled(True) + self.toolButton_selectOutputFile.setEnabled(True) + self.lineEdit_outputFile.setEnabled(True) + self.pushButton_addComponent.setEnabled(True) + self.pushButton_removeComponent.setEnabled(True) + self.pushButton_listMoveDown.setEnabled(True) + self.pushButton_listMoveUp.setEnabled(True) + self.menuButton_newProject.setEnabled(True) + self.menuButton_openProject.setEnabled(True) + self.listWidget_componentList.setEnabled(True) + self.progressLabel.setHidden(True) self.drawPreview(True) @QtCore.pyqtSlot(int) def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) + self.progressBar_createVideo.setValue(value) @QtCore.pyqtSlot(str) def progressBarSetText(self, value): if sys.platform == 'darwin': - self.window.progressLabel.setText(value) + self.progressLabel.setText(value) else: - self.window.progressBar_createVideo.setFormat(value) + self.progressBar_createVideo.setFormat(value) def updateResolution(self): - resIndex = int(self.window.comboBox_resolution.currentIndex()) + resIndex = int(self.comboBox_resolution.currentIndex()) res = Core.resolutions[resIndex].split('x') changed = res[0] != self.settings.value("outputWidth") self.settings.setValue('outputWidth', res[0]) @@ -750,7 +761,7 @@ class MainWindow(QtWidgets.QMainWindow): self.previewWindow.changePixmap(image) def showUndoStack(self): - dialog = QtWidgets.QDialog(self.window) + dialog = QtWidgets.QDialog(self) undoView = QtWidgets.QUndoView(self.undoStack) layout = QtWidgets.QVBoxLayout() layout.addWidget(undoView) @@ -761,8 +772,8 @@ class MainWindow(QtWidgets.QMainWindow): from textwrap import wrap from ..toolkit.ffmpeg import createFfmpegCommand command = createFfmpegCommand( - self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text(), + self.lineEdit_audioFile.text(), + self.lineEdit_outputFile.text(), self.core.selectedComponents ) command = " ".join(command) @@ -779,8 +790,8 @@ class MainWindow(QtWidgets.QMainWindow): def insertComponent(self, index): '''Triggered by Core to finish initializing a new component.''' - componentList = self.window.listWidget_componentList - stackedWidget = self.window.stackedWidget + componentList = self.listWidget_componentList + stackedWidget = self.stackedWidget componentList.insertItem( index, @@ -798,15 +809,15 @@ class MainWindow(QtWidgets.QMainWindow): return index def removeComponent(self): - componentList = self.window.listWidget_componentList + componentList = self.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 = self.stackedWidget + componentList = self.listWidget_componentList stackedWidget.removeWidget(self.pages[index]) componentList.takeItem(index) self.core.removeComponent(index) @@ -817,7 +828,7 @@ class MainWindow(QtWidgets.QMainWindow): @disableWhenEncoding def moveComponent(self, change): '''Moves a component relatively from its current position''' - componentList = self.window.listWidget_componentList + componentList = self.listWidget_componentList tag = change if change == 'top': change = -componentList.currentRow() @@ -837,7 +848,7 @@ class MainWindow(QtWidgets.QMainWindow): Given a QPos, returns the component index under the mouse cursor or -1 if no component is there. ''' - componentList = self.window.listWidget_componentList + componentList = self.listWidget_componentList modelIndexes = [ componentList.model().index(i) @@ -859,7 +870,7 @@ class MainWindow(QtWidgets.QMainWindow): @disableWhenEncoding def dragComponent(self, event): '''Used as Qt drop event for the component listwidget''' - componentList = self.window.listWidget_componentList + componentList = self.listWidget_componentList mousePos = self.getComponentListMousePos(event.pos()) if mousePos > -1: change = (componentList.currentRow() - mousePos) * -1 @@ -868,25 +879,25 @@ class MainWindow(QtWidgets.QMainWindow): self.moveComponent(change) def changeComponentWidget(self): - selected = self.window.listWidget_componentList.selectedItems() + selected = self.listWidget_componentList.selectedItems() if selected: - index = self.window.listWidget_componentList.row(selected[0]) - self.window.stackedWidget.setCurrentIndex(index) + index = self.listWidget_componentList.row(selected[0]) + self.stackedWidget.setCurrentIndex(index) def openPresetManager(self): '''Preset manager for importing, exporting, renaming, deleting''' - self.presetManager.show() + self.presetManager.show_() def clear(self): '''Get a blank slate''' self.core.clearComponents() - self.window.listWidget_componentList.clear() + self.listWidget_componentList.clear() for widget in self.pages: - self.window.stackedWidget.removeWidget(widget) + self.stackedWidget.removeWidget(widget) self.pages = [] for field in ( - self.window.lineEdit_audioFile, - self.window.lineEdit_outputFile + self.lineEdit_audioFile, + self.lineEdit_outputFile ): with blockSignals(field): field.setText('') @@ -906,7 +917,7 @@ class MainWindow(QtWidgets.QMainWindow): def saveCurrentProject(self): if self.currentProject: - self.core.createProjectFile(self.currentProject, self.window) + self.core.createProjectFile(self.currentProject, self) try: os.remove(self.autosavePath) except FileNotFoundError: @@ -933,7 +944,7 @@ class MainWindow(QtWidgets.QMainWindow): def openSaveProjectDialog(self): filename, _ = QtWidgets.QFileDialog.getSaveFileName( - self.window, "Create Project File", + self, "Create Project File", self.settings.value("projectDir"), "Project Files (*.avp)") if not filename: @@ -943,13 +954,13 @@ class MainWindow(QtWidgets.QMainWindow): self.settings.setValue("projectDir", os.path.dirname(filename)) self.settings.setValue("currentProject", filename) self.currentProject = filename - self.core.createProjectFile(filename, self.window) + self.core.createProjectFile(filename, self) self.updateWindowTitle() @disableWhenEncoding def openOpenProjectDialog(self): filename, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Open Project File", + self, "Open Project File", self.settings.value("projectDir"), "Project Files (*.avp)") self.openProject(filename) @@ -973,7 +984,7 @@ class MainWindow(QtWidgets.QMainWindow): self.updateWindowTitle() def showMessage(self, **kwargs): - parent = kwargs['parent'] if 'parent' in kwargs else self.window + parent = kwargs['parent'] if 'parent' in kwargs else self msg = QtWidgets.QMessageBox(parent) msg.setModal(True) msg.setText(kwargs['msg']) @@ -995,8 +1006,8 @@ class MainWindow(QtWidgets.QMainWindow): @disableWhenEncoding def componentContextMenu(self, QPos): '''Appears when right-clicking the component list''' - componentList = self.window.listWidget_componentList - self.menu = QMenu() + componentList = self.listWidget_componentList + self.menu = QtWidgets.QMenu() parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) index = self.getComponentListMousePos(QPos) @@ -1013,7 +1024,7 @@ class MainWindow(QtWidgets.QMainWindow): presets = self.presetManager.presets[ str(self.core.selectedComponents[index]) ] - self.presetSubmenu = QMenu("Open Preset") + self.presetSubmenu = QtWidgets.QMenu("Open Preset") self.menu.addMenu(self.presetSubmenu) for version, presetName in presets: @@ -1033,7 +1044,7 @@ class MainWindow(QtWidgets.QMainWindow): self.menu.addSeparator() # "Add Component" submenu - self.submenu = QMenu("Add") + self.submenu = QtWidgets.QMenu("Add") self.menu.addMenu(self.submenu) insertCompAtTop = self.settings.value("pref_insertCompAtTop") for i, comp in enumerate(self.core.modules): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 1e47a7f..9cf95b4 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -2,7 +2,7 @@ Preset manager object handles all interactions with presets, including the context menu accessed from MainWindow. ''' -from PyQt5 import QtCore, QtWidgets +from PyQt5 import QtCore, QtWidgets, uic import string import os import logging @@ -16,8 +16,10 @@ log = logging.getLogger('AVP.Gui.PresetManager') class PresetManager(QtWidgets.QDialog): - def __init__(self, window, parent): - super().__init__(parent.window) + def __init__(self, parent): + super().__init__() + uic.loadUi( + os.path.join(Core.wd, 'gui', 'presetmanager.ui'), self) self.parent = parent self.core = parent.core self.settings = parent.settings @@ -32,32 +34,31 @@ class PresetManager(QtWidgets.QDialog): # window self.lastFilter = '*' self.presetRows = [] # list of (comp, vers, name) tuples - self.window = window - self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) # connect button signals - self.window.pushButton_delete.clicked.connect( + self.pushButton_delete.clicked.connect( self.openDeletePresetDialog ) - self.window.pushButton_rename.clicked.connect( + self.pushButton_rename.clicked.connect( self.openRenamePresetDialog ) - self.window.pushButton_import.clicked.connect( + self.pushButton_import.clicked.connect( self.openImportDialog ) - self.window.pushButton_export.clicked.connect( + self.pushButton_export.clicked.connect( self.openExportDialog ) - self.window.pushButton_close.clicked.connect( - self.window.close + self.pushButton_close.clicked.connect( + self.close ) # create filter box and preset list self.drawFilterList() - self.window.comboBox_filter.currentIndexChanged.connect( + self.comboBox_filter.currentIndexChanged.connect( lambda: self.drawPresetList( - self.window.comboBox_filter.currentText(), - self.window.lineEdit_search.text() + self.comboBox_filter.currentText(), + self.lineEdit_search.text() ) ) @@ -65,23 +66,24 @@ class PresetManager(QtWidgets.QDialog): self.autocomplete = QtCore.QStringListModel() completer = QtWidgets.QCompleter() completer.setModel(self.autocomplete) - self.window.lineEdit_search.setCompleter(completer) - self.window.lineEdit_search.textChanged.connect( + self.lineEdit_search.setCompleter(completer) + self.lineEdit_search.textChanged.connect( lambda: self.drawPresetList( - self.window.comboBox_filter.currentText(), - self.window.lineEdit_search.text() + self.comboBox_filter.currentText(), + self.lineEdit_search.text() ) ) self.drawPresetList('*') - def show(self): + def show_(self): '''Open a new preset manager window from the mainwindow''' self.findPresets() self.drawFilterList() self.drawPresetList('*') - self.window.show() + self.show() def findPresets(self): + log.debug("Searching %s for presets", self.presetDir) parseList = [] for dirpath, dirnames, filenames in os.walk(self.presetDir): # anything without a subdirectory must be a preset folder @@ -106,7 +108,7 @@ class PresetManager(QtWidgets.QDialog): } def drawPresetList(self, compFilter=None, presetFilter=''): - self.window.listWidget_presets.clear() + self.listWidget_presets.clear() if compFilter: self.lastFilter = str(compFilter) else: @@ -118,7 +120,7 @@ class PresetManager(QtWidgets.QDialog): continue for vers, preset in presets: if not presetFilter or presetFilter in preset: - self.window.listWidget_presets.addItem( + self.listWidget_presets.addItem( '%s: %s' % (component, preset) ) self.presetRows.append((component, vers, preset)) @@ -127,22 +129,21 @@ class PresetManager(QtWidgets.QDialog): self.autocomplete.setStringList(presetNames) def drawFilterList(self): - self.window.comboBox_filter.clear() - self.window.comboBox_filter.addItem('*') + self.comboBox_filter.clear() + self.comboBox_filter.addItem('*') for component in self.presets: - self.window.comboBox_filter.addItem(component) + self.comboBox_filter.addItem(component) def clearPreset(self, compI=None): '''Functions on mainwindow level from the context menu''' - compI = self.parent.window.listWidget_componentList.currentRow() + compI = self.parent.listWidget_componentList.currentRow() action = ClearPreset(self.parent, compI) self.parent.undoStack.push(action) 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 + componentList = self.parent.listWidget_componentList if componentList.currentRow() == -1: return @@ -150,7 +151,7 @@ class PresetManager(QtWidgets.QDialog): index = componentList.currentRow() currentPreset = selectedComponents[index].currentPreset newName, OK = QtWidgets.QInputDialog.getText( - self.parent.window, + self.parent, 'Audio Visualizer', 'New Preset Name:', QtWidgets.QLineEdit.Normal, @@ -158,7 +159,7 @@ class PresetManager(QtWidgets.QDialog): ) if OK: if badName(newName): - self.warnMessage(self.parent.window) + self.warnMessage(self.parent) continue if newName: if index != -1: @@ -170,7 +171,7 @@ class PresetManager(QtWidgets.QDialog): vers = selectedComponents[index].version self.createNewPreset( componentName, vers, newName, - saveValueStore, window=self.parent.window) + saveValueStore, window=self.parent) self.findPresets() self.drawPresetList() self.openPreset(newName, index) @@ -185,8 +186,7 @@ class PresetManager(QtWidgets.QDialog): def presetExists(self, path, **kwargs): if os.path.exists(path): - window = self.window \ - if 'window' not in kwargs else kwargs['window'] + window = kwargs.get("window", self) ch = self.parent.showMessage( msg="%s already exists! Overwrite it?" % os.path.basename(path), @@ -200,7 +200,7 @@ class PresetManager(QtWidgets.QDialog): return False def openPreset(self, presetName, compPos=None): - componentList = self.parent.window.listWidget_componentList + componentList = self.parent.listWidget_componentList index = compPos if compPos is not None else componentList.currentRow() if index == -1: return @@ -228,7 +228,7 @@ class PresetManager(QtWidgets.QDialog): msg='Really delete %s?' % name, showCancel=True, icon='Warning', - parent=self.window + parent=self ) if not ch: return @@ -242,15 +242,15 @@ class PresetManager(QtWidgets.QDialog): self.parent.showMessage( msg='Preset names must contain only letters, ' 'numbers, and spaces.', - parent=window if window else self.window) + parent=window if window else self) def getPresetRow(self): - row = self.window.listWidget_presets.currentRow() + row = self.listWidget_presets.currentRow() if row > -1: return row # check if component selected in MainWindow has preset loaded - componentList = self.parent.window.listWidget_componentList + componentList = self.parent.listWidget_componentList compIndex = componentList.currentRow() if compIndex == -1: return compIndex @@ -273,14 +273,14 @@ class PresetManager(QtWidgets.QDialog): return index def openRenamePresetDialog(self): - presetList = self.window.listWidget_presets + presetList = self.listWidget_presets index = self.getPresetRow() if index == -1: return while True: newName, OK = QtWidgets.QInputDialog.getText( - self.window, + self, 'Preset Manager', 'Rename Preset:', QtWidgets.QLineEdit.Normal, @@ -319,7 +319,7 @@ class PresetManager(QtWidgets.QDialog): def openImportDialog(self): filename, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Import Preset File", + self, "Import Preset File", self.settings.value("presetDir"), "Preset Files (*.avl)") if filename: @@ -345,7 +345,7 @@ class PresetManager(QtWidgets.QDialog): if index == -1: return filename, _ = QtWidgets.QFileDialog.getSaveFileName( - self.window, "Export Preset", + self, "Export Preset", self.settings.value("presetDir"), "Preset Files (*.avl)") if filename: @@ -353,9 +353,9 @@ class PresetManager(QtWidgets.QDialog): if not self.core.exportPreset(filename, comp, vers, name): self.parent.showMessage( msg='Couldn\'t export %s.' % filename, - parent=self.window + parent=self ) self.settings.setValue("presetDir", os.path.dirname(filename)) def clearPresetListSelection(self): - self.window.listWidget_presets.setCurrentRow(-1) + self.listWidget_presets.setCurrentRow(-1) diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 426ff66..d910456 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -37,7 +37,7 @@ class PreviewWindow(QtWidgets.QLabel): if self.parent.encoding: return - i = self.parent.window.listWidget_componentList.currentRow() + i = self.parent.listWidget_componentList.currentRow() if i >= 0: component = self.parent.core.selectedComponents[i] if not hasattr(component, 'previewClickEvent'): diff --git a/src/main.py b/src/main.py index ec4b8bc..709e5e7 100644 --- a/src/main.py +++ b/src/main.py @@ -42,21 +42,9 @@ def main(): if mode == 'GUI': from .gui.mainwindow import MainWindow - window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) - desc = QtWidgets.QDesktopWidget() - dpi = desc.physicalDpiX() - log.info("Detected screen DPI: %s", dpi) - - window.resize( - int(window.width() * - (dpi / 96)), - int(window.height() * - (dpi / 96)) - ) - - main = MainWindow(window, proj) - log.debug("Finished creating main window") - window.raise_() + mainWindow = MainWindow(proj) + log.debug("Finished creating MainWindow") + mainWindow.raise_() sys.exit(app.exec_()) -- cgit v1.2.3