diff options
| author | tassaron | 2026-01-11 14:29:58 -0500 |
|---|---|---|
| committer | tassaron | 2026-01-11 14:29:58 -0500 |
| commit | 669756b391d26661cf2e2a97a304e73343ef6655 (patch) | |
| tree | 9cf2d4858c209bdab9f44d5c7f95c2a30b37f7a6 | |
| parent | 9d45f7f1a986aaa5d3c084c7ae747442b94a61b1 (diff) | |
update to Qt 6 and Pillow 12
and yeah, I accidentally ran black on the codebase. I don't want to spend more free time fixing that. All of these changes are simple renames or removals, nothing too major.
30 files changed, 2558 insertions, 2240 deletions
| @@ -1,89 +1,102 @@ | |||
| 1 | # Audio Visualizer Python | 1 | # Audio Visualizer Python |
| 2 | |||
| 2 | **We need a good name that is not as generic as "audio-visualizer-python"!** | 3 | **We need a good name that is not as generic as "audio-visualizer-python"!** |
| 3 | 4 | ||
| 4 | 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. | 5 | 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. |
| 5 | 6 | ||
| 6 | 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](https://github.com/djfun/audio-visualizer-python/issues) on this project. | 7 | 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](https://github.com/djfun/audio-visualizer-python/issues) on this project. |
| 7 | 8 | ||
| 8 | |||
| 9 | # Screenshots & Videos | 9 | # Screenshots & Videos |
| 10 | |||
| 10 | [<img title="AVP running on Windows" alt="Screenshot of program on Windows" src="screenshot.png" width="707">](/screenshot.png?raw=true) | 11 | [<img title="AVP running on Windows" alt="Screenshot of program on Windows" src="screenshot.png" width="707">](/screenshot.png?raw=true) |
| 11 | 12 | ||
| 12 | ## A video created by this app | 13 | ## A video created by this app |
| 13 | * **[YouTube: A day in spring](https://www.youtube.com/watch?v=-M3jR1NuJHM)** 🎥 | 14 | |
| 15 | - **[YouTube: A day in spring](https://www.youtube.com/watch?v=-M3jR1NuJHM)** 🎥 | ||
| 14 | 16 | ||
| 15 | ## Video demonstration of the app features | 17 | ## Video demonstration of the app features |
| 16 | * [YouTube: Audio Visualizer Python v2.0.0 demonstration](https://www.youtube.com/watch?v=EVt2ckQs1Yg) 🎥 | ||
| 17 | 18 | ||
| 19 | - [YouTube: Audio Visualizer Python v2.0.0 demonstration](https://www.youtube.com/watch?v=EVt2ckQs1Yg) 🎥 | ||
| 18 | 20 | ||
| 19 | # Installation | 21 | # Installation |
| 22 | |||
| 20 | ## Installation on Ubuntu 24.04 | 23 | ## Installation on Ubuntu 24.04 |
| 21 | * Install dependencies: `sudo apt install ffmpeg pipx python3-pyqt5` | ||
| 22 | * Download this repo and run `pipx install .` in this directory | ||
| 23 | * Run the program with `avp` from terminal | ||
| 24 | 24 | ||
| 25 | - Install system dependencies: `sudo apt install ffmpeg` | ||
| 26 | - Make a virtual environment: `python -m venv env` | ||
| 27 | - Activate it: `source env/bin/activate` | ||
| 28 | - Install Python dependencies: `pip install pyqt6 pillow numpy` | ||
| 29 | - Install this program: `pip install .` in this directory | ||
| 30 | - Run the program with `avp` from terminal | ||
| 25 | 31 | ||
| 26 | ## Installation on Windows | 32 | ## Installation on Windows |
| 27 | * Install Python from the Windows Store | 33 | |
| 28 | * Add Python to your system PATH (it should ask during the installation process) | 34 | - Install Python from the Windows Store |
| 29 | * Download this repo (extract from zip if needed) | 35 | - Add Python to your system PATH (it should ask during the installation process) |
| 30 | * Download and install [FFmpeg](https://www.ffmpeg.org/download.html). Use the GPL-licensed static builds. | 36 | - Download this repo (extract from zip if needed) |
| 31 | * Add FFmpeg to the system PATH as well (program will then work anywhere) | 37 | - Download and install [FFmpeg](https://www.ffmpeg.org/download.html). Use the GPL-licensed static builds. |
| 32 | * Alternatively, copy ffmpeg.exe into the folder that you want to run the program within | 38 | - Add FFmpeg to the system PATH as well (program will then work anywhere) |
| 33 | * Open command prompt, `cd` into the repo directory, and run: `pip install .` | 39 | - Alternatively, copy ffmpeg.exe into the folder that you want to run the program within |
| 34 | * Now run `avp` or `python -m avp` from a command prompt window to start the app | 40 | - Open command prompt, `cd` into the repo directory, and run: `pip install pyqt6 pillow numpy .` |
| 41 | - Now run `avp` or `python -m avp` from a command prompt window to start the app | ||
| 35 | 42 | ||
| 36 | ## Installation on macOS | 43 | ## Installation on macOS |
| 37 | * We need help writing instructions for macOS, but the program should work in theory. | 44 | |
| 45 | - We need help writing instructions for macOS, but the program should work in theory. | ||
| 38 | 46 | ||
| 39 | # [Keyboard Shortcuts](https://github.com/djfun/audio-visualizer-python/wiki/Keyboard-Shortcuts) | 47 | # [Keyboard Shortcuts](https://github.com/djfun/audio-visualizer-python/wiki/Keyboard-Shortcuts) |
| 40 | | Key Combo | Effect | | ||
| 41 | | ------------------------- | -------------------------------------------------- | | ||
| 42 | | Ctrl+S | Save Current Project | | ||
| 43 | | Ctrl+A | Save Project As... | | ||
| 44 | | Ctrl+O | Open Project | | ||
| 45 | | Ctrl+N | New Project (prompts to save current project) | | ||
| 46 | | Ctrl+Z | Undo | | ||
| 47 | | Ctrl+Shift+Z _or_ Ctrl+Y | Redo | | ||
| 48 | | Ctrl+T _or_ Insert | Add Component | | ||
| 49 | | Ctrl+R _or_ Delete | Remove Component | | ||
| 50 | | Ctrl+Space | Focus Component List | | ||
| 51 | | Ctrl+Shift+S | Save Component Preset | | ||
| 52 | | Ctrl+Shift+C | Remove Preset from Component | | ||
| 53 | | Ctrl+Up | Move Selected Component Up | | ||
| 54 | | Ctrl+Down | Move Selected Component Down | | ||
| 55 | | Ctrl+Home | Move Selected Component to Top | | ||
| 56 | | Ctrl+End | Move Selected Component to Bottom | | ||
| 57 | | Ctrl+Shift+U | Open Undo History | | ||
| 58 | | Ctrl+Shift+F | Show FFmpeg Command | | ||
| 59 | 48 | ||
| 49 | | Key Combo | Effect | | ||
| 50 | | ------------------------ | --------------------------------------------- | | ||
| 51 | | Ctrl+S | Save Current Project | | ||
| 52 | | Ctrl+A | Save Project As... | | ||
| 53 | | Ctrl+O | Open Project | | ||
| 54 | | Ctrl+N | New Project (prompts to save current project) | | ||
| 55 | | Ctrl+Z | Undo | | ||
| 56 | | Ctrl+Shift+Z _or_ Ctrl+Y | Redo | | ||
| 57 | | Ctrl+T _or_ Insert | Add Component | | ||
| 58 | | Ctrl+R _or_ Delete | Remove Component | | ||
| 59 | | Ctrl+Space | Focus Component List | | ||
| 60 | | Ctrl+Shift+S | Save Component Preset | | ||
| 61 | | Ctrl+Shift+C | Remove Preset from Component | | ||
| 62 | | Ctrl+Up | Move Selected Component Up | | ||
| 63 | | Ctrl+Down | Move Selected Component Down | | ||
| 64 | | Ctrl+Home | Move Selected Component to Top | | ||
| 65 | | Ctrl+End | Move Selected Component to Bottom | | ||
| 66 | | Ctrl+Shift+U | Open Undo History | | ||
| 67 | | Ctrl+Shift+F | Show FFmpeg Command | | ||
| 60 | 68 | ||
| 61 | # Commandline Mode | 69 | # Commandline Mode |
| 70 | |||
| 62 | Projects can be created with the GUI then loaded from the commandline for easy automation of video production. Some components have commandline options for extra customization, and you can save "presets" with settings to load if the commandline option doesn't exist. | 71 | Projects can be created with the GUI then loaded from the commandline for easy automation of video production. Some components have commandline options for extra customization, and you can save "presets" with settings to load if the commandline option doesn't exist. |
| 63 | 72 | ||
| 64 | ## Example/test command | 73 | ## Example/test command |
| 65 | * Create a video with a grey "classic visualizer", background image, and text: | ||
| 66 | * `avp -c 0 image path=src/tests/data/test.jpg -c 1 classic color=180,180,180 -c 2 text "title=Episode 371" -i src/tests/data/test.ogg -o output.mp4` | ||
| 67 | * [See more about commandline mode in the wiki!](https://github.com/djfun/audio-visualizer-python/wiki/Commandline-Mode) | ||
| 68 | 74 | ||
| 75 | - Create a video with a grey "classic visualizer", background image, and text: | ||
| 76 | - `avp -c 0 image path=src/tests/data/test.jpg -c 1 classic color=180,180,180 -c 2 text "title=Episode 371" -i src/tests/data/test.ogg -o output.mp4` | ||
| 77 | - [See more about commandline mode in the wiki!](https://github.com/djfun/audio-visualizer-python/wiki/Commandline-Mode) | ||
| 69 | 78 | ||
| 70 | # Developer Information | 79 | # Developer Information |
| 80 | |||
| 71 | ## Known Working Versions of Dependencies | 81 | ## Known Working Versions of Dependencies |
| 72 | * Python 3.10 | 82 | |
| 73 | * FFmpeg 4.4.1 | 83 | - Python 3.13 |
| 74 | * PyQt5 (Qt v5.15.3) | 84 | - FFmpeg 8.0.1 |
| 75 | * Pillow 9.1.0 | 85 | - PyQt6 v6.10.2 (Qt v6.10.1) |
| 76 | * NumPy 1.22.3 | 86 | - Pillow 12.1.0 |
| 87 | - NumPy 2.4.1 | ||
| 77 | 88 | ||
| 78 | ## Getting Faster Export Times | 89 | ## Getting Faster Export Times |
| 79 | * [Pillow-SIMD](https://github.com/uploadcare/pillow-simd) may be used as a drop-in replacement for Pillow if you desire faster video export times, but it must be compiled from source. For help installing dependencies to compile Pillow-SIMD, see the [Pillow installation guide](https://pillow.readthedocs.io/en/stable/installation.html). Then add the `-SIMD` suffix into `setup.py` and install as usual. | 90 | |
| 91 | - [Pillow-SIMD](https://github.com/uploadcare/pillow-simd) may be used as a drop-in replacement for Pillow if you desire faster video export times, but it must be compiled from source. For help installing dependencies to compile Pillow-SIMD, see the [Pillow installation guide](https://pillow.readthedocs.io/en/stable/installation.html). | ||
| 80 | 92 | ||
| 81 | ## Developing a New Component | 93 | ## Developing a New Component |
| 82 | * Information for developing a component is in our wiki: [How a Component Works](https://github.com/djfun/audio-visualizer-python/wiki/How-a-Component-Works) | ||
| 83 | * File an issue on GitHub if you need help fitting your visualizer into our component system; we would be happy to collaborate | ||
| 84 | 94 | ||
| 95 | - Information for developing a component is in our wiki: [How a Component Works](https://github.com/djfun/audio-visualizer-python/wiki/How-a-Component-Works) | ||
| 96 | - File an issue on GitHub if you need help fitting your visualizer into our component system; we would be happy to collaborate | ||
| 85 | 97 | ||
| 86 | # License | 98 | # License |
| 99 | |||
| 87 | Source code of audio-visualizer-python is licensed under the MIT license. | 100 | Source code of audio-visualizer-python is licensed under the MIT license. |
| 88 | 101 | ||
| 89 | Some dependencies of this application are under the GPL license. When packaged with these dependencies, audio-visualizer-python may also be under the terms of this GPL license. | 102 | Some dependencies of this application are under the GPL license. When packaged with these dependencies, audio-visualizer-python may also be under the terms of this GPL license. |
| @@ -15,49 +15,53 @@ def getTextFromFile(filename, fallback): | |||
| 15 | return output | 15 | return output |
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | PACKAGE_NAME = 'avp' | 18 | PACKAGE_NAME = "avp" |
| 19 | SOURCE_DIRECTORY = 'src' | 19 | SOURCE_DIRECTORY = "src" |
| 20 | SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}') | 20 | SOURCE_PACKAGE_REGEX = re.compile(rf"^{SOURCE_DIRECTORY}") |
| 21 | PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline' | 21 | PACKAGE_DESCRIPTION = "Create audio visualization videos from a GUI or commandline" |
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | avp = import_module(SOURCE_DIRECTORY) | 24 | avp = import_module(SOURCE_DIRECTORY) |
| 25 | source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*']) | 25 | source_packages = find_packages(include=[SOURCE_DIRECTORY, f"{SOURCE_DIRECTORY}.*"]) |
| 26 | proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages] | 26 | proj_packages = [ |
| 27 | SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages | ||
| 28 | ] | ||
| 27 | 29 | ||
| 28 | 30 | ||
| 29 | setup( | 31 | setup( |
| 30 | name='audio_visualizer_python', | 32 | name="audio_visualizer_python", |
| 31 | version=avp.__version__, | 33 | version=avp.__version__, |
| 32 | url='https://github.com/djfun/audio-visualizer-python', | 34 | url="https://github.com/djfun/audio-visualizer-python", |
| 33 | license='MIT', | 35 | license="MIT", |
| 34 | description=PACKAGE_DESCRIPTION, | 36 | description=PACKAGE_DESCRIPTION, |
| 35 | author=getTextFromFile('AUTHORS', 'djfun, tassaron'), | 37 | author=getTextFromFile("AUTHORS", "djfun, tassaron"), |
| 36 | long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION), | 38 | long_description=getTextFromFile("README.md", PACKAGE_DESCRIPTION), |
| 37 | classifiers=[ | 39 | classifiers=[ |
| 38 | 'Development Status :: 4 - Beta', | 40 | "Development Status :: 4 - Beta", |
| 39 | 'License :: OSI Approved :: MIT License', | 41 | "License :: OSI Approved :: MIT License", |
| 40 | 'Programming Language :: Python :: 3 :: Only', | 42 | "Programming Language :: Python :: 3 :: Only", |
| 41 | 'Intended Audience :: End Users/Desktop', | 43 | "Intended Audience :: End Users/Desktop", |
| 42 | 'Topic :: Multimedia :: Video :: Non-Linear Editor', | 44 | "Topic :: Multimedia :: Video :: Non-Linear Editor", |
| 43 | ], | 45 | ], |
| 44 | keywords=[ | 46 | keywords=[ |
| 45 | 'visualizer', 'visualization', 'commandline video', | 47 | "visualizer", |
| 46 | 'video editor', 'ffmpeg', 'podcast' | 48 | "visualization", |
| 49 | "commandline video", | ||
| 50 | "video editor", | ||
| 51 | "ffmpeg", | ||
| 52 | "podcast", | ||
| 47 | ], | 53 | ], |
| 48 | packages=proj_packages, | 54 | packages=proj_packages, |
| 49 | package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, | 55 | package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, |
| 50 | include_package_data=True, | 56 | include_package_data=True, |
| 51 | install_requires=[ | 57 | install_requires=[ |
| 52 | 'Pillow==9.1.1', | 58 | "Pillow", |
| 53 | 'PyQt5', | 59 | "PyQt6", |
| 54 | 'numpy', | 60 | "numpy", |
| 55 | 'pytest', | 61 | "pytest", |
| 56 | 'pytest-qt', | 62 | "pytest-qt", |
| 57 | ], | 63 | ], |
| 58 | entry_points={ | 64 | entry_points={ |
| 59 | 'console_scripts': [ | 65 | "console_scripts": [f"avp = {PACKAGE_NAME}.__main__:main"], |
| 60 | f'avp = {PACKAGE_NAME}.__main__:main' | 66 | }, |
| 61 | ], | ||
| 62 | } | ||
| 63 | ) | 67 | ) |
diff --git a/src/__init__.py b/src/__init__.py index 2080de5..ee9bebb 100644 --- a/src/__init__.py +++ b/src/__init__.py | |||
| @@ -3,20 +3,21 @@ import os | |||
| 3 | import logging | 3 | import logging |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | __version__ = '2.0.0' | 6 | __version__ = "2.1.0" |
| 7 | 7 | ||
| 8 | 8 | ||
| 9 | class Logger(logging.getLoggerClass()): | 9 | class Logger(logging.getLoggerClass()): |
| 10 | ''' | 10 | """ |
| 11 | Custom Logger class to handle custom VERBOSE log level. | 11 | Custom Logger class to handle custom VERBOSE log level. |
| 12 | Levels used in this program are as follows: | 12 | Levels used in this program are as follows: |
| 13 | VERBOSE Annoyingly frequent debug messages (e.g, in loops) | 13 | VERBOSE Annoyingly frequent debug messages (e.g, in loops) |
| 14 | DEBUG Ordinary debug information | 14 | DEBUG Ordinary debug information |
| 15 | INFO Expected events that are expensive or irreversible | 15 | INFO Expected events that are expensive or irreversible |
| 16 | WARNING A non-fatal error or suspicious behaviour | 16 | WARNING A non-fatal error or suspicious behaviour |
| 17 | ERROR Any error that would interrupt the user | 17 | ERROR Any error that would interrupt the user |
| 18 | CRITICAL Things that really shouldn't happen at all | 18 | CRITICAL Things that really shouldn't happen at all |
| 19 | ''' | 19 | """ |
| 20 | |||
| 20 | def __init__(self, name, level=logging.NOTSET): | 21 | def __init__(self, name, level=logging.NOTSET): |
| 21 | super().__init__(name, level) | 22 | super().__init__(name, level) |
| 22 | logging.addLevelName(5, "VERBOSE") | 23 | logging.addLevelName(5, "VERBOSE") |
| @@ -24,11 +25,13 @@ class Logger(logging.getLoggerClass()): | |||
| 24 | def verbose(self, msg, *args, **kwargs): | 25 | def verbose(self, msg, *args, **kwargs): |
| 25 | if self.isEnabledFor(5): | 26 | if self.isEnabledFor(5): |
| 26 | self._log(5, msg, args, **kwargs) | 27 | self._log(5, msg, args, **kwargs) |
| 28 | |||
| 29 | |||
| 27 | logging.setLoggerClass(Logger) | 30 | logging.setLoggerClass(Logger) |
| 28 | logging.VERBOSE = 5 | 31 | logging.VERBOSE = 5 |
| 29 | 32 | ||
| 30 | 33 | ||
| 31 | if getattr(sys, 'frozen', False): | 34 | if getattr(sys, "frozen", False): |
| 32 | # frozen | 35 | # frozen |
| 33 | wd = os.path.dirname(sys.executable) | 36 | wd = os.path.dirname(sys.executable) |
| 34 | else: | 37 | else: |
diff --git a/src/__main__.py b/src/__main__.py index 284fd2c..db48788 100644 --- a/src/__main__.py +++ b/src/__main__.py | |||
| @@ -1,30 +1,30 @@ | |||
| 1 | from PyQt5.QtWidgets import QApplication | 1 | from PyQt6.QtWidgets import QApplication |
| 2 | import sys | 2 | import sys |
| 3 | import logging | 3 | import logging |
| 4 | import re | 4 | import re |
| 5 | import string | 5 | import string |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | log = logging.getLogger('AVP.Main') | 8 | log = logging.getLogger("AVP.Main") |
| 9 | 9 | ||
| 10 | 10 | ||
| 11 | def main() -> int: | 11 | def main() -> int: |
| 12 | """Returns an exit code (0 for success)""" | 12 | """Returns an exit code (0 for success)""" |
| 13 | proj = None | 13 | proj = None |
| 14 | mode = 'GUI' | 14 | mode = "GUI" |
| 15 | 15 | ||
| 16 | # Determine whether we're in GUI or commandline mode | 16 | # Determine whether we're in GUI or commandline mode |
| 17 | if len(sys.argv) > 2: | 17 | if len(sys.argv) > 2: |
| 18 | mode = 'commandline' | 18 | mode = "commandline" |
| 19 | elif len(sys.argv) == 2: | 19 | elif len(sys.argv) == 2: |
| 20 | if sys.argv[1].startswith('-'): | 20 | if sys.argv[1].startswith("-"): |
| 21 | mode = 'commandline' | 21 | mode = "commandline" |
| 22 | else: | 22 | else: |
| 23 | # remove unsafe punctuation characters such as \/?*&^%$# | 23 | # remove unsafe punctuation characters such as \/?*&^%$# |
| 24 | if sys.argv[1].endswith('.avp'): | 24 | if sys.argv[1].endswith(".avp"): |
| 25 | # remove file extension | 25 | # remove file extension |
| 26 | sys.argv[1] = sys.argv[1][:-4] | 26 | sys.argv[1] = sys.argv[1][:-4] |
| 27 | sys.argv[1] = re.sub(f'[{re.escape(string.punctuation)}]', '', sys.argv[1]) | 27 | sys.argv[1] = re.sub(f"[{re.escape(string.punctuation)}]", "", sys.argv[1]) |
| 28 | # opening a project file with gui | 28 | # opening a project file with gui |
| 29 | proj = sys.argv[1] | 29 | proj = sys.argv[1] |
| 30 | 30 | ||
| @@ -32,8 +32,16 @@ def main() -> int: | |||
| 32 | app = QApplication(sys.argv) | 32 | app = QApplication(sys.argv) |
| 33 | app.setApplicationName("audio-visualizer") | 33 | app.setApplicationName("audio-visualizer") |
| 34 | 34 | ||
| 35 | screen = app.primaryScreen() | ||
| 36 | if screen is None: | ||
| 37 | dpi = None | ||
| 38 | log.error("Could not detect DPI") | ||
| 39 | else: | ||
| 40 | dpi = screen.physicalDotsPerInchX() | ||
| 41 | log.info("Detected screen DPI: %s", dpi) | ||
| 42 | |||
| 35 | # Launch program | 43 | # Launch program |
| 36 | if mode == 'commandline': | 44 | if mode == "commandline": |
| 37 | from .command import Command | 45 | from .command import Command |
| 38 | 46 | ||
| 39 | main = Command() | 47 | main = Command() |
| @@ -42,14 +50,15 @@ def main() -> int: | |||
| 42 | 50 | ||
| 43 | # Both branches here may occur in one execution: | 51 | # Both branches here may occur in one execution: |
| 44 | # Commandline parsing could change mode back to GUI | 52 | # Commandline parsing could change mode back to GUI |
| 45 | if mode == 'GUI': | 53 | if mode == "GUI": |
| 46 | from .gui.mainwindow import MainWindow | 54 | from .gui.mainwindow import MainWindow |
| 47 | 55 | ||
| 48 | mainWindow = MainWindow(proj) | 56 | mainWindow = MainWindow(proj, dpi) |
| 49 | log.debug("Finished creating MainWindow") | 57 | log.debug("Finished creating MainWindow") |
| 50 | mainWindow.raise_() | 58 | mainWindow.raise_() |
| 51 | 59 | ||
| 52 | return app.exec_() | 60 | return app.exec() |
| 61 | |||
| 53 | 62 | ||
| 54 | if __name__ == '__main__': | 63 | if __name__ == "__main__": |
| 55 | sys.exit(main()) \ No newline at end of file | 64 | sys.exit(main()) |
diff --git a/src/command.py b/src/command.py index 53801fa..783ac26 100644 --- a/src/command.py +++ b/src/command.py | |||
| @@ -1,9 +1,10 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | When using commandline mode, this module's object handles interpreting | 2 | When using commandline mode, this module's object handles interpreting |
| 3 | the arguments and giving them to Core, which tracks the main program state. | 3 | the arguments and giving them to Core, which tracks the main program state. |
| 4 | Then it immediately exports a video. | 4 | Then it immediately exports a video. |
| 5 | ''' | 5 | """ |
| 6 | from PyQt5 import QtCore | 6 | |
| 7 | from PyQt6 import QtCore | ||
| 7 | import argparse | 8 | import argparse |
| 8 | import os | 9 | import os |
| 9 | import sys | 10 | import sys |
| @@ -16,12 +17,12 @@ import logging | |||
| 16 | from . import core | 17 | from . import core |
| 17 | 18 | ||
| 18 | 19 | ||
| 19 | log = logging.getLogger('AVP.Commandline') | 20 | log = logging.getLogger("AVP.Commandline") |
| 20 | 21 | ||
| 21 | 22 | ||
| 22 | class Command(QtCore.QObject): | 23 | class Command(QtCore.QObject): |
| 23 | """ | 24 | """ |
| 24 | This replaces the GUI MainWindow when in commandline mode. | 25 | This replaces the GUI MainWindow when in commandline mode. |
| 25 | """ | 26 | """ |
| 26 | 27 | ||
| 27 | createVideo = QtCore.pyqtSignal() | 28 | createVideo = QtCore.pyqtSignal() |
| @@ -29,7 +30,7 @@ class Command(QtCore.QObject): | |||
| 29 | def __init__(self): | 30 | def __init__(self): |
| 30 | super().__init__() | 31 | super().__init__() |
| 31 | self.core = core.Core() | 32 | self.core = core.Core() |
| 32 | core.Core.mode = 'commandline' | 33 | core.Core.mode = "commandline" |
| 33 | self.dataDir = self.core.dataDir | 34 | self.dataDir = self.core.dataDir |
| 34 | self.canceled = False | 35 | self.canceled = False |
| 35 | self.settings = core.Core.settings | 36 | self.settings = core.Core.settings |
| @@ -39,52 +40,58 @@ class Command(QtCore.QObject): | |||
| 39 | 40 | ||
| 40 | def parseArgs(self): | 41 | def parseArgs(self): |
| 41 | parser = argparse.ArgumentParser( | 42 | parser = argparse.ArgumentParser( |
| 42 | prog='avp' if os.path.basename(sys.argv[0]) == "__main__.py" else None, | 43 | prog="avp" if os.path.basename(sys.argv[0]) == "__main__.py" else None, |
| 43 | description='Create a visualization for an audio file', | 44 | description="Create a visualization for an audio file", |
| 44 | epilog='EXAMPLE COMMAND: avp myvideotemplate ' | 45 | epilog="EXAMPLE COMMAND: avp myvideotemplate " |
| 45 | '-i ~/Music/song.mp3 -o ~/video.mp4 ' | 46 | "-i ~/Music/song.mp3 -o ~/video.mp4 " |
| 46 | '-c 0 image path=~/Pictures/thisWeeksPicture.jpg ' | 47 | "-c 0 image path=~/Pictures/thisWeeksPicture.jpg " |
| 47 | '-c 1 video "preset=My Logo" -c 2 vis layout=classic' | 48 | '-c 1 video "preset=My Logo" -c 2 vis layout=classic', |
| 48 | ) | 49 | ) |
| 49 | 50 | ||
| 50 | |||
| 51 | # input/output automatic-export commands | 51 | # input/output automatic-export commands |
| 52 | parser.add_argument("-i", "--input", metavar="SOUND", help="input audio file") | ||
| 52 | parser.add_argument( | 53 | parser.add_argument( |
| 53 | '-i', '--input', metavar='SOUND', | 54 | "-o", "--output", metavar="OUTPUT", help="output video file" |
| 54 | help='input audio file' | ||
| 55 | ) | 55 | ) |
| 56 | parser.add_argument( | 56 | parser.add_argument( |
| 57 | '-o', '--output', metavar='OUTPUT', | 57 | "--export-project", |
| 58 | help='output video file' | 58 | action="store_true", |
| 59 | ) | 59 | help="use input and output files from project file if -i or -o is missing", |
| 60 | parser.add_argument( | ||
| 61 | '--export-project', action='store_true', | ||
| 62 | help='use input and output files from project file if -i or -o is missing' | ||
| 63 | ) | 60 | ) |
| 64 | 61 | ||
| 65 | # mutually exclusive debug options | 62 | # mutually exclusive debug options |
| 66 | debugCommands = parser.add_mutually_exclusive_group() | 63 | debugCommands = parser.add_mutually_exclusive_group() |
| 67 | debugCommands.add_argument( | 64 | debugCommands.add_argument( |
| 68 | '--test', action='store_true', | 65 | "--test", |
| 69 | help='run tests and create a report full of debugging info' | 66 | action="store_true", |
| 67 | help="run tests and create a report full of debugging info", | ||
| 70 | ) | 68 | ) |
| 71 | debugCommands.add_argument( | 69 | debugCommands.add_argument( |
| 72 | '--debug', action='store_true', | 70 | "--debug", |
| 73 | help='create bigger logfiles while program is running' | 71 | action="store_true", |
| 72 | help="create bigger logfiles while program is running", | ||
| 74 | ) | 73 | ) |
| 75 | 74 | ||
| 76 | # project/GUI options | 75 | # project/GUI options |
| 77 | parser.add_argument( | 76 | parser.add_argument( |
| 78 | 'projpath', metavar='path-to-project', | 77 | "projpath", |
| 79 | help='open a project file (.avp)', nargs='?') | 78 | metavar="path-to-project", |
| 79 | help="open a project file (.avp)", | ||
| 80 | nargs="?", | ||
| 81 | ) | ||
| 80 | parser.add_argument( | 82 | parser.add_argument( |
| 81 | '-c', '--comp', metavar=('LAYER', 'ARG'), | 83 | "-c", |
| 82 | help='first arg must be component NAME to insert at LAYER.' | 84 | "--comp", |
| 85 | metavar=("LAYER", "ARG"), | ||
| 86 | help="first arg must be component NAME to insert at LAYER." | ||
| 83 | '"help" for information about possible args for a component.', | 87 | '"help" for information about possible args for a component.', |
| 84 | nargs='*', action='append') | 88 | nargs="*", |
| 89 | action="append", | ||
| 90 | ) | ||
| 85 | parser.add_argument( | 91 | parser.add_argument( |
| 86 | '--no-preview', action='store_true', | 92 | "--no-preview", |
| 87 | help='disable live preview during export' | 93 | action="store_true", |
| 94 | help="disable live preview during export", | ||
| 88 | ) | 95 | ) |
| 89 | 96 | ||
| 90 | args = parser.parse_args() | 97 | args = parser.parse_args() |
| @@ -101,15 +108,11 @@ class Command(QtCore.QObject): | |||
| 101 | if args.projpath: | 108 | if args.projpath: |
| 102 | projPath = args.projpath | 109 | projPath = args.projpath |
| 103 | if not os.path.dirname(projPath): | 110 | if not os.path.dirname(projPath): |
| 104 | projPath = os.path.join( | 111 | projPath = os.path.join(self.settings.value("projectDir"), projPath) |
| 105 | self.settings.value("projectDir"), | 112 | if not projPath.endswith(".avp"): |
| 106 | projPath | 113 | projPath += ".avp" |
| 107 | ) | ||
| 108 | if not projPath.endswith('.avp'): | ||
| 109 | projPath += '.avp' | ||
| 110 | self.core.openProject(self, projPath) | 114 | self.core.openProject(self, projPath) |
| 111 | self.core.selectedComponents = list( | 115 | self.core.selectedComponents = list(reversed(self.core.selectedComponents)) |
| 112 | reversed(self.core.selectedComponents)) | ||
| 113 | self.core.componentListChanged() | 116 | self.core.componentListChanged() |
| 114 | 117 | ||
| 115 | if args.comp: | 118 | if args.comp: |
| @@ -120,14 +123,17 @@ class Command(QtCore.QObject): | |||
| 120 | try: | 123 | try: |
| 121 | pos = int(pos) | 124 | pos = int(pos) |
| 122 | except ValueError: | 125 | except ValueError: |
| 123 | print(pos, 'is not a layer number.') | 126 | print(pos, "is not a layer number.") |
| 124 | quit(1) | 127 | quit(1) |
| 125 | realName = self.parseCompName(name) | 128 | realName = self.parseCompName(name) |
| 126 | if not realName: | 129 | if not realName: |
| 127 | print(name, 'is not a valid component name.') | 130 | print(name, "is not a valid component name.") |
| 128 | quit(1) | 131 | quit(1) |
| 129 | modI = self.core.moduleIndexFor(realName) | 132 | modI = self.core.moduleIndexFor(realName) |
| 130 | i = self.core.insertComponent(pos, modI, self) | 133 | i = self.core.insertComponent(pos, modI, self) |
| 134 | if i is None: | ||
| 135 | print(name, "could not be initialized.") | ||
| 136 | quit(1) | ||
| 131 | for arg in compargs: | 137 | for arg in compargs: |
| 132 | self.core.selectedComponents[i].command(arg) | 138 | self.core.selectedComponents[i].command(arg) |
| 133 | 139 | ||
| @@ -135,15 +141,12 @@ class Command(QtCore.QObject): | |||
| 135 | errcode, data = self.core.parseAvFile(projPath) | 141 | errcode, data = self.core.parseAvFile(projPath) |
| 136 | input_ = None | 142 | input_ = None |
| 137 | output = None | 143 | output = None |
| 138 | for key, value in data['WindowFields']: | 144 | for key, value in data["WindowFields"]: |
| 139 | if 'outputFile' in key: | 145 | if "outputFile" in key: |
| 140 | output = value | 146 | output = value |
| 141 | if output and not os.path.dirname(value): | 147 | if output and not os.path.dirname(value): |
| 142 | output = os.path.join( | 148 | output = os.path.join(os.path.expanduser("~"), output) |
| 143 | os.path.expanduser('~'), | 149 | if "audioFile" in key: |
| 144 | output | ||
| 145 | ) | ||
| 146 | if 'audioFile' in key: | ||
| 147 | input_ = value | 150 | input_ = value |
| 148 | 151 | ||
| 149 | # use input/output from project file, overwritten by -i and -o | 152 | # use input/output from project file, overwritten by -i and -o |
| @@ -153,7 +156,7 @@ class Command(QtCore.QObject): | |||
| 153 | 156 | ||
| 154 | self.createAudioVisualization( | 157 | self.createAudioVisualization( |
| 155 | input_ if not args.input else args.input, | 158 | input_ if not args.input else args.input, |
| 156 | output if not args.output else args.output | 159 | output if not args.output else args.output, |
| 157 | ) | 160 | ) |
| 158 | return "commandline" | 161 | return "commandline" |
| 159 | 162 | ||
| @@ -165,11 +168,11 @@ class Command(QtCore.QObject): | |||
| 165 | core.Core.previewEnabled = False | 168 | core.Core.previewEnabled = False |
| 166 | 169 | ||
| 167 | elif ( | 170 | elif ( |
| 168 | args.projpath is None and | 171 | args.projpath is None |
| 169 | 'help' not in sys.argv and | 172 | and "help" not in sys.argv |
| 170 | '--debug' not in sys.argv and | 173 | and "--debug" not in sys.argv |
| 171 | '--test' not in sys.argv | 174 | and "--test" not in sys.argv |
| 172 | ): | 175 | ): |
| 173 | parser.print_help() | 176 | parser.print_help() |
| 174 | quit(1) | 177 | quit(1) |
| 175 | 178 | ||
| @@ -180,12 +183,9 @@ class Command(QtCore.QObject): | |||
| 180 | print("No components selected. Adding a default visualizer.") | 183 | print("No components selected. Adding a default visualizer.") |
| 181 | time.sleep(1) | 184 | time.sleep(1) |
| 182 | self.core.insertComponent(0, 0, self) | 185 | self.core.insertComponent(0, 0, self) |
| 183 | self.core.selectedComponents = list( | 186 | self.core.selectedComponents = list(reversed(self.core.selectedComponents)) |
| 184 | reversed(self.core.selectedComponents)) | ||
| 185 | self.core.componentListChanged() | 187 | self.core.componentListChanged() |
| 186 | self.worker = self.core.newVideoWorker( | 188 | self.worker = self.core.newVideoWorker(self, input, output) |
| 187 | self, input, output | ||
| 188 | ) | ||
| 189 | # quit(0) after video is created | 189 | # quit(0) after video is created |
| 190 | self.worker.videoCreated.connect(self.videoCreated) | 190 | self.worker.videoCreated.connect(self.videoCreated) |
| 191 | self.lastProgressUpdate = time.time() | 191 | self.lastProgressUpdate = time.time() |
| @@ -199,16 +199,18 @@ class Command(QtCore.QObject): | |||
| 199 | 199 | ||
| 200 | @QtCore.pyqtSlot(str) | 200 | @QtCore.pyqtSlot(str) |
| 201 | def progressBarSetText(self, value): | 201 | def progressBarSetText(self, value): |
| 202 | if 'Export ' in value: | 202 | if "Export " in value: |
| 203 | # Don't duplicate completion/failure messages | 203 | # Don't duplicate completion/failure messages |
| 204 | return | 204 | return |
| 205 | if not value.startswith('Exporting') \ | 205 | if ( |
| 206 | and time.time() - self.lastProgressUpdate >= 0.05: | 206 | not value.startswith("Exporting") |
| 207 | and time.time() - self.lastProgressUpdate >= 0.05 | ||
| 208 | ): | ||
| 207 | # Show most messages very often | 209 | # Show most messages very often |
| 208 | print(value) | 210 | print(value) |
| 209 | elif time.time() - self.lastProgressUpdate >= 2.0: | 211 | elif time.time() - self.lastProgressUpdate >= 2.0: |
| 210 | # Give user time to read ffmpeg's output during the export | 212 | # Give user time to read ffmpeg's output during the export |
| 211 | print('##### %s' % value) | 213 | print("##### %s" % value) |
| 212 | else: | 214 | else: |
| 213 | return | 215 | return |
| 214 | self.lastProgressUpdate = time.time() | 216 | self.lastProgressUpdate = time.time() |
| @@ -221,9 +223,9 @@ class Command(QtCore.QObject): | |||
| 221 | quit(code) | 223 | quit(code) |
| 222 | 224 | ||
| 223 | def showMessage(self, **kwargs): | 225 | def showMessage(self, **kwargs): |
| 224 | print(kwargs['msg']) | 226 | print(kwargs["msg"]) |
| 225 | if 'detail' in kwargs: | 227 | if "detail" in kwargs: |
| 226 | print(kwargs['detail']) | 228 | print(kwargs["detail"]) |
| 227 | 229 | ||
| 228 | @QtCore.pyqtSlot(str, str) | 230 | @QtCore.pyqtSlot(str, str) |
| 229 | def videoThreadError(self, msg, detail): | 231 | def videoThreadError(self, msg, detail): |
| @@ -235,7 +237,7 @@ class Command(QtCore.QObject): | |||
| 235 | pass | 237 | pass |
| 236 | 238 | ||
| 237 | def parseCompName(self, name): | 239 | def parseCompName(self, name): |
| 238 | '''Deduces a proper component name out of a commandline arg''' | 240 | """Deduces a proper component name out of a commandline arg""" |
| 239 | 241 | ||
| 240 | if name.title() in self.core.compNames: | 242 | if name.title() in self.core.compNames: |
| 241 | return name.title() | 243 | return name.title() |
| @@ -244,9 +246,7 @@ class Command(QtCore.QObject): | |||
| 244 | return compName | 246 | return compName |
| 245 | 247 | ||
| 246 | compFileNames = [ | 248 | compFileNames = [ |
| 247 | os.path.splitext( | 249 | os.path.splitext(os.path.basename(mod.__file__))[0] |
| 248 | os.path.basename(mod.__file__) | ||
| 249 | )[0] | ||
| 250 | for mod in self.core.modules | 250 | for mod in self.core.modules |
| 251 | ] | 251 | ] |
| 252 | for i, compFileName in enumerate(compFileNames): | 252 | for i, compFileName in enumerate(compFileNames): |
| @@ -258,15 +258,17 @@ class Command(QtCore.QObject): | |||
| 258 | 258 | ||
| 259 | def runTests(self): | 259 | def runTests(self): |
| 260 | from . import tests | 260 | from . import tests |
| 261 | |||
| 261 | test_report = os.path.join(core.Core.logDir, "test_report.log") | 262 | test_report = os.path.join(core.Core.logDir, "test_report.log") |
| 262 | tests.run(test_report) | 263 | tests.run(test_report) |
| 263 | 264 | ||
| 264 | # Choose a numbered location to put the output file | 265 | # Choose a numbered location to put the output file |
| 265 | logNumber = 0 | 266 | logNumber = 0 |
| 267 | |||
| 266 | def getFilename(): | 268 | def getFilename(): |
| 267 | """Get a numbered filename for the final test report""" | 269 | """Get a numbered filename for the final test report""" |
| 268 | nonlocal logNumber | 270 | nonlocal logNumber |
| 269 | name = os.path.join(os.path.expanduser('~'), "avp_test_report") | 271 | name = os.path.join(os.path.expanduser("~"), "avp_test_report") |
| 270 | while True: | 272 | while True: |
| 271 | possibleName = f"{name}{logNumber:0>2}.txt" | 273 | possibleName = f"{name}{logNumber:0>2}.txt" |
| 272 | if os.path.exists(possibleName) and logNumber < 100: | 274 | if os.path.exists(possibleName) and logNumber < 100: |
diff --git a/src/component.py b/src/component.py index 1e10f66..01d4e44 100644 --- a/src/component.py +++ b/src/component.py | |||
| @@ -1,9 +1,10 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Base classes for components to import. Read comments for some documentation | 2 | Base classes for components to import. Read comments for some documentation |
| 3 | on making a valid component. | 3 | on making a valid component. |
| 4 | ''' | 4 | """ |
| 5 | from PyQt5 import uic, QtCore, QtWidgets | 5 | |
| 6 | from PyQt5.QtGui import QColor | 6 | from PyQt6 import uic, QtCore, QtWidgets |
| 7 | from PyQt6.QtGui import QColor, QUndoCommand | ||
| 7 | import os | 8 | import os |
| 8 | import sys | 9 | import sys |
| 9 | import math | 10 | import math |
| @@ -13,18 +14,22 @@ from copy import copy | |||
| 13 | 14 | ||
| 14 | from .toolkit.frame import BlankFrame | 15 | from .toolkit.frame import BlankFrame |
| 15 | from .toolkit import ( | 16 | from .toolkit import ( |
| 16 | getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals | 17 | getWidgetValue, |
| 18 | setWidgetValue, | ||
| 19 | connectWidget, | ||
| 20 | rgbFromString, | ||
| 21 | blockSignals, | ||
| 17 | ) | 22 | ) |
| 18 | 23 | ||
| 19 | 24 | ||
| 20 | log = logging.getLogger('AVP.ComponentHandler') | 25 | log = logging.getLogger("AVP.ComponentHandler") |
| 21 | 26 | ||
| 22 | 27 | ||
| 23 | class ComponentMetaclass(type(QtCore.QObject)): | 28 | class ComponentMetaclass(type(QtCore.QObject)): |
| 24 | ''' | 29 | """ |
| 25 | Checks the validity of each Component class and mutates some attrs. | 30 | Checks the validity of each Component class and mutates some attrs. |
| 26 | E.g., takes only major version from version string & decorates methods | 31 | E.g., takes only major version from version string & decorates methods |
| 27 | ''' | 32 | """ |
| 28 | 33 | ||
| 29 | def initializationWrapper(func): | 34 | def initializationWrapper(func): |
| 30 | def initializationWrapper(self, *args, **kwargs): | 35 | def initializationWrapper(self, *args, **kwargs): |
| @@ -32,51 +37,55 @@ class ComponentMetaclass(type(QtCore.QObject)): | |||
| 32 | return func(self, *args, **kwargs) | 37 | return func(self, *args, **kwargs) |
| 33 | except Exception: | 38 | except Exception: |
| 34 | try: | 39 | try: |
| 35 | raise ComponentError(self, 'initialization process') | 40 | raise ComponentError(self, "initialization process") |
| 36 | except ComponentError: | 41 | except ComponentError: |
| 37 | return | 42 | return |
| 43 | |||
| 38 | return initializationWrapper | 44 | return initializationWrapper |
| 39 | 45 | ||
| 40 | def renderWrapper(func): | 46 | def renderWrapper(func): |
| 41 | def renderWrapper(self, *args, **kwargs): | 47 | def renderWrapper(self, *args, **kwargs): |
| 42 | try: | 48 | try: |
| 43 | log.verbose( | 49 | log.verbose( |
| 44 | '### %s #%s renders a preview frame ###', | 50 | "### %s #%s renders a preview frame ###", |
| 45 | self.__class__.name, str(self.compPos), | 51 | self.__class__.name, |
| 52 | str(self.compPos), | ||
| 46 | ) | 53 | ) |
| 47 | return func(self, *args, **kwargs) | 54 | return func(self, *args, **kwargs) |
| 48 | except Exception as e: | 55 | except Exception as e: |
| 49 | try: | 56 | try: |
| 50 | if e.__class__.__name__.startswith('Component'): | 57 | if e.__class__.__name__.startswith("Component"): |
| 51 | raise | 58 | raise |
| 52 | else: | 59 | else: |
| 53 | raise ComponentError(self, 'renderer') | 60 | raise ComponentError(self, "renderer") |
| 54 | except ComponentError: | 61 | except ComponentError: |
| 55 | return BlankFrame() | 62 | return BlankFrame() |
| 63 | |||
| 56 | return renderWrapper | 64 | return renderWrapper |
| 57 | 65 | ||
| 58 | def commandWrapper(func): | 66 | def commandWrapper(func): |
| 59 | '''Intercepts the command() method to check for global args''' | 67 | """Intercepts the command() method to check for global args""" |
| 68 | |||
| 60 | def commandWrapper(self, arg): | 69 | def commandWrapper(self, arg): |
| 61 | if arg.startswith('preset='): | 70 | if arg.startswith("preset="): |
| 62 | _, preset = arg.split('=', 1) | 71 | _, preset = arg.split("=", 1) |
| 63 | path = os.path.join(self.core.getPresetDir(self), preset) | 72 | path = os.path.join(self.core.getPresetDir(self), preset) |
| 64 | if not os.path.exists(path): | 73 | if not os.path.exists(path): |
| 65 | print('Couldn\'t locate preset "%s"' % preset) | 74 | print('Couldn\'t locate preset "%s"' % preset) |
| 66 | quit(1) | 75 | quit(1) |
| 67 | else: | 76 | else: |
| 68 | print('Opening "%s" preset on layer %s' % ( | 77 | print('Opening "%s" preset on layer %s' % (preset, self.compPos)) |
| 69 | preset, self.compPos) | ||
| 70 | ) | ||
| 71 | self.core.openPreset(path, self.compPos, preset) | 78 | self.core.openPreset(path, self.compPos, preset) |
| 72 | # Don't call the component's command() method | 79 | # Don't call the component's command() method |
| 73 | return | 80 | return |
| 74 | else: | 81 | else: |
| 75 | return func(self, arg) | 82 | return func(self, arg) |
| 83 | |||
| 76 | return commandWrapper | 84 | return commandWrapper |
| 77 | 85 | ||
| 78 | def propertiesWrapper(func): | 86 | def propertiesWrapper(func): |
| 79 | '''Intercepts the usual properties if the properties are locked.''' | 87 | """Intercepts the usual properties if the properties are locked.""" |
| 88 | |||
| 80 | def propertiesWrapper(self): | 89 | def propertiesWrapper(self): |
| 81 | if self._lockedProperties is not None: | 90 | if self._lockedProperties is not None: |
| 82 | return self._lockedProperties | 91 | return self._lockedProperties |
| @@ -85,22 +94,26 @@ class ComponentMetaclass(type(QtCore.QObject)): | |||
| 85 | return func(self) | 94 | return func(self) |
| 86 | except Exception: | 95 | except Exception: |
| 87 | try: | 96 | try: |
| 88 | raise ComponentError(self, 'properties') | 97 | raise ComponentError(self, "properties") |
| 89 | except ComponentError: | 98 | except ComponentError: |
| 90 | return [] | 99 | return [] |
| 100 | |||
| 91 | return propertiesWrapper | 101 | return propertiesWrapper |
| 92 | 102 | ||
| 93 | def errorWrapper(func): | 103 | def errorWrapper(func): |
| 94 | '''Intercepts the usual error message if it is locked.''' | 104 | """Intercepts the usual error message if it is locked.""" |
| 105 | |||
| 95 | def errorWrapper(self): | 106 | def errorWrapper(self): |
| 96 | if self._lockedError is not None: | 107 | if self._lockedError is not None: |
| 97 | return self._lockedError | 108 | return self._lockedError |
| 98 | else: | 109 | else: |
| 99 | return func(self) | 110 | return func(self) |
| 111 | |||
| 100 | return errorWrapper | 112 | return errorWrapper |
| 101 | 113 | ||
| 102 | def loadPresetWrapper(func): | 114 | def loadPresetWrapper(func): |
| 103 | '''Wraps loadPreset to handle the self.openingPreset boolean''' | 115 | """Wraps loadPreset to handle the self.openingPreset boolean""" |
| 116 | |||
| 104 | class openingPreset: | 117 | class openingPreset: |
| 105 | def __init__(self, comp): | 118 | def __init__(self, comp): |
| 106 | self.comp = comp | 119 | self.comp = comp |
| @@ -117,17 +130,19 @@ class ComponentMetaclass(type(QtCore.QObject)): | |||
| 117 | return func(self, *args) | 130 | return func(self, *args) |
| 118 | except Exception: | 131 | except Exception: |
| 119 | try: | 132 | try: |
| 120 | raise ComponentError(self, 'preset loader') | 133 | raise ComponentError(self, "preset loader") |
| 121 | except ComponentError: | 134 | except ComponentError: |
| 122 | return | 135 | return |
| 136 | |||
| 123 | return presetWrapper | 137 | return presetWrapper |
| 124 | 138 | ||
| 125 | def updateWrapper(func): | 139 | def updateWrapper(func): |
| 126 | ''' | 140 | """ |
| 127 | Calls _preUpdate before every subclass update(). | 141 | Calls _preUpdate before every subclass update(). |
| 128 | Afterwards, for non-user updates, calls _autoUpdate(). | 142 | Afterwards, for non-user updates, calls _autoUpdate(). |
| 129 | For undoable updates triggered by the user, calls _userUpdate() | 143 | For undoable updates triggered by the user, calls _userUpdate() |
| 130 | ''' | 144 | """ |
| 145 | |||
| 131 | class wrap: | 146 | class wrap: |
| 132 | def __init__(self, comp, auto): | 147 | def __init__(self, comp, auto): |
| 133 | self.comp = comp | 148 | self.comp = comp |
| @@ -137,28 +152,33 @@ class ComponentMetaclass(type(QtCore.QObject)): | |||
| 137 | self.comp._preUpdate() | 152 | self.comp._preUpdate() |
| 138 | 153 | ||
| 139 | def __exit__(self, *args): | 154 | def __exit__(self, *args): |
| 140 | if self.auto or self.comp.openingPreset \ | 155 | if ( |
| 141 | or not hasattr(self.comp.parent, 'undoStack'): | 156 | self.auto |
| 142 | log.verbose('Automatic update') | 157 | or self.comp.openingPreset |
| 158 | or not hasattr(self.comp.parent, "undoStack") | ||
| 159 | ): | ||
| 160 | log.verbose("Automatic update") | ||
| 143 | self.comp._autoUpdate() | 161 | self.comp._autoUpdate() |
| 144 | else: | 162 | else: |
| 145 | log.verbose('User update') | 163 | log.verbose("User update") |
| 146 | self.comp._userUpdate() | 164 | self.comp._userUpdate() |
| 147 | 165 | ||
| 148 | def updateWrapper(self, **kwargs): | 166 | def updateWrapper(self, **kwargs): |
| 149 | auto = kwargs['auto'] if 'auto' in kwargs else False | 167 | auto = kwargs["auto"] if "auto" in kwargs else False |
| 150 | with wrap(self, auto): | 168 | with wrap(self, auto): |
| 151 | try: | 169 | try: |
| 152 | return func(self) | 170 | return func(self) |
| 153 | except Exception: | 171 | except Exception: |
| 154 | try: | 172 | try: |
| 155 | raise ComponentError(self, 'update method') | 173 | raise ComponentError(self, "update method") |
| 156 | except ComponentError: | 174 | except ComponentError: |
| 157 | return | 175 | return |
| 176 | |||
| 158 | return updateWrapper | 177 | return updateWrapper |
| 159 | 178 | ||
| 160 | def widgetWrapper(func): | 179 | def widgetWrapper(func): |
| 161 | '''Connects all widgets to update method after the subclass's method''' | 180 | """Connects all widgets to update method after the subclass's method""" |
| 181 | |||
| 162 | class wrap: | 182 | class wrap: |
| 163 | def __init__(self, comp): | 183 | def __init__(self, comp): |
| 164 | self.comp = comp | 184 | self.comp = comp |
| @@ -169,92 +189,99 @@ class ComponentMetaclass(type(QtCore.QObject)): | |||
| 169 | def __exit__(self, *args): | 189 | def __exit__(self, *args): |
| 170 | for widgetList in self.comp._allWidgets.values(): | 190 | for widgetList in self.comp._allWidgets.values(): |
| 171 | for widget in widgetList: | 191 | for widget in widgetList: |
| 172 | log.verbose('Connecting %s', str( | 192 | log.verbose("Connecting %s", str(widget.__class__.__name__)) |
| 173 | widget.__class__.__name__)) | ||
| 174 | connectWidget(widget, self.comp.update) | 193 | connectWidget(widget, self.comp.update) |
| 175 | 194 | ||
| 176 | def widgetWrapper(self, *args, **kwargs): | 195 | def widgetWrapper(self, *args, **kwargs): |
| 177 | auto = kwargs['auto'] if 'auto' in kwargs else False | 196 | auto = kwargs["auto"] if "auto" in kwargs else False |
| 178 | with wrap(self): | 197 | with wrap(self): |
| 179 | try: | 198 | try: |
| 180 | return func(self, *args, **kwargs) | 199 | return func(self, *args, **kwargs) |
| 181 | except Exception: | 200 | except Exception: |
| 182 | try: | 201 | try: |
| 183 | raise ComponentError(self, 'widget creation') | 202 | raise ComponentError(self, "widget creation") |
| 184 | except ComponentError: | 203 | except ComponentError: |
| 185 | return | 204 | return |
| 205 | |||
| 186 | return widgetWrapper | 206 | return widgetWrapper |
| 187 | 207 | ||
| 188 | def __new__(cls, name, parents, attrs): | 208 | def __new__(cls, name, parents, attrs): |
| 189 | if 'ui' not in attrs: | 209 | if "ui" not in attrs: |
| 190 | # Use module name as ui filename by default | 210 | # Use module name as ui filename by default |
| 191 | attrs['ui'] = '%s.ui' % os.path.splitext( | 211 | attrs["ui"] = ( |
| 192 | attrs['__module__'].split('.')[-1] | 212 | "%s.ui" % os.path.splitext(attrs["__module__"].split(".")[-1])[0] |
| 193 | )[0] | 213 | ) |
| 194 | 214 | ||
| 195 | decorate = ( | 215 | decorate = ( |
| 196 | 'names', # Class methods | 216 | "names", # Class methods |
| 197 | 'error', 'audio', 'properties', # Properties | 217 | "error", |
| 198 | 'preFrameRender', 'previewRender', | 218 | "audio", |
| 199 | 'loadPreset', 'command', | 219 | "properties", # Properties |
| 200 | 'update', 'widget', | 220 | "preFrameRender", |
| 221 | "previewRender", | ||
| 222 | "loadPreset", | ||
| 223 | "command", | ||
| 224 | "update", | ||
| 225 | "widget", | ||
| 201 | ) | 226 | ) |
| 202 | 227 | ||
| 203 | # Auto-decorate methods | 228 | # Auto-decorate methods |
| 204 | for key in decorate: | 229 | for key in decorate: |
| 205 | if key not in attrs: | 230 | if key not in attrs: |
| 206 | continue | 231 | continue |
| 207 | if key in ('names'): | 232 | if key in ("names"): |
| 208 | attrs[key] = classmethod(attrs[key]) | 233 | attrs[key] = classmethod(attrs[key]) |
| 209 | elif key in ('audio'): | 234 | elif key in ("audio"): |
| 210 | attrs[key] = property(attrs[key]) | 235 | attrs[key] = property(attrs[key]) |
| 211 | elif key == 'command': | 236 | elif key == "command": |
| 212 | attrs[key] = cls.commandWrapper(attrs[key]) | 237 | attrs[key] = cls.commandWrapper(attrs[key]) |
| 213 | elif key == 'previewRender': | 238 | elif key == "previewRender": |
| 214 | attrs[key] = cls.renderWrapper(attrs[key]) | 239 | attrs[key] = cls.renderWrapper(attrs[key]) |
| 215 | elif key == 'preFrameRender': | 240 | elif key == "preFrameRender": |
| 216 | attrs[key] = cls.initializationWrapper(attrs[key]) | 241 | attrs[key] = cls.initializationWrapper(attrs[key]) |
| 217 | elif key == 'properties': | 242 | elif key == "properties": |
| 218 | attrs[key] = cls.propertiesWrapper(attrs[key]) | 243 | attrs[key] = cls.propertiesWrapper(attrs[key]) |
| 219 | elif key == 'error': | 244 | elif key == "error": |
| 220 | attrs[key] = cls.errorWrapper(attrs[key]) | 245 | attrs[key] = cls.errorWrapper(attrs[key]) |
| 221 | elif key == 'loadPreset': | 246 | elif key == "loadPreset": |
| 222 | attrs[key] = cls.loadPresetWrapper(attrs[key]) | 247 | attrs[key] = cls.loadPresetWrapper(attrs[key]) |
| 223 | elif key == 'update': | 248 | elif key == "update": |
| 224 | attrs[key] = cls.updateWrapper(attrs[key]) | 249 | attrs[key] = cls.updateWrapper(attrs[key]) |
| 225 | elif key == 'widget' and parents[0] != QtCore.QObject: | 250 | elif key == "widget" and parents[0] != QtCore.QObject: |
| 226 | attrs[key] = cls.widgetWrapper(attrs[key]) | 251 | attrs[key] = cls.widgetWrapper(attrs[key]) |
| 227 | 252 | ||
| 228 | # Turn version string into a number | 253 | # Turn version string into a number |
| 229 | try: | 254 | try: |
| 230 | if 'version' not in attrs: | 255 | if "version" not in attrs: |
| 231 | log.error( | 256 | log.error( |
| 232 | 'No version attribute in %s. Defaulting to 1', | 257 | "No version attribute in %s. Defaulting to 1", |
| 233 | attrs['name']) | 258 | attrs["name"], |
| 234 | attrs['version'] = 1 | 259 | ) |
| 260 | attrs["version"] = 1 | ||
| 235 | else: | 261 | else: |
| 236 | attrs['version'] = int(attrs['version'].split('.')[0]) | 262 | attrs["version"] = int(attrs["version"].split(".")[0]) |
| 237 | except ValueError: | 263 | except ValueError: |
| 238 | log.critical( | 264 | log.critical( |
| 239 | '%s component has an invalid version string:\n%s', | 265 | "%s component has an invalid version string:\n%s", |
| 240 | attrs['name'], str(attrs['version']) | 266 | attrs["name"], |
| 267 | str(attrs["version"]), | ||
| 241 | ) | 268 | ) |
| 242 | except KeyError: | 269 | except KeyError: |
| 243 | log.critical('%s component has no version string.', attrs['name']) | 270 | log.critical("%s component has no version string.", attrs["name"]) |
| 244 | else: | 271 | else: |
| 245 | return super().__new__(cls, name, parents, attrs) | 272 | return super().__new__(cls, name, parents, attrs) |
| 246 | quit(1) | 273 | quit(1) |
| 247 | 274 | ||
| 248 | 275 | ||
| 249 | class Component(QtCore.QObject, metaclass=ComponentMetaclass): | 276 | class Component(QtCore.QObject, metaclass=ComponentMetaclass): |
| 250 | ''' | 277 | """ |
| 251 | The base class for components to inherit. | 278 | The base class for components to inherit. |
| 252 | ''' | 279 | """ |
| 253 | 280 | ||
| 254 | name = 'Component' | 281 | name = "Component" |
| 255 | # ui = 'name_Of_Non_Default_Ui_File' | 282 | # ui = 'name_Of_Non_Default_Ui_File' |
| 256 | 283 | ||
| 257 | version = '1.0.0' | 284 | version = "1.0.0" |
| 258 | # The major version (before the first dot) is used to determine | 285 | # The major version (before the first dot) is used to determine |
| 259 | # preset compatibility; the rest is ignored so it can be non-numeric. | 286 | # preset compatibility; the rest is ignored so it can be non-numeric. |
| 260 | 287 | ||
| @@ -297,19 +324,19 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 297 | 324 | ||
| 298 | def __repr__(self): | 325 | def __repr__(self): |
| 299 | import pprint | 326 | import pprint |
| 327 | |||
| 300 | try: | 328 | try: |
| 301 | preset = self.savePreset() | 329 | preset = self.savePreset() |
| 302 | except Exception as e: | 330 | except Exception as e: |
| 303 | preset = '%s occurred while saving preset' % str(e) | 331 | preset = "%s occurred while saving preset" % str(e) |
| 304 | 332 | ||
| 305 | return ( | 333 | return "Component(module %s, pos %s) (%s)\n" "Name: %s v%s\nPreset: %s" % ( |
| 306 | 'Component(module %s, pos %s) (%s)\n' | 334 | self.moduleIndex, |
| 307 | 'Name: %s v%s\nPreset: %s' % ( | 335 | self.compPos, |
| 308 | self.moduleIndex, self.compPos, | 336 | object.__repr__(self), |
| 309 | object.__repr__(self), | 337 | self.__class__.name, |
| 310 | self.__class__.name, str(self.__class__.version), | 338 | str(self.__class__.version), |
| 311 | pprint.pformat(preset) | 339 | pprint.pformat(preset), |
| 312 | ) | ||
| 313 | ) | 340 | ) |
| 314 | 341 | ||
| 315 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 342 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| @@ -321,17 +348,17 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 321 | return image | 348 | return image |
| 322 | 349 | ||
| 323 | def preFrameRender(self, **kwargs): | 350 | def preFrameRender(self, **kwargs): |
| 324 | ''' | 351 | """ |
| 325 | Must call super() when subclassing | 352 | Must call super() when subclassing |
| 326 | Triggered only before a video is exported (video_thread.py) | 353 | Triggered only before a video is exported (video_thread.py) |
| 327 | self.audioFile = filepath to the main input audio file | 354 | self.audioFile = filepath to the main input audio file |
| 328 | self.completeAudioArray = a list of audio samples | 355 | self.completeAudioArray = a list of audio samples |
| 329 | self.sampleSize = number of audio samples per video frame | 356 | self.sampleSize = number of audio samples per video frame |
| 330 | self.progressBarUpdate = signal to set progress bar number | 357 | self.progressBarUpdate = signal to set progress bar number |
| 331 | self.progressBarSetText = signal to set progress bar text | 358 | self.progressBarSetText = signal to set progress bar text |
| 332 | Use the latter two signals to update the MainWindow if needed | 359 | Use the latter two signals to update the MainWindow if needed |
| 333 | for a long initialization procedure (i.e., for a visualizer) | 360 | for a long initialization procedure (i.e., for a visualizer) |
| 334 | ''' | 361 | """ |
| 335 | for key, value in kwargs.items(): | 362 | for key, value in kwargs.items(): |
| 336 | setattr(self, key, value) | 363 | setattr(self, key, value) |
| 337 | 364 | ||
| @@ -348,92 +375,94 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 348 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 375 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 349 | 376 | ||
| 350 | def properties(self): | 377 | def properties(self): |
| 351 | ''' | 378 | """ |
| 352 | Return a list of properties to signify if your component is | 379 | Return a list of properties to signify if your component is |
| 353 | non-animated ('static'), returns sound ('audio'), or has | 380 | non-animated ('static'), returns sound ('audio'), or has |
| 354 | encountered an error in configuration ('error'). | 381 | encountered an error in configuration ('error'). |
| 355 | ''' | 382 | """ |
| 356 | return [] | 383 | return [] |
| 357 | 384 | ||
| 358 | def error(self): | 385 | def error(self): |
| 359 | ''' | 386 | """ |
| 360 | Return a string containing an error message, or None for a default. | 387 | Return a string containing an error message, or None for a default. |
| 361 | Or tuple of two strings for a message with details. | 388 | Or tuple of two strings for a message with details. |
| 362 | Alternatively use lockError(msgString) within properties() | 389 | Alternatively use lockError(msgString) within properties() |
| 363 | to skip this method entirely. | 390 | to skip this method entirely. |
| 364 | ''' | 391 | """ |
| 365 | return | 392 | return |
| 366 | 393 | ||
| 367 | def audio(self): | 394 | def audio(self): |
| 368 | ''' | 395 | """ |
| 369 | Return audio to mix into master as a tuple with two elements: | 396 | Return audio to mix into master as a tuple with two elements: |
| 370 | The first element can be: | 397 | The first element can be: |
| 371 | - A string (path to audio file), | 398 | - A string (path to audio file), |
| 372 | - Or an object that returns audio data through a pipe | 399 | - Or an object that returns audio data through a pipe |
| 373 | The second element must be a dictionary of ffmpeg filters/options | 400 | The second element must be a dictionary of ffmpeg filters/options |
| 374 | to apply to the input stream. See the filter docs for ideas: | 401 | to apply to the input stream. See the filter docs for ideas: |
| 375 | https://ffmpeg.org/ffmpeg-filters.html | 402 | https://ffmpeg.org/ffmpeg-filters.html |
| 376 | ''' | 403 | """ |
| 377 | 404 | ||
| 378 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 405 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 379 | # Idle Methods | 406 | # Idle Methods |
| 380 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 407 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 381 | 408 | ||
| 382 | def widget(self, parent): | 409 | def widget(self, parent): |
| 383 | ''' | 410 | """ |
| 384 | Call super().widget(*args) to create the component widget | 411 | Call super().widget(*args) to create the component widget |
| 385 | which also auto-connects any common widgets (e.g., checkBoxes) | 412 | which also auto-connects any common widgets (e.g., checkBoxes) |
| 386 | to self.update(). Then in a subclass connect special actions | 413 | to self.update(). Then in a subclass connect special actions |
| 387 | (e.g., pushButtons to select a file) and initialize | 414 | (e.g., pushButtons to select a file) and initialize |
| 388 | ''' | 415 | """ |
| 389 | self.parent = parent | 416 | self.parent = parent |
| 390 | self.settings = parent.settings | 417 | self.settings = parent.settings |
| 391 | log.verbose( | 418 | log.verbose( |
| 392 | 'Creating UI for %s #%s\'s widget', | 419 | "Creating UI for %s #%s's widget", |
| 393 | self.__class__.name, self.compPos | 420 | self.__class__.name, |
| 421 | self.compPos, | ||
| 394 | ) | 422 | ) |
| 395 | self.page = self.loadUi(self.__class__.ui) | 423 | self.page = self.loadUi(self.__class__.ui) |
| 396 | 424 | ||
| 397 | # Find all normal widgets which will be connected after subclass method | 425 | # Find all normal widgets which will be connected after subclass method |
| 398 | self._allWidgets = { | 426 | self._allWidgets = { |
| 399 | 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), | 427 | "lineEdit": self.page.findChildren(QtWidgets.QLineEdit), |
| 400 | 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), | 428 | "checkBox": self.page.findChildren(QtWidgets.QCheckBox), |
| 401 | 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), | 429 | "spinBox": self.page.findChildren(QtWidgets.QSpinBox), |
| 402 | 'comboBox': self.page.findChildren(QtWidgets.QComboBox), | 430 | "comboBox": self.page.findChildren(QtWidgets.QComboBox), |
| 403 | } | 431 | } |
| 404 | self._allWidgets['spinBox'].extend( | 432 | self._allWidgets["spinBox"].extend( |
| 405 | self.page.findChildren(QtWidgets.QDoubleSpinBox) | 433 | self.page.findChildren(QtWidgets.QDoubleSpinBox) |
| 406 | ) | 434 | ) |
| 407 | 435 | ||
| 408 | def update(self): | 436 | def update(self): |
| 409 | ''' | 437 | """ |
| 410 | Starting point for a component update. A subclass should override | 438 | Starting point for a component update. A subclass should override |
| 411 | this method, and the base class will then magically insert a call | 439 | this method, and the base class will then magically insert a call |
| 412 | to either _autoUpdate() or _userUpdate() at the end. | 440 | to either _autoUpdate() or _userUpdate() at the end. |
| 413 | ''' | 441 | """ |
| 414 | 442 | ||
| 415 | def loadPreset(self, presetDict, presetName=None): | 443 | def loadPreset(self, presetDict, presetName=None): |
| 416 | ''' | 444 | """ |
| 417 | Subclasses should take (presetDict, *args) as args. | 445 | Subclasses should take (presetDict, *args) as args. |
| 418 | Must use super().loadPreset(presetDict, *args) first, | 446 | Must use super().loadPreset(presetDict, *args) first, |
| 419 | then update self.page widgets using the preset dict. | 447 | then update self.page widgets using the preset dict. |
| 420 | ''' | 448 | """ |
| 421 | self.currentPreset = presetName \ | 449 | self.currentPreset = ( |
| 422 | if presetName is not None else presetDict['preset'] | 450 | presetName if presetName is not None else presetDict["preset"] |
| 451 | ) | ||
| 423 | for attr, widget in self._trackedWidgets.items(): | 452 | for attr, widget in self._trackedWidgets.items(): |
| 424 | key = attr if attr not in self._presetNames \ | 453 | key = attr if attr not in self._presetNames else self._presetNames[attr] |
| 425 | else self._presetNames[attr] | ||
| 426 | try: | 454 | try: |
| 427 | val = presetDict[key] | 455 | val = presetDict[key] |
| 428 | except KeyError as e: | 456 | except KeyError as e: |
| 429 | log.info( | 457 | log.info( |
| 430 | '%s missing value %s. Outdated preset?', | 458 | "%s missing value %s. Outdated preset?", |
| 431 | self.currentPreset, str(e) | 459 | self.currentPreset, |
| 460 | str(e), | ||
| 432 | ) | 461 | ) |
| 433 | val = getattr(self, key) | 462 | val = getattr(self, key) |
| 434 | 463 | ||
| 435 | if attr in self._colorWidgets: | 464 | if attr in self._colorWidgets: |
| 436 | widget.setText('%s,%s,%s' % val) | 465 | widget.setText("%s,%s,%s" % val) |
| 437 | btnStyle = ( | 466 | btnStyle = ( |
| 438 | "QPushButton { background-color : %s; outline: none; }" | 467 | "QPushButton { background-color : %s; outline: none; }" |
| 439 | % QColor(*val).name() | 468 | % QColor(*val).name() |
| @@ -450,8 +479,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 450 | saveValueStore = {} | 479 | saveValueStore = {} |
| 451 | for attr, widget in self._trackedWidgets.items(): | 480 | for attr, widget in self._trackedWidgets.items(): |
| 452 | presetAttrName = ( | 481 | presetAttrName = ( |
| 453 | attr if attr not in self._presetNames | 482 | attr if attr not in self._presetNames else self._presetNames[attr] |
| 454 | else self._presetNames[attr] | ||
| 455 | ) | 483 | ) |
| 456 | if attr in self._relativeWidgets: | 484 | if attr in self._relativeWidgets: |
| 457 | try: | 485 | try: |
| @@ -465,19 +493,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 465 | return saveValueStore | 493 | return saveValueStore |
| 466 | 494 | ||
| 467 | def commandHelp(self): | 495 | def commandHelp(self): |
| 468 | '''Help text as string for this component's commandline arguments''' | 496 | """Help text as string for this component's commandline arguments""" |
| 469 | 497 | ||
| 470 | def command(self, arg=''): | 498 | def command(self, arg=""): |
| 471 | ''' | 499 | """ |
| 472 | Configure a component using an arg from the commandline. This is | 500 | Configure a component using an arg from the commandline. This is |
| 473 | never called if global args like 'preset=' are found in the arg. | 501 | never called if global args like 'preset=' are found in the arg. |
| 474 | So simply check for any non-global args in your component and | 502 | So simply check for any non-global args in your component and |
| 475 | call super().command() at the end to get a Help message. | 503 | call super().command() at the end to get a Help message. |
| 476 | ''' | 504 | """ |
| 477 | print( | 505 | print( |
| 478 | self.__class__.name, 'Usage:\n' | 506 | self.__class__.name, |
| 479 | 'Open a preset for this component:\n' | 507 | "Usage:\n" "Open a preset for this component:\n" ' "preset=Preset Name"', |
| 480 | ' "preset=Preset Name"' | ||
| 481 | ) | 508 | ) |
| 482 | self.commandHelp() | 509 | self.commandHelp() |
| 483 | quit(0) | 510 | quit(0) |
| @@ -486,19 +513,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 486 | # "Private" Methods | 513 | # "Private" Methods |
| 487 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 514 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 488 | def _preUpdate(self): | 515 | def _preUpdate(self): |
| 489 | '''Happens before subclass update()''' | 516 | """Happens before subclass update()""" |
| 490 | for attr in self._relativeWidgets: | 517 | for attr in self._relativeWidgets: |
| 491 | self.updateRelativeWidget(attr) | 518 | self.updateRelativeWidget(attr) |
| 492 | 519 | ||
| 493 | def _userUpdate(self): | 520 | def _userUpdate(self): |
| 494 | '''Happens after subclass update() for an undoable update by user.''' | 521 | """Happens after subclass update() for an undoable update by user.""" |
| 495 | oldWidgetVals = { | 522 | oldWidgetVals = { |
| 496 | attr: copy(getattr(self, attr)) | 523 | attr: copy(getattr(self, attr)) for attr in self._trackedWidgets |
| 497 | for attr in self._trackedWidgets | ||
| 498 | } | 524 | } |
| 499 | newWidgetVals = { | 525 | newWidgetVals = { |
| 500 | attr: getWidgetValue(widget) | 526 | attr: ( |
| 501 | if attr not in self._colorWidgets else rgbFromString(widget.text()) | 527 | getWidgetValue(widget) |
| 528 | if attr not in self._colorWidgets | ||
| 529 | else rgbFromString(widget.text()) | ||
| 530 | ) | ||
| 502 | for attr, widget in self._trackedWidgets.items() | 531 | for attr, widget in self._trackedWidgets.items() |
| 503 | } | 532 | } |
| 504 | modifiedWidgets = { | 533 | modifiedWidgets = { |
| @@ -511,7 +540,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 511 | self.parent.undoStack.push(action) | 540 | self.parent.undoStack.push(action) |
| 512 | 541 | ||
| 513 | def _autoUpdate(self): | 542 | def _autoUpdate(self): |
| 514 | '''Happens after subclass update() for an internal component update.''' | 543 | """Happens after subclass update() for an internal component update.""" |
| 515 | newWidgetVals = { | 544 | newWidgetVals = { |
| 516 | attr: getWidgetValue(widget) | 545 | attr: getWidgetValue(widget) |
| 517 | for attr, widget in self._trackedWidgets.items() | 546 | for attr, widget in self._trackedWidgets.items() |
| @@ -520,10 +549,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 520 | self._sendUpdateSignal() | 549 | self._sendUpdateSignal() |
| 521 | 550 | ||
| 522 | def setAttrs(self, attrDict): | 551 | def setAttrs(self, attrDict): |
| 523 | ''' | 552 | """ |
| 524 | Sets attrs (linked to trackedWidgets) in this component to | 553 | Sets attrs (linked to trackedWidgets) in this component to |
| 525 | the values in the attrDict. Mutates certain widget values if needed | 554 | the values in the attrDict. Mutates certain widget values if needed |
| 526 | ''' | 555 | """ |
| 527 | for attr, val in attrDict.items(): | 556 | for attr, val in attrDict.items(): |
| 528 | if attr in self._colorWidgets: | 557 | if attr in self._colorWidgets: |
| 529 | # Color Widgets must have a tuple & have a button to update | 558 | # Color Widgets must have a tuple & have a button to update |
| @@ -533,110 +562,111 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 533 | rgbTuple = rgbFromString(val) | 562 | rgbTuple = rgbFromString(val) |
| 534 | btnStyle = ( | 563 | btnStyle = ( |
| 535 | "QPushButton { background-color : %s; outline: none; }" | 564 | "QPushButton { background-color : %s; outline: none; }" |
| 536 | % QColor(*rgbTuple).name()) | 565 | % QColor(*rgbTuple).name() |
| 566 | ) | ||
| 537 | self._colorWidgets[attr].setStyleSheet(btnStyle) | 567 | self._colorWidgets[attr].setStyleSheet(btnStyle) |
| 538 | setattr(self, attr, rgbTuple) | 568 | setattr(self, attr, rgbTuple) |
| 539 | 569 | ||
| 540 | else: | 570 | else: |
| 541 | # Normal tracked widget | 571 | # Normal tracked widget |
| 542 | setattr(self, attr, val) | 572 | setattr(self, attr, val) |
| 543 | log.verbose('Setting %s self.%s to %s' % ( | 573 | log.verbose("Setting %s self.%s to %s" % (self.__class__.name, attr, val)) |
| 544 | self.__class__.name, attr, val)) | ||
| 545 | 574 | ||
| 546 | def setWidgetValues(self, attrDict): | 575 | def setWidgetValues(self, attrDict): |
| 547 | ''' | 576 | """ |
| 548 | Sets widgets defined by keys in trackedWidgets in this preset to | 577 | Sets widgets defined by keys in trackedWidgets in this preset to |
| 549 | the values in the attrDict. | 578 | the values in the attrDict. |
| 550 | ''' | 579 | """ |
| 551 | affectedWidgets = [ | 580 | affectedWidgets = [self._trackedWidgets[attr] for attr in attrDict] |
| 552 | self._trackedWidgets[attr] for attr in attrDict | ||
| 553 | ] | ||
| 554 | with blockSignals(affectedWidgets): | 581 | with blockSignals(affectedWidgets): |
| 555 | for attr, val in attrDict.items(): | 582 | for attr, val in attrDict.items(): |
| 556 | widget = self._trackedWidgets[attr] | 583 | widget = self._trackedWidgets[attr] |
| 557 | if attr in self._colorWidgets: | 584 | if attr in self._colorWidgets: |
| 558 | val = '%s,%s,%s' % val | 585 | val = "%s,%s,%s" % val |
| 559 | setWidgetValue(widget, val) | 586 | setWidgetValue(widget, val) |
| 560 | 587 | ||
| 561 | def _sendUpdateSignal(self): | 588 | def _sendUpdateSignal(self): |
| 562 | if not self.core.openingProject: | 589 | if not self.core.openingProject: |
| 563 | self.parent.drawPreview() | 590 | self.parent.drawPreview() |
| 564 | saveValueStore = self.savePreset() | 591 | saveValueStore = self.savePreset() |
| 565 | saveValueStore['preset'] = self.currentPreset | 592 | saveValueStore["preset"] = self.currentPreset |
| 566 | self.modified.emit(self.compPos, saveValueStore) | 593 | self.modified.emit(self.compPos, saveValueStore) |
| 567 | 594 | ||
| 568 | def trackWidgets(self, trackDict, **kwargs): | 595 | def trackWidgets(self, trackDict, **kwargs): |
| 569 | ''' | 596 | """ |
| 570 | Name widgets to track in update(), savePreset(), loadPreset(), and | 597 | Name widgets to track in update(), savePreset(), loadPreset(), and |
| 571 | command(). Requires a dict of attr names as keys, widgets as values | 598 | command(). Requires a dict of attr names as keys, widgets as values |
| 572 | 599 | ||
| 573 | Optional args: | 600 | Optional args: |
| 574 | 'presetNames': preset variable names to replace attr names | 601 | 'presetNames': preset variable names to replace attr names |
| 575 | 'commandArgs': arg keywords that differ from attr names | 602 | 'commandArgs': arg keywords that differ from attr names |
| 576 | 'colorWidgets': identify attr as RGB tuple & update button CSS | 603 | 'colorWidgets': identify attr as RGB tuple & update button CSS |
| 577 | 'relativeWidgets': change value proportionally to resolution | 604 | 'relativeWidgets': change value proportionally to resolution |
| 578 | 605 | ||
| 579 | NOTE: Any kwarg key set to None will selectively disable tracking. | 606 | NOTE: Any kwarg key set to None will selectively disable tracking. |
| 580 | ''' | 607 | """ |
| 581 | self._trackedWidgets = trackDict | 608 | self._trackedWidgets = trackDict |
| 582 | for kwarg in kwargs: | 609 | for kwarg in kwargs: |
| 583 | try: | 610 | try: |
| 584 | if kwarg in ( | 611 | if kwarg in ( |
| 585 | 'presetNames', | 612 | "presetNames", |
| 586 | 'commandArgs', | 613 | "commandArgs", |
| 587 | 'colorWidgets', | 614 | "colorWidgets", |
| 588 | 'relativeWidgets', | 615 | "relativeWidgets", |
| 589 | ): | 616 | ): |
| 590 | setattr(self, '_{}'.format(kwarg), kwargs[kwarg]) | 617 | setattr(self, "_{}".format(kwarg), kwargs[kwarg]) |
| 591 | else: | 618 | else: |
| 592 | raise ComponentError( | 619 | raise ComponentError(self, "Nonsensical keywords to trackWidgets.") |
| 593 | self, 'Nonsensical keywords to trackWidgets.') | ||
| 594 | except ComponentError: | 620 | except ComponentError: |
| 595 | continue | 621 | continue |
| 596 | 622 | ||
| 597 | if kwarg == 'colorWidgets': | 623 | if kwarg == "colorWidgets": |
| 624 | |||
| 598 | def makeColorFunc(attr): | 625 | def makeColorFunc(attr): |
| 599 | def pickColor_(): | 626 | def pickColor_(): |
| 600 | self.mergeUndo = False | 627 | self.mergeUndo = False |
| 601 | self.pickColor( | 628 | self.pickColor( |
| 602 | self._trackedWidgets[attr], | 629 | self._trackedWidgets[attr], |
| 603 | self._colorWidgets[attr] | 630 | self._colorWidgets[attr], |
| 604 | ) | 631 | ) |
| 605 | self.mergeUndo = True | 632 | self.mergeUndo = True |
| 633 | |||
| 606 | return pickColor_ | 634 | return pickColor_ |
| 607 | self._colorFuncs = { | 635 | |
| 608 | attr: makeColorFunc(attr) for attr in kwargs[kwarg] | 636 | self._colorFuncs = {attr: makeColorFunc(attr) for attr in kwargs[kwarg]} |
| 609 | } | ||
| 610 | for attr, func in self._colorFuncs.items(): | 637 | for attr, func in self._colorFuncs.items(): |
| 611 | self._colorWidgets[attr].clicked.connect(func) | 638 | self._colorWidgets[attr].clicked.connect(func) |
| 612 | self._colorWidgets[attr].setStyleSheet( | 639 | self._colorWidgets[attr].setStyleSheet( |
| 613 | "QPushButton {" | 640 | "QPushButton {" "background-color : #FFFFFF; outline: none; }" |
| 614 | "background-color : #FFFFFF; outline: none; }" | ||
| 615 | ) | 641 | ) |
| 616 | 642 | ||
| 617 | if kwarg == 'relativeWidgets': | 643 | if kwarg == "relativeWidgets": |
| 618 | # store maximum values of spinBoxes to be scaled appropriately | 644 | # store maximum values of spinBoxes to be scaled appropriately |
| 619 | for attr in kwargs[kwarg]: | 645 | for attr in kwargs[kwarg]: |
| 620 | self._relativeMaximums[attr] = \ | 646 | self._relativeMaximums[attr] = self._trackedWidgets[attr].maximum() |
| 621 | self._trackedWidgets[attr].maximum() | ||
| 622 | self.updateRelativeWidgetMaximum(attr) | 647 | self.updateRelativeWidgetMaximum(attr) |
| 623 | setattr( | 648 | setattr(self, attr, getWidgetValue(self._trackedWidgets[attr])) |
| 624 | self, attr, getWidgetValue(self._trackedWidgets[attr]) | ||
| 625 | ) | ||
| 626 | 649 | ||
| 627 | self._preUpdate() | 650 | self._preUpdate() |
| 628 | self._autoUpdate() | 651 | self._autoUpdate() |
| 629 | 652 | ||
| 630 | def pickColor(self, textWidget, button): | 653 | def pickColor(self, textWidget, button): |
| 631 | '''Use color picker to get color input from the user.''' | 654 | """Use color picker to get color input from the user.""" |
| 632 | dialog = QtWidgets.QColorDialog() | 655 | dialog = QtWidgets.QColorDialog() |
| 633 | dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True) | 656 | # TODO alpha channel is not actually shown in most color picker widgets? |
| 657 | dialog.setOption( | ||
| 658 | QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel, True | ||
| 659 | ) | ||
| 634 | color = dialog.getColor() | 660 | color = dialog.getColor() |
| 635 | if color.isValid(): | 661 | if color.isValid(): |
| 636 | RGBstring = '%s,%s,%s' % ( | 662 | RGBstring = "%s,%s,%s" % ( |
| 637 | str(color.red()), str(color.green()), str(color.blue())) | 663 | str(color.red()), |
| 638 | btnStyle = "QPushButton{background-color: %s; outline: none;}" \ | 664 | str(color.green()), |
| 639 | % color.name() | 665 | str(color.blue()), |
| 666 | ) | ||
| 667 | btnStyle = ( | ||
| 668 | "QPushButton{background-color: %s; outline: none;}" % color.name() | ||
| 669 | ) | ||
| 640 | textWidget.setText(RGBstring) | 670 | textWidget.setText(RGBstring) |
| 641 | button.setStyleSheet(btnStyle) | 671 | button.setStyleSheet(btnStyle) |
| 642 | 672 | ||
| @@ -659,25 +689,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 659 | self._lockedSize = None | 689 | self._lockedSize = None |
| 660 | 690 | ||
| 661 | def loadUi(self, filename): | 691 | def loadUi(self, filename): |
| 662 | '''Load a Qt Designer ui file to use for this component's widget''' | 692 | """Load a Qt Designer ui file to use for this component's widget""" |
| 663 | return uic.loadUi(os.path.join(self.core.componentsPath, filename)) | 693 | return uic.loadUi(os.path.join(self.core.componentsPath, filename)) |
| 664 | 694 | ||
| 665 | @property | 695 | @property |
| 666 | def width(self): | 696 | def width(self): |
| 667 | if self._lockedSize is None: | 697 | if self._lockedSize is None: |
| 668 | return int(self.settings.value('outputWidth')) | 698 | return int(self.settings.value("outputWidth")) |
| 669 | else: | 699 | else: |
| 670 | return self._lockedSize[0] | 700 | return self._lockedSize[0] |
| 671 | 701 | ||
| 672 | @property | 702 | @property |
| 673 | def height(self): | 703 | def height(self): |
| 674 | if self._lockedSize is None: | 704 | if self._lockedSize is None: |
| 675 | return int(self.settings.value('outputHeight')) | 705 | return int(self.settings.value("outputHeight")) |
| 676 | else: | 706 | else: |
| 677 | return self._lockedSize[1] | 707 | return self._lockedSize[1] |
| 678 | 708 | ||
| 679 | def cancel(self): | 709 | def cancel(self): |
| 680 | '''Stop any lengthy process in response to this variable.''' | 710 | """Stop any lengthy process in response to this variable.""" |
| 681 | self.canceled = True | 711 | self.canceled = True |
| 682 | 712 | ||
| 683 | def reset(self): | 713 | def reset(self): |
| @@ -688,22 +718,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 688 | def relativeWidgetAxis(func): | 718 | def relativeWidgetAxis(func): |
| 689 | def relativeWidgetAxis(self, attr, *args, **kwargs): | 719 | def relativeWidgetAxis(self, attr, *args, **kwargs): |
| 690 | hasVerticalWords = ( | 720 | hasVerticalWords = ( |
| 691 | lambda attr: | 721 | lambda attr: "height" in attr.lower() |
| 692 | 'height' in attr.lower() or | 722 | or "ypos" in attr.lower() |
| 693 | 'ypos' in attr.lower() or | 723 | or attr == "y" |
| 694 | attr == 'y' | ||
| 695 | ) | 724 | ) |
| 696 | if 'axis' not in kwargs: | 725 | if "axis" not in kwargs: |
| 697 | axis = self.width | 726 | axis = self.width |
| 698 | if hasVerticalWords(attr): | 727 | if hasVerticalWords(attr): |
| 699 | axis = self.height | 728 | axis = self.height |
| 700 | kwargs['axis'] = axis | 729 | kwargs["axis"] = axis |
| 701 | if 'axis' in kwargs and type(kwargs['axis']) is tuple: | 730 | if "axis" in kwargs and type(kwargs["axis"]) is tuple: |
| 702 | axis = kwargs['axis'][0] | 731 | axis = kwargs["axis"][0] |
| 703 | if hasVerticalWords(attr): | 732 | if hasVerticalWords(attr): |
| 704 | axis = kwargs['axis'][1] | 733 | axis = kwargs["axis"][1] |
| 705 | kwargs['axis'] = axis | 734 | kwargs["axis"] = axis |
| 706 | return func(self, attr, *args, **kwargs) | 735 | return func(self, attr, *args, **kwargs) |
| 736 | |||
| 707 | return relativeWidgetAxis | 737 | return relativeWidgetAxis |
| 708 | 738 | ||
| 709 | @relativeWidgetAxis | 739 | @relativeWidgetAxis |
| @@ -712,14 +742,20 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 712 | val = self._relativeValues[attr] | 742 | val = self._relativeValues[attr] |
| 713 | if val > 50.0: | 743 | if val > 50.0: |
| 714 | log.warning( | 744 | log.warning( |
| 715 | '%s #%s attempted to set %s to dangerously high number %s', | 745 | "%s #%s attempted to set %s to dangerously high number %s", |
| 716 | self.__class__.name, self.compPos, attr, val | 746 | self.__class__.name, |
| 747 | self.compPos, | ||
| 748 | attr, | ||
| 749 | val, | ||
| 717 | ) | 750 | ) |
| 718 | val = 50.0 | 751 | val = 50.0 |
| 719 | result = math.ceil(kwargs['axis'] * val) | 752 | result = math.ceil(kwargs["axis"] * val) |
| 720 | log.verbose( | 753 | log.verbose( |
| 721 | 'Converting %s: f%s to px%s using axis %s', | 754 | "Converting %s: f%s to px%s using axis %s", |
| 722 | attr, val, result, kwargs['axis'] | 755 | attr, |
| 756 | val, | ||
| 757 | result, | ||
| 758 | kwargs["axis"], | ||
| 723 | ) | 759 | ) |
| 724 | return result | 760 | return result |
| 725 | 761 | ||
| @@ -727,65 +763,63 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): | |||
| 727 | def floatValForAttr(self, attr, val=None, **kwargs): | 763 | def floatValForAttr(self, attr, val=None, **kwargs): |
| 728 | if val is None: | 764 | if val is None: |
| 729 | val = self._trackedWidgets[attr].value() | 765 | val = self._trackedWidgets[attr].value() |
| 730 | return val / kwargs['axis'] | 766 | return val / kwargs["axis"] |
| 731 | 767 | ||
| 732 | def setRelativeWidget(self, attr, floatVal): | 768 | def setRelativeWidget(self, attr, floatVal): |
| 733 | '''Set a relative widget using a float''' | 769 | """Set a relative widget using a float""" |
| 734 | pixelVal = self.pixelValForAttr(attr, floatVal) | 770 | pixelVal = self.pixelValForAttr(attr, floatVal) |
| 735 | with blockSignals(self._trackedWidgets[attr]): | 771 | with blockSignals(self._trackedWidgets[attr]): |
| 736 | self._trackedWidgets[attr].setValue(pixelVal) | 772 | self._trackedWidgets[attr].setValue(pixelVal) |
| 737 | self.update(auto=True) | 773 | self.update(auto=True) |
| 738 | 774 | ||
| 739 | def getOldAttr(self, attr): | 775 | def getOldAttr(self, attr): |
| 740 | ''' | 776 | """ |
| 741 | Returns previous state of this attr. Used to determine whether | 777 | Returns previous state of this attr. Used to determine whether |
| 742 | a relative widget must be updated. Required because undoing/redoing | 778 | a relative widget must be updated. Required because undoing/redoing |
| 743 | can make determining the 'previous' value tricky. | 779 | can make determining the 'previous' value tricky. |
| 744 | ''' | 780 | """ |
| 745 | if self.oldAttrs is not None: | 781 | if self.oldAttrs is not None: |
| 746 | return self.oldAttrs[attr] | 782 | return self.oldAttrs[attr] |
| 747 | else: | 783 | else: |
| 748 | try: | 784 | try: |
| 749 | return getattr(self, attr) | 785 | return getattr(self, attr) |
| 750 | except AttributeError: | 786 | except AttributeError: |
| 751 | log.error('Using visible values instead of oldAttrs') | 787 | log.error("Using visible values instead of oldAttrs") |
| 752 | return self._trackedWidgets[attr].value() | 788 | return self._trackedWidgets[attr].value() |
| 753 | 789 | ||
| 754 | def updateRelativeWidget(self, attr): | 790 | def updateRelativeWidget(self, attr): |
| 755 | '''Called by _preUpdate() for each relativeWidget before each update''' | 791 | """Called by _preUpdate() for each relativeWidget before each update""" |
| 756 | oldUserValue = self.getOldAttr(attr) | 792 | oldUserValue = self.getOldAttr(attr) |
| 757 | newUserValue = self._trackedWidgets[attr].value() | 793 | newUserValue = self._trackedWidgets[attr].value() |
| 758 | newRelativeVal = self.floatValForAttr(attr, newUserValue) | 794 | newRelativeVal = self.floatValForAttr(attr, newUserValue) |
| 759 | 795 | ||
| 760 | if attr in self._relativeValues: | 796 | if attr in self._relativeValues: |
| 761 | oldRelativeVal = self._relativeValues[attr] | 797 | oldRelativeVal = self._relativeValues[attr] |
| 762 | if oldUserValue == newUserValue \ | 798 | if oldUserValue == newUserValue and oldRelativeVal != newRelativeVal: |
| 763 | and oldRelativeVal != newRelativeVal: | ||
| 764 | # Float changed without pixel value changing, which | 799 | # Float changed without pixel value changing, which |
| 765 | # means the pixel value needs to be updated | 800 | # means the pixel value needs to be updated |
| 766 | log.debug( | 801 | log.debug( |
| 767 | 'Updating %s #%s\'s relative widget: %s', | 802 | "Updating %s #%s's relative widget: %s", |
| 768 | self.__class__.name, self.compPos, attr) | 803 | self.__class__.name, |
| 804 | self.compPos, | ||
| 805 | attr, | ||
| 806 | ) | ||
| 769 | with blockSignals(self._trackedWidgets[attr]): | 807 | with blockSignals(self._trackedWidgets[attr]): |
| 770 | self.updateRelativeWidgetMaximum(attr) | 808 | self.updateRelativeWidgetMaximum(attr) |
| 771 | pixelVal = self.pixelValForAttr(attr, oldRelativeVal) | 809 | pixelVal = self.pixelValForAttr(attr, oldRelativeVal) |
| 772 | self._trackedWidgets[attr].setValue(pixelVal) | 810 | self._trackedWidgets[attr].setValue(pixelVal) |
| 773 | 811 | ||
| 774 | if attr not in self._relativeValues \ | 812 | if attr not in self._relativeValues or oldUserValue != newUserValue: |
| 775 | or oldUserValue != newUserValue: | ||
| 776 | self._relativeValues[attr] = newRelativeVal | 813 | self._relativeValues[attr] = newRelativeVal |
| 777 | 814 | ||
| 778 | def updateRelativeWidgetMaximum(self, attr): | 815 | def updateRelativeWidgetMaximum(self, attr): |
| 779 | maxRes = int(self.core.resolutions[0].split('x')[0]) | 816 | maxRes = int(self.core.resolutions[0].split("x")[0]) |
| 780 | newMaximumValue = self.width * ( | 817 | newMaximumValue = self.width * (self._relativeMaximums[attr] / maxRes) |
| 781 | self._relativeMaximums[attr] / | ||
| 782 | maxRes | ||
| 783 | ) | ||
| 784 | self._trackedWidgets[attr].setMaximum(int(newMaximumValue)) | 818 | self._trackedWidgets[attr].setMaximum(int(newMaximumValue)) |
| 785 | 819 | ||
| 786 | 820 | ||
| 787 | class ComponentError(RuntimeError): | 821 | class ComponentError(RuntimeError): |
| 788 | '''Gives the MainWindow a traceback to display, and cancels the export.''' | 822 | """Gives the MainWindow a traceback to display, and cancels the export.""" |
| 789 | 823 | ||
| 790 | prevErrors = [] | 824 | prevErrors = [] |
| 791 | lastTime = time.time() | 825 | lastTime = time.time() |
| @@ -794,42 +828,46 @@ class ComponentError(RuntimeError): | |||
| 794 | if msg is None and sys.exc_info()[0] is not None: | 828 | if msg is None and sys.exc_info()[0] is not None: |
| 795 | msg = str(sys.exc_info()[1]) | 829 | msg = str(sys.exc_info()[1]) |
| 796 | else: | 830 | else: |
| 797 | msg = 'Unknown error.' | 831 | msg = "Unknown error." |
| 798 | log.error("ComponentError by %s's %s: %s" % ( | 832 | log.error("ComponentError by %s's %s: %s" % (caller.name, name, msg)) |
| 799 | caller.name, name, msg)) | ||
| 800 | 833 | ||
| 801 | # Don't create multiple windows for quickly repeated messages | 834 | # Don't create multiple windows for quickly repeated messages |
| 802 | if len(ComponentError.prevErrors) > 1: | 835 | if len(ComponentError.prevErrors) > 1: |
| 803 | ComponentError.prevErrors.pop() | 836 | ComponentError.prevErrors.pop() |
| 804 | ComponentError.prevErrors.insert(0, name) | 837 | ComponentError.prevErrors.insert(0, name) |
| 805 | curTime = time.time() | 838 | curTime = time.time() |
| 806 | if name in ComponentError.prevErrors[1:] \ | 839 | if ( |
| 807 | and curTime - ComponentError.lastTime < 1.0: | 840 | name in ComponentError.prevErrors[1:] |
| 841 | and curTime - ComponentError.lastTime < 1.0 | ||
| 842 | ): | ||
| 808 | return | 843 | return |
| 809 | ComponentError.lastTime = time.time() | 844 | ComponentError.lastTime = time.time() |
| 810 | 845 | ||
| 811 | from .toolkit import formatTraceback | 846 | from .toolkit import formatTraceback |
| 847 | |||
| 812 | if sys.exc_info()[0] is not None: | 848 | if sys.exc_info()[0] is not None: |
| 813 | string = ( | 849 | string = "%s component (#%s): %s encountered %s %s: %s" % ( |
| 814 | "%s component (#%s): %s encountered %s %s: %s" % ( | 850 | caller.__class__.name, |
| 815 | caller.__class__.name, | 851 | str(caller.compPos), |
| 816 | str(caller.compPos), | 852 | name, |
| 817 | name, | 853 | ( |
| 818 | 'an' if any([ | 854 | "an" |
| 819 | sys.exc_info()[0].__name__.startswith(vowel) | 855 | if any( |
| 820 | for vowel in ('A', 'I', 'U', 'O', 'E') | 856 | [ |
| 821 | ]) else 'a', | 857 | sys.exc_info()[0].__name__.startswith(vowel) |
| 822 | sys.exc_info()[0].__name__, | 858 | for vowel in ("A", "I", "U", "O", "E") |
| 823 | str(sys.exc_info()[1]) | 859 | ] |
| 824 | ) | 860 | ) |
| 861 | else "a" | ||
| 862 | ), | ||
| 863 | sys.exc_info()[0].__name__, | ||
| 864 | str(sys.exc_info()[1]), | ||
| 825 | ) | 865 | ) |
| 826 | detail = formatTraceback(sys.exc_info()[2]) | 866 | detail = formatTraceback(sys.exc_info()[2]) |
| 827 | else: | 867 | else: |
| 828 | string = name | 868 | string = name |
| 829 | detail = "Attributes:\n%s" % ( | 869 | detail = "Attributes:\n%s" % ( |
| 830 | "\n".join( | 870 | "\n".join([m for m in dir(caller) if not m.startswith("_")]) |
| 831 | [m for m in dir(caller) if not m.startswith('_')] | ||
| 832 | ) | ||
| 833 | ) | 871 | ) |
| 834 | 872 | ||
| 835 | super().__init__(string) | 873 | super().__init__(string) |
| @@ -837,28 +875,29 @@ class ComponentError(RuntimeError): | |||
| 837 | caller._error.emit(string, detail) | 875 | caller._error.emit(string, detail) |
| 838 | 876 | ||
| 839 | 877 | ||
| 840 | class ComponentUpdate(QtWidgets.QUndoCommand): | 878 | class ComponentUpdate(QUndoCommand): |
| 841 | '''Command object for making a component action undoable''' | 879 | """Command object for making a component action undoable""" |
| 880 | |||
| 842 | def __init__(self, parent, oldWidgetVals, modifiedVals): | 881 | def __init__(self, parent, oldWidgetVals, modifiedVals): |
| 843 | super().__init__( | 882 | super().__init__("change %s component #%s" % (parent.name, parent.compPos)) |
| 844 | 'change %s component #%s' % ( | ||
| 845 | parent.name, parent.compPos | ||
| 846 | ) | ||
| 847 | ) | ||
| 848 | self.undone = False | 883 | self.undone = False |
| 849 | self.res = (int(parent.width), int(parent.height)) | 884 | self.res = (int(parent.width), int(parent.height)) |
| 850 | self.parent = parent | 885 | self.parent = parent |
| 851 | self.oldWidgetVals = { | 886 | self.oldWidgetVals = { |
| 852 | attr: copy(val) | 887 | attr: ( |
| 853 | if attr not in self.parent._relativeWidgets | 888 | copy(val) |
| 854 | else self.parent.floatValForAttr(attr, val, axis=self.res) | 889 | if attr not in self.parent._relativeWidgets |
| 890 | else self.parent.floatValForAttr(attr, val, axis=self.res) | ||
| 891 | ) | ||
| 855 | for attr, val in oldWidgetVals.items() | 892 | for attr, val in oldWidgetVals.items() |
| 856 | if attr in modifiedVals | 893 | if attr in modifiedVals |
| 857 | } | 894 | } |
| 858 | self.modifiedVals = { | 895 | self.modifiedVals = { |
| 859 | attr: val | 896 | attr: ( |
| 860 | if attr not in self.parent._relativeWidgets | 897 | val |
| 861 | else self.parent.floatValForAttr(attr, val, axis=self.res) | 898 | if attr not in self.parent._relativeWidgets |
| 899 | else self.parent.floatValForAttr(attr, val, axis=self.res) | ||
| 900 | ) | ||
| 862 | for attr, val in modifiedVals.items() | 901 | for attr, val in modifiedVals.items() |
| 863 | } | 902 | } |
| 864 | 903 | ||
| @@ -877,12 +916,13 @@ class ComponentUpdate(QtWidgets.QUndoCommand): | |||
| 877 | self.modifiedVals[attr] = val | 916 | self.modifiedVals[attr] = val |
| 878 | else: | 917 | else: |
| 879 | log.warning( | 918 | log.warning( |
| 880 | '%s component settings changed at once. (%s)', | 919 | "%s component settings changed at once. (%s)", |
| 881 | len(self.modifiedVals), repr(self.modifiedVals) | 920 | len(self.modifiedVals), |
| 921 | repr(self.modifiedVals), | ||
| 882 | ) | 922 | ) |
| 883 | 923 | ||
| 884 | def id(self): | 924 | def id(self): |
| 885 | '''If 2 consecutive updates have same id, Qt will call mergeWith()''' | 925 | """If 2 consecutive updates have same id, Qt will call mergeWith()""" |
| 886 | return self.id_ | 926 | return self.id_ |
| 887 | 927 | ||
| 888 | def mergeWith(self, other): | 928 | def mergeWith(self, other): |
| @@ -890,20 +930,23 @@ class ComponentUpdate(QtWidgets.QUndoCommand): | |||
| 890 | return True | 930 | return True |
| 891 | 931 | ||
| 892 | def setWidgetValues(self, attrDict): | 932 | def setWidgetValues(self, attrDict): |
| 893 | ''' | 933 | """ |
| 894 | Mask the component's usual method to handle our | 934 | Mask the component's usual method to handle our |
| 895 | relative widgets in case the resolution has changed. | 935 | relative widgets in case the resolution has changed. |
| 896 | ''' | 936 | """ |
| 897 | newAttrDict = { | 937 | newAttrDict = { |
| 898 | attr: val if attr not in self.parent._relativeWidgets | 938 | attr: ( |
| 899 | else self.parent.pixelValForAttr(attr, val) | 939 | val |
| 940 | if attr not in self.parent._relativeWidgets | ||
| 941 | else self.parent.pixelValForAttr(attr, val) | ||
| 942 | ) | ||
| 900 | for attr, val in attrDict.items() | 943 | for attr, val in attrDict.items() |
| 901 | } | 944 | } |
| 902 | self.parent.setWidgetValues(newAttrDict) | 945 | self.parent.setWidgetValues(newAttrDict) |
| 903 | 946 | ||
| 904 | def redo(self): | 947 | def redo(self): |
| 905 | if self.undone: | 948 | if self.undone: |
| 906 | log.info('Redoing component update') | 949 | log.info("Redoing component update") |
| 907 | self.parent.oldAttrs = self.relativeWidgetValsAfterUndo | 950 | self.parent.oldAttrs = self.relativeWidgetValsAfterUndo |
| 908 | self.setWidgetValues(self.modifiedVals) | 951 | self.setWidgetValues(self.modifiedVals) |
| 909 | self.parent.update(auto=True) | 952 | self.parent.update(auto=True) |
| @@ -916,7 +959,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): | |||
| 916 | self.parent._sendUpdateSignal() | 959 | self.parent._sendUpdateSignal() |
| 917 | 960 | ||
| 918 | def undo(self): | 961 | def undo(self): |
| 919 | log.info('Undoing component update') | 962 | log.info("Undoing component update") |
| 920 | self.undone = True | 963 | self.undone = True |
| 921 | self.parent.oldAttrs = self.relativeWidgetValsAfterRedo | 964 | self.parent.oldAttrs = self.relativeWidgetValsAfterRedo |
| 922 | self.setWidgetValues(self.oldWidgetVals) | 965 | self.setWidgetValues(self.oldWidgetVals) |
diff --git a/src/components/color.py b/src/components/color.py index 8d0edd2..1f32c23 100644 --- a/src/components/color.py +++ b/src/components/color.py | |||
| @@ -1,16 +1,16 @@ | |||
| 1 | from PyQt5 import QtGui | 1 | from PyQt6 import QtGui |
| 2 | import logging | 2 | import logging |
| 3 | 3 | ||
| 4 | from ..component import Component | 4 | from ..component import Component |
| 5 | from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor | 5 | from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | log = logging.getLogger('AVP.Components.Color') | 8 | log = logging.getLogger("AVP.Components.Color") |
| 9 | 9 | ||
| 10 | 10 | ||
| 11 | class Component(Component): | 11 | class Component(Component): |
| 12 | name = 'Color' | 12 | name = "Color" |
| 13 | version = '1.0.0' | 13 | version = "1.0.0" |
| 14 | 14 | ||
| 15 | def widget(self, *args): | 15 | def widget(self, *args): |
| 16 | self.x = 0 | 16 | self.x = 0 |
| @@ -20,48 +20,56 @@ class Component(Component): | |||
| 20 | # disable color #2 until non-default 'fill' option gets changed | 20 | # disable color #2 until non-default 'fill' option gets changed |
| 21 | self.page.lineEdit_color2.setDisabled(True) | 21 | self.page.lineEdit_color2.setDisabled(True) |
| 22 | self.page.pushButton_color2.setDisabled(True) | 22 | self.page.pushButton_color2.setDisabled(True) |
| 23 | self.page.spinBox_width.setValue( | 23 | self.page.spinBox_width.setValue(int(self.settings.value("outputWidth"))) |
| 24 | int(self.settings.value("outputWidth"))) | 24 | self.page.spinBox_height.setValue(int(self.settings.value("outputHeight"))) |
| 25 | self.page.spinBox_height.setValue( | ||
| 26 | int(self.settings.value("outputHeight"))) | ||
| 27 | 25 | ||
| 28 | self.fillLabels = [ | 26 | self.fillLabels = [ |
| 29 | 'Solid', | 27 | "Solid", |
| 30 | 'Linear Gradient', | 28 | "Linear Gradient", |
| 31 | 'Radial Gradient', | 29 | "Radial Gradient", |
| 32 | ] | 30 | ] |
| 33 | for label in self.fillLabels: | 31 | for label in self.fillLabels: |
| 34 | self.page.comboBox_fill.addItem(label) | 32 | self.page.comboBox_fill.addItem(label) |
| 35 | self.page.comboBox_fill.setCurrentIndex(0) | 33 | self.page.comboBox_fill.setCurrentIndex(0) |
| 36 | 34 | ||
| 37 | self.trackWidgets({ | 35 | self.trackWidgets( |
| 38 | 'x': self.page.spinBox_x, | 36 | { |
| 39 | 'y': self.page.spinBox_y, | 37 | "x": self.page.spinBox_x, |
| 40 | 'sizeWidth': self.page.spinBox_width, | 38 | "y": self.page.spinBox_y, |
| 41 | 'sizeHeight': self.page.spinBox_height, | 39 | "sizeWidth": self.page.spinBox_width, |
| 42 | 'trans': self.page.checkBox_trans, | 40 | "sizeHeight": self.page.spinBox_height, |
| 43 | 'spread': self.page.comboBox_spread, | 41 | "trans": self.page.checkBox_trans, |
| 44 | 'stretch': self.page.checkBox_stretch, | 42 | "spread": self.page.comboBox_spread, |
| 45 | 'RG_start': self.page.spinBox_radialGradient_start, | 43 | "stretch": self.page.checkBox_stretch, |
| 46 | 'LG_start': self.page.spinBox_linearGradient_start, | 44 | "RG_start": self.page.spinBox_radialGradient_start, |
| 47 | 'RG_end': self.page.spinBox_radialGradient_end, | 45 | "LG_start": self.page.spinBox_linearGradient_start, |
| 48 | 'LG_end': self.page.spinBox_linearGradient_end, | 46 | "RG_end": self.page.spinBox_radialGradient_end, |
| 49 | 'RG_centre': self.page.spinBox_radialGradient_spread, | 47 | "LG_end": self.page.spinBox_linearGradient_end, |
| 50 | 'fillType': self.page.comboBox_fill, | 48 | "RG_centre": self.page.spinBox_radialGradient_spread, |
| 51 | 'color1': self.page.lineEdit_color1, | 49 | "fillType": self.page.comboBox_fill, |
| 52 | 'color2': self.page.lineEdit_color2, | 50 | "color1": self.page.lineEdit_color1, |
| 53 | }, presetNames={ | 51 | "color2": self.page.lineEdit_color2, |
| 54 | 'sizeWidth': 'width', | 52 | }, |
| 55 | 'sizeHeight': 'height', | 53 | presetNames={ |
| 56 | }, colorWidgets={ | 54 | "sizeWidth": "width", |
| 57 | 'color1': self.page.pushButton_color1, | 55 | "sizeHeight": "height", |
| 58 | 'color2': self.page.pushButton_color2, | 56 | }, |
| 59 | }, relativeWidgets=[ | 57 | colorWidgets={ |
| 60 | 'x', 'y', | 58 | "color1": self.page.pushButton_color1, |
| 61 | 'sizeWidth', 'sizeHeight', | 59 | "color2": self.page.pushButton_color2, |
| 62 | 'LG_start', 'LG_end', | 60 | }, |
| 63 | 'RG_start', 'RG_end', 'RG_centre', | 61 | relativeWidgets=[ |
| 64 | ]) | 62 | "x", |
| 63 | "y", | ||
| 64 | "sizeWidth", | ||
| 65 | "sizeHeight", | ||
| 66 | "LG_start", | ||
| 67 | "LG_end", | ||
| 68 | "RG_start", | ||
| 69 | "RG_end", | ||
| 70 | "RG_centre", | ||
| 71 | ], | ||
| 72 | ) | ||
| 65 | 73 | ||
| 66 | def update(self): | 74 | def update(self): |
| 67 | fillType = self.page.comboBox_fill.currentIndex() | 75 | fillType = self.page.comboBox_fill.currentIndex() |
| @@ -86,7 +94,7 @@ class Component(Component): | |||
| 86 | return self.drawFrame(self.width, self.height) | 94 | return self.drawFrame(self.width, self.height) |
| 87 | 95 | ||
| 88 | def properties(self): | 96 | def properties(self): |
| 89 | return ['static'] | 97 | return ["static"] |
| 90 | 98 | ||
| 91 | def frameRender(self, frameNo): | 99 | def frameRender(self, frameNo): |
| 92 | log.debug("Color component is drawing frame #%s", frameNo) | 100 | log.debug("Color component is drawing frame #%s", frameNo) |
| @@ -96,8 +104,12 @@ class Component(Component): | |||
| 96 | r, g, b = self.color1 | 104 | r, g, b = self.color1 |
| 97 | shapeSize = (self.sizeWidth, self.sizeHeight) | 105 | shapeSize = (self.sizeWidth, self.sizeHeight) |
| 98 | # in default state, skip all this logic and return a plain fill | 106 | # in default state, skip all this logic and return a plain fill |
| 99 | if self.fillType == 0 and shapeSize == (width, height) \ | 107 | if ( |
| 100 | and self.x == 0 and self.y == 0: | 108 | self.fillType == 0 |
| 109 | and shapeSize == (width, height) | ||
| 110 | and self.x == 0 | ||
| 111 | and self.y == 0 | ||
| 112 | ): | ||
| 101 | return FloodFrame(width, height, (r, g, b, 255)) | 113 | return FloodFrame(width, height, (r, g, b, 255)) |
| 102 | 114 | ||
| 103 | # Return a solid image at x, y | 115 | # Return a solid image at x, y |
| @@ -120,19 +132,26 @@ class Component(Component): | |||
| 120 | 132 | ||
| 121 | if self.fillType == 1: # Linear Gradient | 133 | if self.fillType == 1: # Linear Gradient |
| 122 | brush = QtGui.QLinearGradient( | 134 | brush = QtGui.QLinearGradient( |
| 123 | self.LG_start, | 135 | float(self.LG_start), |
| 124 | self.LG_start, | 136 | float(self.LG_start), |
| 125 | self.LG_end+width/3, | 137 | float(self.LG_end + width / 3), |
| 126 | self.LG_end) | 138 | float(self.LG_end), |
| 139 | ) | ||
| 127 | 140 | ||
| 128 | elif self.fillType == 2: # Radial Gradient | 141 | elif self.fillType == 2: # Radial Gradient |
| 129 | brush = QtGui.QRadialGradient( | 142 | brush = QtGui.QRadialGradient( |
| 130 | self.RG_start, | 143 | float(self.RG_start), |
| 131 | self.RG_end, | 144 | float(self.RG_end), |
| 132 | w, h, | 145 | float(w), |
| 133 | self.RG_centre) | 146 | float(h), |
| 134 | 147 | float(self.RG_centre), | |
| 135 | brush.setSpread(self.spread) | 148 | ) |
| 149 | spread = QtGui.QGradient.Spread.PadSpread | ||
| 150 | if self.spread == 1: | ||
| 151 | spread = QtGui.QGradient.Spread.ReflectSpread | ||
| 152 | elif self.spread == 2: | ||
| 153 | spread = QtGui.QGradient.Spread.RepeatSpread | ||
| 154 | brush.setSpread(spread) | ||
| 136 | brush.setColorAt(0.0, PaintColor(*self.color1)) | 155 | brush.setColorAt(0.0, PaintColor(*self.color1)) |
| 137 | if self.trans: | 156 | if self.trans: |
| 138 | brush.setColorAt(1.0, PaintColor(0, 0, 0, 0)) | 157 | brush.setColorAt(1.0, PaintColor(0, 0, 0, 0)) |
| @@ -141,20 +160,17 @@ class Component(Component): | |||
| 141 | else: | 160 | else: |
| 142 | brush.setColorAt(1.0, PaintColor(*self.color2)) | 161 | brush.setColorAt(1.0, PaintColor(*self.color2)) |
| 143 | image.setBrush(brush) | 162 | image.setBrush(brush) |
| 144 | image.drawRect( | 163 | image.drawRect(self.x, self.y, self.sizeWidth, self.sizeHeight) |
| 145 | self.x, self.y, | ||
| 146 | self.sizeWidth, self.sizeHeight | ||
| 147 | ) | ||
| 148 | 164 | ||
| 149 | return image.finalize() | 165 | return image.finalize() |
| 150 | 166 | ||
| 151 | def commandHelp(self): | 167 | def commandHelp(self): |
| 152 | print('Specify a color:\n color=255,255,255') | 168 | print("Specify a color:\n color=255,255,255") |
| 153 | 169 | ||
| 154 | def command(self, arg): | 170 | def command(self, arg): |
| 155 | if '=' in arg: | 171 | if "=" in arg: |
| 156 | key, arg = arg.split('=', 1) | 172 | key, arg = arg.split("=", 1) |
| 157 | if key == 'color': | 173 | if key == "color": |
| 158 | self.page.lineEdit_color1.setText(arg) | 174 | self.page.lineEdit_color1.setText(arg) |
| 159 | return | 175 | return |
| 160 | super().command(arg) | 176 | super().command(arg) |
diff --git a/src/components/image.py b/src/components/image.py index 42f9564..2393611 100644 --- a/src/components/image.py +++ b/src/components/image.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | from PIL import Image, ImageDraw, ImageEnhance | 1 | from PIL import Image, ImageDraw, ImageEnhance |
| 2 | from PyQt5 import QtGui, QtCore, QtWidgets | 2 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 3 | import os | 3 | import os |
| 4 | 4 | ||
| 5 | from ..component import Component | 5 | from ..component import Component |
| @@ -7,37 +7,39 @@ from ..toolkit.frame import BlankFrame | |||
| 7 | 7 | ||
| 8 | 8 | ||
| 9 | class Component(Component): | 9 | class Component(Component): |
| 10 | name = 'Image' | 10 | name = "Image" |
| 11 | version = '1.0.1' | 11 | version = "1.0.1" |
| 12 | 12 | ||
| 13 | def widget(self, *args): | 13 | def widget(self, *args): |
| 14 | super().widget(*args) | 14 | super().widget(*args) |
| 15 | self.page.pushButton_image.clicked.connect(self.pickImage) | 15 | self.page.pushButton_image.clicked.connect(self.pickImage) |
| 16 | self.trackWidgets({ | 16 | self.trackWidgets( |
| 17 | 'imagePath': self.page.lineEdit_image, | 17 | { |
| 18 | 'scale': self.page.spinBox_scale, | 18 | "imagePath": self.page.lineEdit_image, |
| 19 | 'stretchScale': self.page.spinBox_scale_stretch, | 19 | "scale": self.page.spinBox_scale, |
| 20 | 'rotate': self.page.spinBox_rotate, | 20 | "stretchScale": self.page.spinBox_scale_stretch, |
| 21 | 'color': self.page.spinBox_color, | 21 | "rotate": self.page.spinBox_rotate, |
| 22 | 'xPosition': self.page.spinBox_x, | 22 | "color": self.page.spinBox_color, |
| 23 | 'yPosition': self.page.spinBox_y, | 23 | "xPosition": self.page.spinBox_x, |
| 24 | 'stretched': self.page.checkBox_stretch, | 24 | "yPosition": self.page.spinBox_y, |
| 25 | 'mirror': self.page.checkBox_mirror, | 25 | "stretched": self.page.checkBox_stretch, |
| 26 | }, presetNames={ | 26 | "mirror": self.page.checkBox_mirror, |
| 27 | 'imagePath': 'image', | 27 | }, |
| 28 | 'xPosition': 'x', | 28 | presetNames={ |
| 29 | 'yPosition': 'y', | 29 | "imagePath": "image", |
| 30 | }, relativeWidgets=[ | 30 | "xPosition": "x", |
| 31 | 'xPosition', 'yPosition', 'scale' | 31 | "yPosition": "y", |
| 32 | ]) | 32 | }, |
| 33 | relativeWidgets=["xPosition", "yPosition", "scale"], | ||
| 34 | ) | ||
| 33 | 35 | ||
| 34 | def previewRender(self): | 36 | def previewRender(self): |
| 35 | return self.drawFrame(self.width, self.height) | 37 | return self.drawFrame(self.width, self.height) |
| 36 | 38 | ||
| 37 | def properties(self): | 39 | def properties(self): |
| 38 | props = ['static'] | 40 | props = ["static"] |
| 39 | if not os.path.exists(self.imagePath): | 41 | if not os.path.exists(self.imagePath): |
| 40 | props.append('error') | 42 | props.append("error") |
| 41 | return props | 43 | return props |
| 42 | 44 | ||
| 43 | def error(self): | 45 | def error(self): |
| @@ -57,17 +59,15 @@ class Component(Component): | |||
| 57 | 59 | ||
| 58 | # Modify image's appearance | 60 | # Modify image's appearance |
| 59 | if self.color != 100: | 61 | if self.color != 100: |
| 60 | image = ImageEnhance.Color(image).enhance( | 62 | image = ImageEnhance.Color(image).enhance(float(self.color / 100)) |
| 61 | float(self.color / 100) | ||
| 62 | ) | ||
| 63 | if self.mirror: | 63 | if self.mirror: |
| 64 | image = image.transpose(Image.FLIP_LEFT_RIGHT) | 64 | image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) |
| 65 | if self.stretched and image.size != (width, height): | 65 | if self.stretched and image.size != (width, height): |
| 66 | image = image.resize((width, height), Image.ANTIALIAS) | 66 | image = image.resize((width, height), Image.Resampling.LANCZOS) |
| 67 | if scale != 100: | 67 | if scale != 100: |
| 68 | newHeight = int((image.height / 100) * scale) | 68 | newHeight = int((image.height / 100) * scale) |
| 69 | newWidth = int((image.width / 100) * scale) | 69 | newWidth = int((image.width / 100) * scale) |
| 70 | image = image.resize((newWidth, newHeight), Image.ANTIALIAS) | 70 | image = image.resize((newWidth, newHeight), Image.Resampling.LANCZOS) |
| 71 | 71 | ||
| 72 | # Paste image at correct position | 72 | # Paste image at correct position |
| 73 | frame.paste(image, box=(self.xPosition, self.yPosition)) | 73 | frame.paste(image, box=(self.xPosition, self.yPosition)) |
| @@ -79,8 +79,11 @@ class Component(Component): | |||
| 79 | def pickImage(self): | 79 | def pickImage(self): |
| 80 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) | 80 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) |
| 81 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 81 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 82 | self.page, "Choose Image", imgDir, | 82 | self.page, |
| 83 | "Image Files (%s)" % " ".join(self.core.imageFormats)) | 83 | "Choose Image", |
| 84 | imgDir, | ||
| 85 | "Image Files (%s)" % " ".join(self.core.imageFormats), | ||
| 86 | ) | ||
| 84 | if filename: | 87 | if filename: |
| 85 | self.settings.setValue("componentDir", os.path.dirname(filename)) | 88 | self.settings.setValue("componentDir", os.path.dirname(filename)) |
| 86 | self.mergeUndo = False | 89 | self.mergeUndo = False |
| @@ -88,9 +91,9 @@ class Component(Component): | |||
| 88 | self.mergeUndo = True | 91 | self.mergeUndo = True |
| 89 | 92 | ||
| 90 | def command(self, arg): | 93 | def command(self, arg): |
| 91 | if '=' in arg: | 94 | if "=" in arg: |
| 92 | key, arg = arg.split('=', 1) | 95 | key, arg = arg.split("=", 1) |
| 93 | if key == 'path' and os.path.exists(arg): | 96 | if key == "path" and os.path.exists(arg): |
| 94 | try: | 97 | try: |
| 95 | Image.open(arg) | 98 | Image.open(arg) |
| 96 | self.page.lineEdit_image.setText(arg) | 99 | self.page.lineEdit_image.setText(arg) |
| @@ -102,7 +105,7 @@ class Component(Component): | |||
| 102 | super().command(arg) | 105 | super().command(arg) |
| 103 | 106 | ||
| 104 | def commandHelp(self): | 107 | def commandHelp(self): |
| 105 | print('Load an image:\n path=/filepath/to/image.png') | 108 | print("Load an image:\n path=/filepath/to/image.png") |
| 106 | 109 | ||
| 107 | def savePreset(self): | 110 | def savePreset(self): |
| 108 | # Maintain the illusion that the scale spinbox is one widget | 111 | # Maintain the illusion that the scale spinbox is one widget |
diff --git a/src/components/life.py b/src/components/life.py index e19ed36..b3c2c58 100644 --- a/src/components/life.py +++ b/src/components/life.py | |||
| @@ -1,16 +1,21 @@ | |||
| 1 | from PyQt5 import QtGui, QtCore, QtWidgets | 1 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 2 | from PyQt5.QtWidgets import QUndoCommand | 2 | from PyQt6.QtGui import QUndoCommand |
| 3 | from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter | 3 | from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter |
| 4 | import os | 4 | import os |
| 5 | import math | 5 | import math |
| 6 | import logging | ||
| 7 | |||
| 6 | 8 | ||
| 7 | from ..component import Component | 9 | from ..component import Component |
| 8 | from ..toolkit.frame import BlankFrame, scale | 10 | from ..toolkit.frame import BlankFrame, scale |
| 9 | 11 | ||
| 10 | 12 | ||
| 13 | log = logging.getLogger("AVP.Component.Life") | ||
| 14 | |||
| 15 | |||
| 11 | class Component(Component): | 16 | class Component(Component): |
| 12 | name = 'Conway\'s Game of Life' | 17 | name = "Conway's Game of Life" |
| 13 | version = '1.0.0' | 18 | version = "1.0.0" |
| 14 | 19 | ||
| 15 | def widget(self, *args): | 20 | def widget(self, *args): |
| 16 | super().widget(*args) | 21 | super().widget(*args) |
| @@ -18,34 +23,50 @@ class Component(Component): | |||
| 18 | self.updateGridSize() | 23 | self.updateGridSize() |
| 19 | # The initial grid: a "Queen Bee Shuttle" | 24 | # The initial grid: a "Queen Bee Shuttle" |
| 20 | # https://conwaylife.com/wiki/Queen_bee_shuttle | 25 | # https://conwaylife.com/wiki/Queen_bee_shuttle |
| 21 | self.startingGrid = set([ | 26 | self.startingGrid = set( |
| 22 | (3, 7), (3, 8), | 27 | [ |
| 23 | (4, 7), (4, 8), | 28 | (3, 7), |
| 24 | (8, 7), | 29 | (3, 8), |
| 25 | (9, 6), (9, 8), | 30 | (4, 7), |
| 26 | (10, 5), (10, 9), | 31 | (4, 8), |
| 27 | (11, 6), (11, 7), (11, 8), | 32 | (8, 7), |
| 28 | (12, 4), (12, 5), (12, 9), (12, 10), | 33 | (9, 6), |
| 29 | (23, 6), (23, 7), | 34 | (9, 8), |
| 30 | (24, 6), (24, 7) | 35 | (10, 5), |
| 31 | ]) | 36 | (10, 9), |
| 37 | (11, 6), | ||
| 38 | (11, 7), | ||
| 39 | (11, 8), | ||
| 40 | (12, 4), | ||
| 41 | (12, 5), | ||
| 42 | (12, 9), | ||
| 43 | (12, 10), | ||
| 44 | (23, 6), | ||
| 45 | (23, 7), | ||
| 46 | (24, 6), | ||
| 47 | (24, 7), | ||
| 48 | ] | ||
| 49 | ) | ||
| 32 | 50 | ||
| 33 | # Amount of 'bleed' (off-canvas coordinates) on each side of the grid | 51 | # Amount of 'bleed' (off-canvas coordinates) on each side of the grid |
| 34 | self.bleedSize = 40 | 52 | self.bleedSize = 40 |
| 35 | 53 | ||
| 36 | self.page.pushButton_pickImage.clicked.connect(self.pickImage) | 54 | self.page.pushButton_pickImage.clicked.connect(self.pickImage) |
| 37 | self.trackWidgets({ | 55 | self.trackWidgets( |
| 38 | 'tickRate': self.page.spinBox_tickRate, | 56 | { |
| 39 | 'scale': self.page.spinBox_scale, | 57 | "tickRate": self.page.spinBox_tickRate, |
| 40 | 'color': self.page.lineEdit_color, | 58 | "scale": self.page.spinBox_scale, |
| 41 | 'shapeType': self.page.comboBox_shapeType, | 59 | "color": self.page.lineEdit_color, |
| 42 | 'shadow': self.page.checkBox_shadow, | 60 | "shapeType": self.page.comboBox_shapeType, |
| 43 | 'customImg': self.page.checkBox_customImg, | 61 | "shadow": self.page.checkBox_shadow, |
| 44 | 'showGrid': self.page.checkBox_showGrid, | 62 | "customImg": self.page.checkBox_customImg, |
| 45 | 'image': self.page.lineEdit_image, | 63 | "showGrid": self.page.checkBox_showGrid, |
| 46 | }, colorWidgets={ | 64 | "image": self.page.lineEdit_image, |
| 47 | 'color': self.page.pushButton_color, | 65 | }, |
| 48 | }) | 66 | colorWidgets={ |
| 67 | "color": self.page.pushButton_color, | ||
| 68 | }, | ||
| 69 | ) | ||
| 49 | self.shiftButtons = ( | 70 | self.shiftButtons = ( |
| 50 | self.page.toolButton_up, | 71 | self.page.toolButton_up, |
| 51 | self.page.toolButton_down, | 72 | self.page.toolButton_down, |
| @@ -56,7 +77,9 @@ class Component(Component): | |||
| 56 | def shiftFunc(i): | 77 | def shiftFunc(i): |
| 57 | def shift(): | 78 | def shift(): |
| 58 | self.shiftGrid(i) | 79 | self.shiftGrid(i) |
| 80 | |||
| 59 | return shift | 81 | return shift |
| 82 | |||
| 60 | shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))] | 83 | shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))] |
| 61 | for i, widget in enumerate(self.shiftButtons): | 84 | for i, widget in enumerate(self.shiftButtons): |
| 62 | widget.clicked.connect(shiftFuncs[i]) | 85 | widget.clicked.connect(shiftFuncs[i]) |
| @@ -66,8 +89,11 @@ class Component(Component): | |||
| 66 | def pickImage(self): | 89 | def pickImage(self): |
| 67 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) | 90 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) |
| 68 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 91 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 69 | self.page, "Choose Image", imgDir, | 92 | self.page, |
| 70 | "Image Files (%s)" % " ".join(self.core.imageFormats)) | 93 | "Choose Image", |
| 94 | imgDir, | ||
| 95 | "Image Files (%s)" % " ".join(self.core.imageFormats), | ||
| 96 | ) | ||
| 71 | if filename: | 97 | if filename: |
| 72 | self.settings.setValue("componentDir", os.path.dirname(filename)) | 98 | self.settings.setValue("componentDir", os.path.dirname(filename)) |
| 73 | self.mergeUndo = False | 99 | self.mergeUndo = False |
| @@ -98,20 +124,20 @@ class Component(Component): | |||
| 98 | self.page.label_image.setVisible(False) | 124 | self.page.label_image.setVisible(False) |
| 99 | self.page.lineEdit_image.setVisible(False) | 125 | self.page.lineEdit_image.setVisible(False) |
| 100 | self.page.pushButton_pickImage.setVisible(False) | 126 | self.page.pushButton_pickImage.setVisible(False) |
| 101 | enabled = (len(self.startingGrid) > 0) | 127 | enabled = len(self.startingGrid) > 0 |
| 102 | for widget in self.shiftButtons: | 128 | for widget in self.shiftButtons: |
| 103 | widget.setEnabled(enabled) | 129 | widget.setEnabled(enabled) |
| 104 | 130 | ||
| 105 | def previewClickEvent(self, pos, size, button): | 131 | def previewClickEvent(self, pos, size, button): |
| 106 | pos = ( | 132 | pos = ( |
| 107 | math.ceil((pos[0] / size[0]) * self.gridWidth) - 1, | 133 | math.ceil((pos[0] / size[0]) * self.gridWidth) - 1, |
| 108 | math.ceil((pos[1] / size[1]) * self.gridHeight) - 1 | 134 | math.ceil((pos[1] / size[1]) * self.gridHeight) - 1, |
| 109 | ) | 135 | ) |
| 110 | action = ClickGrid(self, pos, button) | 136 | action = ClickGrid(self, pos, button) |
| 111 | self.parent.undoStack.push(action) | 137 | self.parent.undoStack.push(action) |
| 112 | 138 | ||
| 113 | def updateGridSize(self): | 139 | def updateGridSize(self): |
| 114 | w, h = self.core.resolutions[-1].split('x') | 140 | w, h = self.core.resolutions[-1].split("x") |
| 115 | self.gridWidth = int(int(w) / self.scale) | 141 | self.gridWidth = int(int(w) / self.scale) |
| 116 | self.gridHeight = int(int(h) / self.scale) | 142 | self.gridHeight = int(int(h) / self.scale) |
| 117 | self.pxWidth = math.ceil(self.width / self.gridWidth) | 143 | self.pxWidth = math.ceil(self.width / self.gridWidth) |
| @@ -125,10 +151,8 @@ class Component(Component): | |||
| 125 | self.tickGrids = {0: self.startingGrid} | 151 | self.tickGrids = {0: self.startingGrid} |
| 126 | 152 | ||
| 127 | def properties(self): | 153 | def properties(self): |
| 128 | if self.customImg and ( | 154 | if self.customImg and (not self.image or not os.path.exists(self.image)): |
| 129 | not self.image or not os.path.exists(self.image) | 155 | return ["error"] |
| 130 | ): | ||
| 131 | return ['error'] | ||
| 132 | return [] | 156 | return [] |
| 133 | 157 | ||
| 134 | def error(self): | 158 | def error(self): |
| @@ -162,42 +186,47 @@ class Component(Component): | |||
| 162 | drawer = ImageDraw.Draw(frame) | 186 | drawer = ImageDraw.Draw(frame) |
| 163 | rect = ( | 187 | rect = ( |
| 164 | (drawPtX, drawPtY), | 188 | (drawPtX, drawPtY), |
| 165 | (drawPtX + self.pxWidth, drawPtY + self.pxHeight) | 189 | (drawPtX + self.pxWidth, drawPtY + self.pxHeight), |
| 166 | ) | 190 | ) |
| 167 | shape = self.page.comboBox_shapeType.currentText().lower() | 191 | shape = self.page.comboBox_shapeType.currentText().lower() |
| 168 | 192 | ||
| 169 | # Rectangle | 193 | # Rectangle |
| 170 | if shape == 'rectangle': | 194 | if shape == "rectangle": |
| 171 | drawer.rectangle(rect, fill=self.color) | 195 | drawer.rectangle(rect, fill=self.color) |
| 172 | 196 | ||
| 173 | # Elliptical | 197 | # Elliptical |
| 174 | elif shape == 'elliptical': | 198 | elif shape == "elliptical": |
| 175 | drawer.ellipse(rect, fill=self.color) | 199 | drawer.ellipse(rect, fill=self.color) |
| 176 | 200 | ||
| 177 | tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int) | 201 | tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int) |
| 178 | smallerShape = ( | 202 | smallerShape = ( |
| 179 | (drawPtX + tenthX + int(tenthX / 4), | 203 | ( |
| 180 | drawPtY + tenthY + int(tenthY / 2)), | 204 | drawPtX + tenthX + int(tenthX / 4), |
| 181 | (drawPtX + self.pxWidth - tenthX - int(tenthX / 4), | 205 | drawPtY + tenthY + int(tenthY / 2), |
| 182 | drawPtY + self.pxHeight - (tenthY + int(tenthY / 2))) | 206 | ), |
| 207 | ( | ||
| 208 | drawPtX + self.pxWidth - tenthX - int(tenthX / 4), | ||
| 209 | drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)), | ||
| 210 | ), | ||
| 183 | ) | 211 | ) |
| 184 | outlineShape = ( | 212 | outlineShape = ( |
| 185 | (drawPtX + int(tenthX / 4), | 213 | (drawPtX + int(tenthX / 4), drawPtY + int(tenthY / 2)), |
| 186 | drawPtY + int(tenthY / 2)), | 214 | ( |
| 187 | (drawPtX + self.pxWidth - int(tenthX / 4), | 215 | drawPtX + self.pxWidth - int(tenthX / 4), |
| 188 | drawPtY + self.pxHeight - int(tenthY / 2)) | 216 | drawPtY + self.pxHeight - int(tenthY / 2), |
| 217 | ), | ||
| 189 | ) | 218 | ) |
| 190 | # Circle | 219 | # Circle |
| 191 | if shape == 'circle': | 220 | if shape == "circle": |
| 192 | drawer.ellipse(outlineShape, fill=self.color) | 221 | drawer.ellipse(outlineShape, fill=self.color) |
| 193 | drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) | 222 | drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) |
| 194 | 223 | ||
| 195 | # Lilypad | 224 | # Lilypad |
| 196 | elif shape == 'lilypad': | 225 | elif shape == "lilypad": |
| 197 | drawer.pieslice(smallerShape, 290, 250, fill=self.color) | 226 | drawer.pieslice(smallerShape, 290, 250, fill=self.color) |
| 198 | 227 | ||
| 199 | # Pie | 228 | # Pie |
| 200 | elif shape == 'pie': | 229 | elif shape == "pie": |
| 201 | drawer.pieslice(outlineShape, 35, 320, fill=self.color) | 230 | drawer.pieslice(outlineShape, 35, 320, fill=self.color) |
| 202 | 231 | ||
| 203 | hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline | 232 | hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline |
| @@ -205,12 +234,15 @@ class Component(Component): | |||
| 205 | qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline | 234 | qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline |
| 206 | 235 | ||
| 207 | # Path | 236 | # Path |
| 208 | if shape == 'path': | 237 | if shape == "path": |
| 209 | drawer.ellipse(rect, fill=self.color) | 238 | drawer.ellipse(rect, fill=self.color) |
| 210 | rects = { | 239 | rects = { |
| 211 | direction: False | 240 | direction: False |
| 212 | for direction in ( | 241 | for direction in ( |
| 213 | 'up', 'down', 'left', 'right', | 242 | "up", |
| 243 | "down", | ||
| 244 | "left", | ||
| 245 | "right", | ||
| 214 | ) | 246 | ) |
| 215 | } | 247 | } |
| 216 | for cell in self.nearbyCoords(x, y): | 248 | for cell in self.nearbyCoords(x, y): |
| @@ -218,60 +250,59 @@ class Component(Component): | |||
| 218 | continue | 250 | continue |
| 219 | if cell[0] == x: | 251 | if cell[0] == x: |
| 220 | if cell[1] < y: | 252 | if cell[1] < y: |
| 221 | rects['up'] = True | 253 | rects["up"] = True |
| 222 | if cell[1] > y: | 254 | if cell[1] > y: |
| 223 | rects['down'] = True | 255 | rects["down"] = True |
| 224 | if cell[1] == y: | 256 | if cell[1] == y: |
| 225 | if cell[0] < x: | 257 | if cell[0] < x: |
| 226 | rects['left'] = True | 258 | rects["left"] = True |
| 227 | if cell[0] > x: | 259 | if cell[0] > x: |
| 228 | rects['right'] = True | 260 | rects["right"] = True |
| 229 | 261 | ||
| 230 | for direction, rect in rects.items(): | 262 | for direction, rect in rects.items(): |
| 231 | if rect: | 263 | if rect: |
| 232 | if direction == 'up': | 264 | if direction == "up": |
| 233 | sect = ( | 265 | sect = ( |
| 234 | (drawPtX, drawPtY), | 266 | (drawPtX, drawPtY), |
| 235 | (drawPtX + self.pxWidth, drawPtY + hY) | 267 | (drawPtX + self.pxWidth, drawPtY + hY), |
| 236 | ) | 268 | ) |
| 237 | elif direction == 'down': | 269 | elif direction == "down": |
| 238 | sect = ( | 270 | sect = ( |
| 239 | (drawPtX, drawPtY + hY), | 271 | (drawPtX, drawPtY + hY), |
| 240 | (drawPtX + self.pxWidth, | 272 | ( |
| 241 | drawPtY + self.pxHeight) | 273 | drawPtX + self.pxWidth, |
| 274 | drawPtY + self.pxHeight, | ||
| 275 | ), | ||
| 242 | ) | 276 | ) |
| 243 | elif direction == 'left': | 277 | elif direction == "left": |
| 244 | sect = ( | 278 | sect = ( |
| 245 | (drawPtX, drawPtY), | 279 | (drawPtX, drawPtY), |
| 246 | (drawPtX + hX, | 280 | (drawPtX + hX, drawPtY + self.pxHeight), |
| 247 | drawPtY + self.pxHeight) | ||
| 248 | ) | 281 | ) |
| 249 | elif direction == 'right': | 282 | elif direction == "right": |
| 250 | sect = ( | 283 | sect = ( |
| 251 | (drawPtX + hX, drawPtY), | 284 | (drawPtX + hX, drawPtY), |
| 252 | (drawPtX + self.pxWidth, | 285 | ( |
| 253 | drawPtY + self.pxHeight) | 286 | drawPtX + self.pxWidth, |
| 287 | drawPtY + self.pxHeight, | ||
| 288 | ), | ||
| 254 | ) | 289 | ) |
| 255 | drawer.rectangle(sect, fill=self.color) | 290 | drawer.rectangle(sect, fill=self.color) |
| 256 | 291 | ||
| 257 | # Duck | 292 | # Duck |
| 258 | elif shape == 'duck': | 293 | elif shape == "duck": |
| 259 | duckHead = ( | 294 | duckHead = ( |
| 260 | (drawPtX + qX, drawPtY + qY), | 295 | (drawPtX + qX, drawPtY + qY), |
| 261 | (drawPtX + int(qX * 3), drawPtY + int(tY * 2)) | 296 | (drawPtX + int(qX * 3), drawPtY + int(tY * 2)), |
| 262 | ) | 297 | ) |
| 263 | duckBeak = ( | 298 | duckBeak = ( |
| 264 | (drawPtX + hX, drawPtY + qY), | 299 | (drawPtX + hX, drawPtY + qY), |
| 265 | (drawPtX + self.pxWidth + qX, | 300 | (drawPtX + self.pxWidth + qX, drawPtY + int(qY * 3)), |
| 266 | drawPtY + int(qY * 3)) | ||
| 267 | ) | ||
| 268 | duckWing = ( | ||
| 269 | (drawPtX, drawPtY + hY), | ||
| 270 | rect[1] | ||
| 271 | ) | 301 | ) |
| 302 | duckWing = ((drawPtX, drawPtY + hY), rect[1]) | ||
| 272 | duckBody = ( | 303 | duckBody = ( |
| 273 | (drawPtX + int(qX / 4), drawPtY + int(qY * 3)), | 304 | (drawPtX + int(qX / 4), drawPtY + int(qY * 3)), |
| 274 | (drawPtX + int(tX * 2), drawPtY + self.pxHeight) | 305 | (drawPtX + int(tX * 2), drawPtY + self.pxHeight), |
| 275 | ) | 306 | ) |
| 276 | drawer.ellipse(duckBody, fill=self.color) | 307 | drawer.ellipse(duckBody, fill=self.color) |
| 277 | drawer.ellipse(duckHead, fill=self.color) | 308 | drawer.ellipse(duckHead, fill=self.color) |
| @@ -279,11 +310,16 @@ class Component(Component): | |||
| 279 | drawer.pieslice(duckBeak, 145, 200, fill=self.color) | 310 | drawer.pieslice(duckBeak, 145, 200, fill=self.color) |
| 280 | 311 | ||
| 281 | # Peace | 312 | # Peace |
| 282 | elif shape == 'peace': | 313 | elif shape == "peace": |
| 283 | line = (( | 314 | line = ( |
| 284 | drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)), | 315 | ( |
| 285 | (drawPtX + hX + int(tenthX / 2), | 316 | drawPtX + hX - int(tenthX / 2), |
| 286 | drawPtY + self.pxHeight - int(tenthY / 2)) | 317 | drawPtY + int(tenthY / 2), |
| 318 | ), | ||
| 319 | ( | ||
| 320 | drawPtX + hX + int(tenthX / 2), | ||
| 321 | drawPtY + self.pxHeight - int(tenthY / 2), | ||
| 322 | ), | ||
| 287 | ) | 323 | ) |
| 288 | drawer.ellipse(outlineShape, fill=self.color) | 324 | drawer.ellipse(outlineShape, fill=self.color) |
| 289 | drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) | 325 | drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) |
| @@ -291,21 +327,15 @@ class Component(Component): | |||
| 291 | 327 | ||
| 292 | def slantLine(difference): | 328 | def slantLine(difference): |
| 293 | return ( | 329 | return ( |
| 294 | (drawPtX + difference), (drawPtY + self.pxHeight - qY) | 330 | (drawPtX + difference), |
| 331 | (drawPtY + self.pxHeight - qY), | ||
| 295 | ), ( | 332 | ), ( |
| 296 | (drawPtX + hX), (drawPtY + hY) | 333 | (drawPtX + hX), |
| 334 | (drawPtY + hY), | ||
| 297 | ) | 335 | ) |
| 298 | 336 | ||
| 299 | drawer.line( | 337 | drawer.line(slantLine(qX), fill=self.color, width=tenthX) |
| 300 | slantLine(qX), | 338 | drawer.line(slantLine(self.pxWidth - qX), fill=self.color, width=tenthX) |
| 301 | fill=self.color, | ||
| 302 | width=tenthX | ||
| 303 | ) | ||
| 304 | drawer.line( | ||
| 305 | slantLine(self.pxWidth - qX), | ||
| 306 | fill=self.color, | ||
| 307 | width=tenthX | ||
| 308 | ) | ||
| 309 | 339 | ||
| 310 | for x, y in grid: | 340 | for x, y in grid: |
| 311 | drawPtX = x * self.pxWidth | 341 | drawPtX = x * self.pxWidth |
| @@ -331,44 +361,38 @@ class Component(Component): | |||
| 331 | w, h = scale(0.05, self.width, self.height, int) | 361 | w, h = scale(0.05, self.width, self.height, int) |
| 332 | for x in range(self.pxWidth, self.width, self.pxWidth): | 362 | for x in range(self.pxWidth, self.width, self.pxWidth): |
| 333 | drawer.rectangle( | 363 | drawer.rectangle( |
| 334 | ((x, 0), | 364 | ((x, 0), (x + w, self.height)), |
| 335 | (x + w, self.height)), | ||
| 336 | fill=self.color, | 365 | fill=self.color, |
| 337 | ) | 366 | ) |
| 338 | for y in range(self.pxHeight, self.height, self.pxHeight): | 367 | for y in range(self.pxHeight, self.height, self.pxHeight): |
| 339 | drawer.rectangle( | 368 | drawer.rectangle( |
| 340 | ((0, y), | 369 | ((0, y), (self.width, y + h)), |
| 341 | (self.width, y + h)), | ||
| 342 | fill=self.color, | 370 | fill=self.color, |
| 343 | ) | 371 | ) |
| 344 | 372 | ||
| 345 | return frame | 373 | return frame |
| 346 | 374 | ||
| 347 | def gridForTick(self, tick): | 375 | def gridForTick(self, tick): |
| 348 | ''' | 376 | """ |
| 349 | Given a tick number over 0, returns a new grid (a set of tuples). | 377 | Given a tick number over 0, returns a new grid (a set of tuples). |
| 350 | This must compute the previous ticks' grids if not already computed | 378 | This must compute the previous ticks' grids if not already computed |
| 351 | ''' | 379 | """ |
| 352 | if tick - 1 not in self.tickGrids: | 380 | if tick - 1 not in self.tickGrids: |
| 353 | self.tickGrids[tick - 1] = self.gridForTick(tick - 1) | 381 | self.tickGrids[tick - 1] = self.gridForTick(tick - 1) |
| 354 | 382 | ||
| 355 | lastGrid = self.tickGrids[tick - 1] | 383 | lastGrid = self.tickGrids[tick - 1] |
| 356 | 384 | ||
| 357 | def neighbours(x, y): | 385 | def neighbours(x, y): |
| 358 | return { | 386 | return {cell for cell in self.nearbyCoords(x, y) if cell in lastGrid} |
| 359 | cell for cell in self.nearbyCoords(x, y) | ||
| 360 | if cell in lastGrid | ||
| 361 | } | ||
| 362 | 387 | ||
| 363 | newGrid = set() | 388 | newGrid = set() |
| 364 | # Copy cells from the previous grid if they have 2 or 3 neighbouring cells | 389 | # Copy cells from the previous grid if they have 2 or 3 neighbouring cells |
| 365 | # and if they are within the grid or its bleed area (off-canvas area) | 390 | # and if they are within the grid or its bleed area (off-canvas area) |
| 366 | for x, y in lastGrid: | 391 | for x, y in lastGrid: |
| 367 | if ( | 392 | if ( |
| 368 | -self.bleedSize > x > self.gridWidth + self.bleedSize | 393 | -self.bleedSize > x > self.gridWidth + self.bleedSize |
| 369 | or | 394 | or -self.bleedSize > y > self.gridHeight + self.bleedSize |
| 370 | -self.bleedSize > y > self.gridHeight + self.bleedSize | 395 | ): |
| 371 | ): | ||
| 372 | continue | 396 | continue |
| 373 | surrounding = len(neighbours(x, y)) | 397 | surrounding = len(neighbours(x, y)) |
| 374 | if surrounding == 2 or surrounding == 3: | 398 | if surrounding == 2 or surrounding == 3: |
| @@ -376,7 +400,8 @@ class Component(Component): | |||
| 376 | 400 | ||
| 377 | # Find positions around living cells which must be checked for reproduction | 401 | # Find positions around living cells which must be checked for reproduction |
| 378 | potentialNewCells = { | 402 | potentialNewCells = { |
| 379 | coordTup for origin in lastGrid | 403 | coordTup |
| 404 | for origin in lastGrid | ||
| 380 | for coordTup in list(self.nearbyCoords(*origin)) | 405 | for coordTup in list(self.nearbyCoords(*origin)) |
| 381 | } | 406 | } |
| 382 | # Check for reproduction | 407 | # Check for reproduction |
| @@ -392,11 +417,11 @@ class Component(Component): | |||
| 392 | 417 | ||
| 393 | def savePreset(self): | 418 | def savePreset(self): |
| 394 | pr = super().savePreset() | 419 | pr = super().savePreset() |
| 395 | pr['GRID'] = sorted(self.startingGrid) | 420 | pr["GRID"] = sorted(self.startingGrid) |
| 396 | return pr | 421 | return pr |
| 397 | 422 | ||
| 398 | def loadPreset(self, pr, *args): | 423 | def loadPreset(self, pr, *args): |
| 399 | self.startingGrid = set(pr['GRID']) | 424 | self.startingGrid = set(pr["GRID"]) |
| 400 | if self.startingGrid: | 425 | if self.startingGrid: |
| 401 | for widget in self.shiftButtons: | 426 | for widget in self.shiftButtons: |
| 402 | widget.setEnabled(True) | 427 | widget.setEnabled(True) |
| @@ -414,15 +439,17 @@ class Component(Component): | |||
| 414 | 439 | ||
| 415 | 440 | ||
| 416 | class ClickGrid(QUndoCommand): | 441 | class ClickGrid(QUndoCommand): |
| 417 | def __init__(self, comp, pos, id_): | 442 | def __init__(self, comp, pos, button): |
| 418 | super().__init__( | 443 | super().__init__("click %s component #%s" % (comp.name, comp.compPos)) |
| 419 | "click %s component #%s" % (comp.name, comp.compPos)) | ||
| 420 | self.comp = comp | 444 | self.comp = comp |
| 421 | self.pos = [pos] | 445 | self.pos = [pos] |
| 422 | self.id_ = id_ | 446 | if button == QtCore.Qt.MouseButton.RightButton: |
| 447 | self.button = 2 | ||
| 448 | else: | ||
| 449 | self.button = 1 | ||
| 423 | 450 | ||
| 424 | def id(self): | 451 | def id(self): |
| 425 | return self.id_ | 452 | return self.button |
| 426 | 453 | ||
| 427 | def mergeWith(self, other): | 454 | def mergeWith(self, other): |
| 428 | self.pos.extend(other.pos) | 455 | self.pos.extend(other.pos) |
| @@ -439,21 +466,21 @@ class ClickGrid(QUndoCommand): | |||
| 439 | self.comp.update(auto=True) | 466 | self.comp.update(auto=True) |
| 440 | 467 | ||
| 441 | def redo(self): | 468 | def redo(self): |
| 442 | if self.id_ == 1: # Left-click | 469 | if self.button == 1: # Left-click |
| 443 | self.add() | 470 | self.add() |
| 444 | elif self.id_ == 2: # Right-click | 471 | elif self.button == 2: # Right-click |
| 445 | self.remove() | 472 | self.remove() |
| 446 | 473 | ||
| 447 | def undo(self): | 474 | def undo(self): |
| 448 | if self.id_ == 1: # Left-click | 475 | if self.button == 1: # Left-click |
| 449 | self.remove() | 476 | self.remove() |
| 450 | elif self.id_ == 2: # Right-click | 477 | elif self.button == 2: # Right-click |
| 451 | self.add() | 478 | self.add() |
| 452 | 479 | ||
| 480 | |||
| 453 | class ShiftGrid(QUndoCommand): | 481 | class ShiftGrid(QUndoCommand): |
| 454 | def __init__(self, comp, direction): | 482 | def __init__(self, comp, direction): |
| 455 | super().__init__( | 483 | super().__init__("change %s component #%s" % (comp.name, comp.compPos)) |
| 456 | "change %s component #%s" % (comp.name, comp.compPos)) | ||
| 457 | self.comp = comp | 484 | self.comp = comp |
| 458 | self.direction = direction | 485 | self.direction = direction |
| 459 | self.distance = 1 | 486 | self.distance = 1 |
| @@ -466,10 +493,7 @@ class ShiftGrid(QUndoCommand): | |||
| 466 | return True | 493 | return True |
| 467 | 494 | ||
| 468 | def newGrid(self, Xchange, Ychange): | 495 | def newGrid(self, Xchange, Ychange): |
| 469 | return { | 496 | return {(x + Xchange, y + Ychange) for x, y in self.comp.startingGrid} |
| 470 | (x + Xchange, y + Ychange) | ||
| 471 | for x, y in self.comp.startingGrid | ||
| 472 | } | ||
| 473 | 497 | ||
| 474 | def redo(self): | 498 | def redo(self): |
| 475 | if self.direction == 0: | 499 | if self.direction == 0: |
diff --git a/src/components/life.ui b/src/components/life.ui index 3d6ad5a..30cf9d0 100644 --- a/src/components/life.ui +++ b/src/components/life.ui | |||
| @@ -372,7 +372,7 @@ p, li { white-space: pre-wrap; } | |||
| 372 | <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with more than 3 neighbours will die from overpopulation.</p> | 372 | <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with more than 3 neighbours will die from overpopulation.</p> |
| 373 | <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- An empty space surrounded by 3 live cells will cause reproduction.</p></body></html></string> | 373 | <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- An empty space surrounded by 3 live cells will cause reproduction.</p></body></html></string> |
| 374 | </property> | 374 | </property> |
| 375 | <property name="tabStopWidth"> | 375 | <property name="tabStopDistance"> |
| 376 | <number>80</number> | 376 | <number>80</number> |
| 377 | </property> | 377 | </property> |
| 378 | <property name="textInteractionFlags"> | 378 | <property name="textInteractionFlags"> |
diff --git a/src/components/original.py b/src/components/original.py index 289e982..fad797b 100644 --- a/src/components/original.py +++ b/src/components/original.py | |||
| @@ -1,9 +1,5 @@ | |||
| 1 | import numpy | 1 | import numpy |
| 2 | from PIL import Image, ImageDraw | 2 | from PIL import Image, ImageDraw |
| 3 | from PyQt5 import QtGui, QtCore, QtWidgets | ||
| 4 | from PyQt5.QtGui import QColor | ||
| 5 | import os | ||
| 6 | import time | ||
| 7 | from copy import copy | 3 | from copy import copy |
| 8 | 4 | ||
| 9 | from ..component import Component | 5 | from ..component import Component |
| @@ -11,14 +7,14 @@ from ..toolkit.frame import BlankFrame | |||
| 11 | 7 | ||
| 12 | 8 | ||
| 13 | class Component(Component): | 9 | class Component(Component): |
| 14 | name = 'Classic Visualizer' | 10 | name = "Classic Visualizer" |
| 15 | version = '1.0.0' | 11 | version = "1.0.0" |
| 16 | 12 | ||
| 17 | def names(*args): | 13 | def names(*args): |
| 18 | return ['Original Audio Visualization'] | 14 | return ["Original Audio Visualization"] |
| 19 | 15 | ||
| 20 | def properties(self): | 16 | def properties(self): |
| 21 | return ['pcm'] | 17 | return ["pcm"] |
| 22 | 18 | ||
| 23 | def widget(self, *args): | 19 | def widget(self, *args): |
| 24 | self.scale = 20 | 20 | self.scale = 20 |
| @@ -31,23 +27,30 @@ class Component(Component): | |||
| 31 | self.page.comboBox_visLayout.addItem("Top") | 27 | self.page.comboBox_visLayout.addItem("Top") |
| 32 | self.page.comboBox_visLayout.setCurrentIndex(0) | 28 | self.page.comboBox_visLayout.setCurrentIndex(0) |
| 33 | 29 | ||
| 34 | self.page.lineEdit_visColor.setText('255,255,255') | 30 | self.page.lineEdit_visColor.setText("255,255,255") |
| 35 | 31 | ||
| 36 | self.trackWidgets({ | 32 | self.trackWidgets( |
| 37 | 'visColor': self.page.lineEdit_visColor, | 33 | { |
| 38 | 'layout': self.page.comboBox_visLayout, | 34 | "visColor": self.page.lineEdit_visColor, |
| 39 | 'scale': self.page.spinBox_scale, | 35 | "layout": self.page.comboBox_visLayout, |
| 40 | 'y': self.page.spinBox_y, | 36 | "scale": self.page.spinBox_scale, |
| 41 | 'smooth': self.page.spinBox_smooth, | 37 | "y": self.page.spinBox_y, |
| 42 | }, colorWidgets={ | 38 | "smooth": self.page.spinBox_smooth, |
| 43 | 'visColor': self.page.pushButton_visColor, | 39 | }, |
| 44 | }, relativeWidgets=[ | 40 | colorWidgets={ |
| 45 | 'y', | 41 | "visColor": self.page.pushButton_visColor, |
| 46 | ]) | 42 | }, |
| 43 | relativeWidgets=[ | ||
| 44 | "y", | ||
| 45 | ], | ||
| 46 | ) | ||
| 47 | 47 | ||
| 48 | def previewRender(self): | 48 | def previewRender(self): |
| 49 | spectrum = numpy.fromfunction( | 49 | spectrum = numpy.fromfunction( |
| 50 | lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16") | 50 | lambda x: float(self.scale) / 2500 * (x - 128) ** 2, |
| 51 | (255,), | ||
| 52 | dtype="int16", | ||
| 53 | ) | ||
| 51 | return self.drawBars( | 54 | return self.drawBars( |
| 52 | self.width, self.height, spectrum, self.visColor, self.layout | 55 | self.width, self.height, spectrum, self.visColor, self.layout |
| 53 | ) | 56 | ) |
| @@ -63,41 +66,53 @@ class Component(Component): | |||
| 63 | if self.canceled: | 66 | if self.canceled: |
| 64 | break | 67 | break |
| 65 | self.lastSpectrum = self.transformData( | 68 | self.lastSpectrum = self.transformData( |
| 66 | i, self.completeAudioArray, self.sampleSize, | 69 | i, |
| 67 | self.smoothConstantDown, self.smoothConstantUp, | 70 | self.completeAudioArray, |
| 68 | self.lastSpectrum) | 71 | self.sampleSize, |
| 72 | self.smoothConstantDown, | ||
| 73 | self.smoothConstantUp, | ||
| 74 | self.lastSpectrum, | ||
| 75 | ) | ||
| 69 | self.spectrumArray[i] = copy(self.lastSpectrum) | 76 | self.spectrumArray[i] = copy(self.lastSpectrum) |
| 70 | 77 | ||
| 71 | progress = int(100*(i/len(self.completeAudioArray))) | 78 | progress = int(100 * (i / len(self.completeAudioArray))) |
| 72 | if progress >= 100: | 79 | if progress >= 100: |
| 73 | progress = 100 | 80 | progress = 100 |
| 74 | pStr = "Analyzing audio: "+str(progress)+'%' | 81 | pStr = "Analyzing audio: " + str(progress) + "%" |
| 75 | self.progressBarSetText.emit(pStr) | 82 | self.progressBarSetText.emit(pStr) |
| 76 | self.progressBarUpdate.emit(int(progress)) | 83 | self.progressBarUpdate.emit(int(progress)) |
| 77 | 84 | ||
| 78 | def frameRender(self, frameNo): | 85 | def frameRender(self, frameNo): |
| 79 | arrayNo = frameNo * self.sampleSize | 86 | arrayNo = frameNo * self.sampleSize |
| 80 | return self.drawBars( | 87 | return self.drawBars( |
| 81 | self.width, self.height, | 88 | self.width, |
| 89 | self.height, | ||
| 82 | self.spectrumArray[arrayNo], | 90 | self.spectrumArray[arrayNo], |
| 83 | self.visColor, self.layout) | 91 | self.visColor, |
| 92 | self.layout, | ||
| 93 | ) | ||
| 84 | 94 | ||
| 85 | def transformData( | 95 | def transformData( |
| 86 | self, i, completeAudioArray, sampleSize, | 96 | self, |
| 87 | smoothConstantDown, smoothConstantUp, lastSpectrum): | 97 | i, |
| 98 | completeAudioArray, | ||
| 99 | sampleSize, | ||
| 100 | smoothConstantDown, | ||
| 101 | smoothConstantUp, | ||
| 102 | lastSpectrum, | ||
| 103 | ): | ||
| 88 | if len(completeAudioArray) < (i + sampleSize): | 104 | if len(completeAudioArray) < (i + sampleSize): |
| 89 | sampleSize = len(completeAudioArray) - i | 105 | sampleSize = len(completeAudioArray) - i |
| 90 | 106 | ||
| 91 | window = numpy.hanning(sampleSize) | 107 | window = numpy.hanning(sampleSize) |
| 92 | data = completeAudioArray[i:i+sampleSize][::1] * window | 108 | data = completeAudioArray[i : i + sampleSize][::1] * window |
| 93 | paddedSampleSize = 2048 | 109 | paddedSampleSize = 2048 |
| 94 | paddedData = numpy.pad( | 110 | paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), "constant") |
| 95 | data, (0, paddedSampleSize - sampleSize), 'constant') | ||
| 96 | spectrum = numpy.fft.fft(paddedData) | 111 | spectrum = numpy.fft.fft(paddedData) |
| 97 | sample_rate = 44100 | 112 | sample_rate = 44100 |
| 98 | frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate) | 113 | frequencies = numpy.fft.fftfreq(len(spectrum), 1.0 / sample_rate) |
| 99 | 114 | ||
| 100 | y = abs(spectrum[0:int(paddedSampleSize/2) - 1]) | 115 | y = abs(spectrum[0 : int(paddedSampleSize / 2) - 1]) |
| 101 | 116 | ||
| 102 | # filter the noise away | 117 | # filter the noise away |
| 103 | # y[y<80] = 0 | 118 | # y[y<80] = 0 |
| @@ -106,22 +121,26 @@ class Component(Component): | |||
| 106 | y[numpy.isinf(y)] = 0 | 121 | y[numpy.isinf(y)] = 0 |
| 107 | 122 | ||
| 108 | if lastSpectrum is not None: | 123 | if lastSpectrum is not None: |
| 109 | lastSpectrum[y < lastSpectrum] = \ | 124 | lastSpectrum[y < lastSpectrum] = y[ |
| 110 | y[y < lastSpectrum] * smoothConstantDown + \ | 125 | y < lastSpectrum |
| 111 | lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown) | 126 | ] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * ( |
| 112 | 127 | 1 - smoothConstantDown | |
| 113 | lastSpectrum[y >= lastSpectrum] = \ | 128 | ) |
| 114 | y[y >= lastSpectrum] * smoothConstantUp + \ | 129 | |
| 115 | lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp) | 130 | lastSpectrum[y >= lastSpectrum] = y[ |
| 131 | y >= lastSpectrum | ||
| 132 | ] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * ( | ||
| 133 | 1 - smoothConstantUp | ||
| 134 | ) | ||
| 116 | else: | 135 | else: |
| 117 | lastSpectrum = y | 136 | lastSpectrum = y |
| 118 | 137 | ||
| 119 | x = frequencies[0:int(paddedSampleSize/2) - 1] | 138 | x = frequencies[0 : int(paddedSampleSize / 2) - 1] |
| 120 | 139 | ||
| 121 | return lastSpectrum | 140 | return lastSpectrum |
| 122 | 141 | ||
| 123 | def drawBars(self, width, height, spectrum, color, layout): | 142 | def drawBars(self, width, height, spectrum, color, layout): |
| 124 | vH = height-height/8 | 143 | vH = height - height / 8 |
| 125 | bF = width / 64 | 144 | bF = width / 64 |
| 126 | bH = bF / 2 | 145 | bH = bF / 2 |
| 127 | bQ = bF / 4 | 146 | bQ = bF / 4 |
| @@ -133,72 +152,92 @@ class Component(Component): | |||
| 133 | bP = height / 1200 | 152 | bP = height / 1200 |
| 134 | 153 | ||
| 135 | for j in range(0, 63): | 154 | for j in range(0, 63): |
| 136 | draw.rectangle(( | 155 | x0 = bH + j * bF |
| 137 | bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ - | 156 | y0 = vH + bQ |
| 138 | spectrum[j * 4] * bP - bH), fill=color2) | 157 | y1 = vH + bQ - spectrum[j * 4] * bP - bH |
| 139 | 158 | x1 = bH + j * bF + bF | |
| 140 | draw.rectangle(( | 159 | draw.rectangle( |
| 141 | bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH - | 160 | ( |
| 142 | spectrum[j * 4] * bP), fill=color) | 161 | x0, |
| 143 | 162 | y0 if y0 < y1 else y1, | |
| 144 | imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM) | 163 | x1 if x1 > x0 else x0, |
| 164 | y1 if y0 < y1 else y0, | ||
| 165 | ), | ||
| 166 | fill=color2, | ||
| 167 | ) | ||
| 168 | |||
| 169 | x0 = bH + bQ + j * bF | ||
| 170 | y0 = vH | ||
| 171 | x1 = bH + bQ + j * bF + bH | ||
| 172 | y1 = vH - spectrum[j * 4] * bP | ||
| 173 | draw.rectangle( | ||
| 174 | ( | ||
| 175 | x0, | ||
| 176 | y0 if y0 < y1 else y1, | ||
| 177 | x1 if x1 > x0 else x0, | ||
| 178 | y1 if y0 < y1 else y0, | ||
| 179 | ), | ||
| 180 | fill=color, | ||
| 181 | ) | ||
| 182 | |||
| 183 | imBottom = imTop.transpose(Image.Transpose.FLIP_TOP_BOTTOM) | ||
| 145 | 184 | ||
| 146 | im = BlankFrame(width, height) | 185 | im = BlankFrame(width, height) |
| 147 | 186 | ||
| 148 | if layout == 0: # Classic | 187 | if layout == 0: # Classic |
| 149 | y = self.y - int(height/100*43) | 188 | y = self.y - int(height / 100 * 43) |
| 150 | im.paste(imTop, (0, y), mask=imTop) | 189 | im.paste(imTop, (0, y), mask=imTop) |
| 151 | y = self.y + int(height/100*43) | 190 | y = self.y + int(height / 100 * 43) |
| 152 | im.paste(imBottom, (0, y), mask=imBottom) | 191 | im.paste(imBottom, (0, y), mask=imBottom) |
| 153 | 192 | ||
| 154 | if layout == 1: # Split | 193 | if layout == 1: # Split |
| 155 | y = self.y + int(height/100*10) | 194 | y = self.y + int(height / 100 * 10) |
| 156 | im.paste(imTop, (0, y), mask=imTop) | 195 | im.paste(imTop, (0, y), mask=imTop) |
| 157 | y = self.y - int(height/100*10) | 196 | y = self.y - int(height / 100 * 10) |
| 158 | im.paste(imBottom, (0, y), mask=imBottom) | 197 | im.paste(imBottom, (0, y), mask=imBottom) |
| 159 | 198 | ||
| 160 | if layout == 2: # Bottom | 199 | if layout == 2: # Bottom |
| 161 | y = self.y + int(height/100*10) | 200 | y = self.y + int(height / 100 * 10) |
| 162 | im.paste(imTop, (0, y), mask=imTop) | 201 | im.paste(imTop, (0, y), mask=imTop) |
| 163 | 202 | ||
| 164 | if layout == 3: # Top | 203 | if layout == 3: # Top |
| 165 | y = self.y - int(height/100*10) | 204 | y = self.y - int(height / 100 * 10) |
| 166 | im.paste(imBottom, (0, y), mask=imBottom) | 205 | im.paste(imBottom, (0, y), mask=imBottom) |
| 167 | 206 | ||
| 168 | return im | 207 | return im |
| 169 | 208 | ||
| 170 | def command(self, arg): | 209 | def command(self, arg): |
| 171 | if '=' in arg: | 210 | if "=" in arg: |
| 172 | key, arg = arg.split('=', 1) | 211 | key, arg = arg.split("=", 1) |
| 173 | try: | 212 | try: |
| 174 | if key == 'color': | 213 | if key == "color": |
| 175 | self.page.lineEdit_visColor.setText(arg) | 214 | self.page.lineEdit_visColor.setText(arg) |
| 176 | return | 215 | return |
| 177 | elif key == 'layout': | 216 | elif key == "layout": |
| 178 | if arg == 'classic': | 217 | if arg == "classic": |
| 179 | self.page.comboBox_visLayout.setCurrentIndex(0) | 218 | self.page.comboBox_visLayout.setCurrentIndex(0) |
| 180 | elif arg == 'split': | 219 | elif arg == "split": |
| 181 | self.page.comboBox_visLayout.setCurrentIndex(1) | 220 | self.page.comboBox_visLayout.setCurrentIndex(1) |
| 182 | elif arg == 'bottom': | 221 | elif arg == "bottom": |
| 183 | self.page.comboBox_visLayout.setCurrentIndex(2) | 222 | self.page.comboBox_visLayout.setCurrentIndex(2) |
| 184 | elif arg == 'top': | 223 | elif arg == "top": |
| 185 | self.page.comboBox_visLayout.setCurrentIndex(3) | 224 | self.page.comboBox_visLayout.setCurrentIndex(3) |
| 186 | return | 225 | return |
| 187 | elif key == 'scale': | 226 | elif key == "scale": |
| 188 | arg = int(arg) | 227 | arg = int(arg) |
| 189 | self.page.spinBox_scale.setValue(arg) | 228 | self.page.spinBox_scale.setValue(arg) |
| 190 | return | 229 | return |
| 191 | elif key == 'y': | 230 | elif key == "y": |
| 192 | arg = int(arg) | 231 | arg = int(arg) |
| 193 | self.page.spinBox_y.setValue(arg) | 232 | self.page.spinBox_y.setValue(arg) |
| 194 | return | 233 | return |
| 195 | except ValueError: | 234 | except ValueError: |
| 196 | print('You must enter a number.') | 235 | print("You must enter a number.") |
| 197 | quit(1) | 236 | quit(1) |
| 198 | super().command(arg) | 237 | super().command(arg) |
| 199 | 238 | ||
| 200 | def commandHelp(self): | 239 | def commandHelp(self): |
| 201 | print('Give a layout name:\n layout=[classic/split/bottom/top]') | 240 | print("Give a layout name:\n layout=[classic/split/bottom/top]") |
| 202 | print('Specify a color:\n color=255,255,255') | 241 | print("Specify a color:\n color=255,255,255") |
| 203 | print('Visualizer scale (20 is default):\n scale=number') | 242 | print("Visualizer scale (20 is default):\n scale=number") |
| 204 | print('Y position:\n y=number') | 243 | print("Y position:\n y=number") |
diff --git a/src/components/sound.py b/src/components/sound.py index 118ea23..2df8e38 100644 --- a/src/components/sound.py +++ b/src/components/sound.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | from PyQt5 import QtGui, QtCore, QtWidgets | 1 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 2 | import os | 2 | import os |
| 3 | 3 | ||
| 4 | from ..component import Component | 4 | from ..component import Component |
| @@ -6,25 +6,28 @@ from ..toolkit.frame import BlankFrame | |||
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | class Component(Component): | 8 | class Component(Component): |
| 9 | name = 'Sound' | 9 | name = "Sound" |
| 10 | version = '1.0.0' | 10 | version = "1.0.0" |
| 11 | 11 | ||
| 12 | def widget(self, *args): | 12 | def widget(self, *args): |
| 13 | super().widget(*args) | 13 | super().widget(*args) |
| 14 | self.page.pushButton_sound.clicked.connect(self.pickSound) | 14 | self.page.pushButton_sound.clicked.connect(self.pickSound) |
| 15 | self.trackWidgets({ | 15 | self.trackWidgets( |
| 16 | 'sound': self.page.lineEdit_sound, | 16 | { |
| 17 | 'chorus': self.page.checkBox_chorus, | 17 | "sound": self.page.lineEdit_sound, |
| 18 | 'delay': self.page.spinBox_delay, | 18 | "chorus": self.page.checkBox_chorus, |
| 19 | 'volume': self.page.spinBox_volume, | 19 | "delay": self.page.spinBox_delay, |
| 20 | }, commandArgs={ | 20 | "volume": self.page.spinBox_volume, |
| 21 | 'sound': None, | 21 | }, |
| 22 | }) | 22 | commandArgs={ |
| 23 | "sound": None, | ||
| 24 | }, | ||
| 25 | ) | ||
| 23 | 26 | ||
| 24 | def properties(self): | 27 | def properties(self): |
| 25 | props = ['static', 'audio'] | 28 | props = ["static", "audio"] |
| 26 | if not os.path.exists(self.sound): | 29 | if not os.path.exists(self.sound): |
| 27 | props.append('error') | 30 | props.append("error") |
| 28 | return props | 31 | return props |
| 29 | 32 | ||
| 30 | def error(self): | 33 | def error(self): |
| @@ -36,20 +39,22 @@ class Component(Component): | |||
| 36 | def audio(self): | 39 | def audio(self): |
| 37 | params = {} | 40 | params = {} |
| 38 | if self.delay != 0.0: | 41 | if self.delay != 0.0: |
| 39 | params['adelay'] = '=%s' % str(int(self.delay * 1000.00)) | 42 | params["adelay"] = "=%s" % str(int(self.delay * 1000.00)) |
| 40 | if self.chorus: | 43 | if self.chorus: |
| 41 | params['chorus'] = \ | 44 | 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" |
| 42 | '=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3' | ||
| 43 | if self.volume != 1.0: | 45 | if self.volume != 1.0: |
| 44 | params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume) | 46 | params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume) |
| 45 | 47 | ||
| 46 | return (self.sound, params) | 48 | return (self.sound, params) |
| 47 | 49 | ||
| 48 | def pickSound(self): | 50 | def pickSound(self): |
| 49 | sndDir = self.settings.value("componentDir", os.path.expanduser("~")) | 51 | sndDir = self.settings.value("componentDir", os.path.expanduser("~")) |
| 50 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 52 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 51 | self.page, "Choose Sound", sndDir, | 53 | self.page, |
| 52 | "Audio Files (%s)" % " ".join(self.core.audioFormats)) | 54 | "Choose Sound", |
| 55 | sndDir, | ||
| 56 | "Audio Files (%s)" % " ".join(self.core.audioFormats), | ||
| 57 | ) | ||
| 53 | if filename: | 58 | if filename: |
| 54 | self.settings.setValue("componentDir", os.path.dirname(filename)) | 59 | self.settings.setValue("componentDir", os.path.dirname(filename)) |
| 55 | self.mergeUndo = False | 60 | self.mergeUndo = False |
| @@ -57,14 +62,13 @@ class Component(Component): | |||
| 57 | self.mergeUndo = True | 62 | self.mergeUndo = True |
| 58 | 63 | ||
| 59 | def commandHelp(self): | 64 | def commandHelp(self): |
| 60 | print('Path to audio file:\n path=/filepath/to/sound.ogg') | 65 | print("Path to audio file:\n path=/filepath/to/sound.ogg") |
| 61 | 66 | ||
| 62 | def command(self, arg): | 67 | def command(self, arg): |
| 63 | if '=' in arg: | 68 | if "=" in arg: |
| 64 | key, arg = arg.split('=', 1) | 69 | key, arg = arg.split("=", 1) |
| 65 | if key == 'path': | 70 | if key == "path": |
| 66 | if '*%s' % os.path.splitext(arg)[1] \ | 71 | if "*%s" % os.path.splitext(arg)[1] not in self.core.audioFormats: |
| 67 | not in self.core.audioFormats: | ||
| 68 | print("Not a supported audio format") | 72 | print("Not a supported audio format") |
| 69 | quit(1) | 73 | quit(1) |
| 70 | self.page.lineEdit_sound.setText(arg) | 74 | self.page.lineEdit_sound.setText(arg) |
diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 30d5426..062ebc7 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | from PIL import Image | 1 | from PIL import Image |
| 2 | from PyQt5 import QtGui, QtCore, QtWidgets | 2 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 3 | import os | 3 | import os |
| 4 | import math | 4 | import math |
| 5 | import subprocess | 5 | import subprocess |
| @@ -10,16 +10,20 @@ from ..component import Component | |||
| 10 | from ..toolkit.frame import BlankFrame, scale | 10 | from ..toolkit.frame import BlankFrame, scale |
| 11 | from ..toolkit import checkOutput, connectWidget | 11 | from ..toolkit import checkOutput, connectWidget |
| 12 | from ..toolkit.ffmpeg import ( | 12 | from ..toolkit.ffmpeg import ( |
| 13 | openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound | 13 | openPipe, |
| 14 | closePipe, | ||
| 15 | getAudioDuration, | ||
| 16 | FfmpegVideo, | ||
| 17 | exampleSound, | ||
| 14 | ) | 18 | ) |
| 15 | 19 | ||
| 16 | 20 | ||
| 17 | log = logging.getLogger('AVP.Components.Spectrum') | 21 | log = logging.getLogger("AVP.Components.Spectrum") |
| 18 | 22 | ||
| 19 | 23 | ||
| 20 | class Component(Component): | 24 | class Component(Component): |
| 21 | name = 'Spectrum' | 25 | name = "Spectrum" |
| 22 | version = '1.0.1' | 26 | version = "1.0.1" |
| 23 | 27 | ||
| 24 | def widget(self, *args): | 28 | def widget(self, *args): |
| 25 | self.previewFrame = None | 29 | self.previewFrame = None |
| @@ -30,34 +34,36 @@ class Component(Component): | |||
| 30 | self.previewSize = (214, 120) | 34 | self.previewSize = (214, 120) |
| 31 | self.previewPipe = None | 35 | self.previewPipe = None |
| 32 | 36 | ||
| 33 | if hasattr(self.parent, 'lineEdit_audioFile'): | 37 | if hasattr(self.parent, "lineEdit_audioFile"): |
| 34 | # update preview when audio file changes (if genericPreview is off) | 38 | # update preview when audio file changes (if genericPreview is off) |
| 35 | self.parent.lineEdit_audioFile.textChanged.connect( | 39 | self.parent.lineEdit_audioFile.textChanged.connect(self.update) |
| 36 | self.update | ||
| 37 | ) | ||
| 38 | 40 | ||
| 39 | self.trackWidgets({ | 41 | self.trackWidgets( |
| 40 | 'filterType': self.page.comboBox_filterType, | 42 | { |
| 41 | 'window': self.page.comboBox_window, | 43 | "filterType": self.page.comboBox_filterType, |
| 42 | 'mode': self.page.comboBox_mode, | 44 | "window": self.page.comboBox_window, |
| 43 | 'amplitude': self.page.comboBox_amplitude0, | 45 | "mode": self.page.comboBox_mode, |
| 44 | 'amplitude1': self.page.comboBox_amplitude1, | 46 | "amplitude": self.page.comboBox_amplitude0, |
| 45 | 'amplitude2': self.page.comboBox_amplitude2, | 47 | "amplitude1": self.page.comboBox_amplitude1, |
| 46 | 'display': self.page.comboBox_display, | 48 | "amplitude2": self.page.comboBox_amplitude2, |
| 47 | 'zoom': self.page.spinBox_zoom, | 49 | "display": self.page.comboBox_display, |
| 48 | 'tc': self.page.spinBox_tc, | 50 | "zoom": self.page.spinBox_zoom, |
| 49 | 'x': self.page.spinBox_x, | 51 | "tc": self.page.spinBox_tc, |
| 50 | 'y': self.page.spinBox_y, | 52 | "x": self.page.spinBox_x, |
| 51 | 'mirror': self.page.checkBox_mirror, | 53 | "y": self.page.spinBox_y, |
| 52 | 'draw': self.page.checkBox_draw, | 54 | "mirror": self.page.checkBox_mirror, |
| 53 | 'scale': self.page.spinBox_scale, | 55 | "draw": self.page.checkBox_draw, |
| 54 | 'color': self.page.comboBox_color, | 56 | "scale": self.page.spinBox_scale, |
| 55 | 'compress': self.page.checkBox_compress, | 57 | "color": self.page.comboBox_color, |
| 56 | 'mono': self.page.checkBox_mono, | 58 | "compress": self.page.checkBox_compress, |
| 57 | 'hue': self.page.spinBox_hue, | 59 | "mono": self.page.checkBox_mono, |
| 58 | }, relativeWidgets=[ | 60 | "hue": self.page.spinBox_hue, |
| 59 | 'x', 'y', | 61 | }, |
| 60 | ]) | 62 | relativeWidgets=[ |
| 63 | "x", | ||
| 64 | "y", | ||
| 65 | ], | ||
| 66 | ) | ||
| 61 | for widget in self._trackedWidgets.values(): | 67 | for widget in self._trackedWidgets.values(): |
| 62 | connectWidget(widget, lambda: self.changed()) | 68 | connectWidget(widget, lambda: self.changed()) |
| 63 | 69 | ||
| @@ -78,18 +84,18 @@ class Component(Component): | |||
| 78 | 84 | ||
| 79 | def previewRender(self): | 85 | def previewRender(self): |
| 80 | changedSize = self.updateChunksize() | 86 | changedSize = self.updateChunksize() |
| 81 | if not changedSize \ | 87 | if ( |
| 82 | and not self.changedOptions \ | 88 | not changedSize |
| 83 | and self.previewFrame is not None: | 89 | and not self.changedOptions |
| 84 | log.debug( | 90 | and self.previewFrame is not None |
| 85 | 'Spectrum #%s is reusing old preview frame' % self.compPos) | 91 | ): |
| 92 | log.debug("Spectrum #%s is reusing old preview frame" % self.compPos) | ||
| 86 | return self.previewFrame | 93 | return self.previewFrame |
| 87 | 94 | ||
| 88 | frame = self.getPreviewFrame() | 95 | frame = self.getPreviewFrame() |
| 89 | self.changedOptions = False | 96 | self.changedOptions = False |
| 90 | if not frame: | 97 | if not frame: |
| 91 | log.warning( | 98 | log.warning("Spectrum #%s failed to create a preview frame" % self.compPos) |
| 92 | 'Spectrum #%s failed to create a preview frame' % self.compPos) | ||
| 93 | self.previewFrame = None | 99 | self.previewFrame = None |
| 94 | return BlankFrame(self.width, self.height) | 100 | return BlankFrame(self.width, self.height) |
| 95 | else: | 101 | else: |
| @@ -105,10 +111,12 @@ class Component(Component): | |||
| 105 | self.video = FfmpegVideo( | 111 | self.video = FfmpegVideo( |
| 106 | inputPath=self.audioFile, | 112 | inputPath=self.audioFile, |
| 107 | filter_=self.makeFfmpegFilter(), | 113 | filter_=self.makeFfmpegFilter(), |
| 108 | width=w, height=h, | 114 | width=w, |
| 115 | height=h, | ||
| 109 | chunkSize=self.chunkSize, | 116 | chunkSize=self.chunkSize, |
| 110 | frameRate=int(self.settings.value("outputFrameRate")), | 117 | frameRate=int(self.settings.value("outputFrameRate")), |
| 111 | parent=self.parent, component=self, | 118 | parent=self.parent, |
| 119 | component=self, | ||
| 112 | ) | 120 | ) |
| 113 | 121 | ||
| 114 | def frameRender(self, frameNo): | 122 | def frameRender(self, frameNo): |
| @@ -133,38 +141,55 @@ class Component(Component): | |||
| 133 | 141 | ||
| 134 | command = [ | 142 | command = [ |
| 135 | self.core.FFMPEG_BIN, | 143 | self.core.FFMPEG_BIN, |
| 136 | '-thread_queue_size', '512', | 144 | "-thread_queue_size", |
| 137 | '-r', str(self.settings.value("outputFrameRate")), | 145 | "512", |
| 138 | '-ss', "{0:.3f}".format(startPt), | 146 | "-r", |
| 139 | '-i', | 147 | str(self.settings.value("outputFrameRate")), |
| 140 | self.core.junkStream | 148 | "-ss", |
| 141 | if genericPreview else inputFile, | 149 | "{0:.3f}".format(startPt), |
| 142 | '-f', 'image2pipe', | 150 | "-i", |
| 143 | '-pix_fmt', 'rgba', | 151 | self.core.junkStream if genericPreview else inputFile, |
| 152 | "-f", | ||
| 153 | "image2pipe", | ||
| 154 | "-pix_fmt", | ||
| 155 | "rgba", | ||
| 144 | ] | 156 | ] |
| 145 | command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) | 157 | command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) |
| 146 | command.extend([ | 158 | command.extend( |
| 147 | '-an', | 159 | [ |
| 148 | '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), | 160 | "-an", |
| 149 | '-codec:v', 'rawvideo', '-', | 161 | "-s:v", |
| 150 | '-frames:v', '1', | 162 | "%sx%s" % scale(self.scale, self.width, self.height, str), |
| 151 | ]) | 163 | "-codec:v", |
| 164 | "rawvideo", | ||
| 165 | "-", | ||
| 166 | "-frames:v", | ||
| 167 | "1", | ||
| 168 | ] | ||
| 169 | ) | ||
| 152 | 170 | ||
| 153 | if self.core.logEnabled: | 171 | if self.core.logEnabled: |
| 154 | logFilename = os.path.join( | 172 | logFilename = os.path.join( |
| 155 | self.core.logDir, 'preview_%s.log' % str(self.compPos)) | 173 | self.core.logDir, "preview_%s.log" % str(self.compPos) |
| 156 | log.debug('Creating FFmpeg process (log at %s)' % logFilename) | 174 | ) |
| 157 | with open(logFilename, 'w') as logf: | 175 | log.debug("Creating FFmpeg process (log at %s)" % logFilename) |
| 158 | logf.write(" ".join(command) + '\n\n') | 176 | with open(logFilename, "w") as logf: |
| 159 | with open(logFilename, 'a') as logf: | 177 | logf.write(" ".join(command) + "\n\n") |
| 178 | with open(logFilename, "a") as logf: | ||
| 160 | self.previewPipe = openPipe( | 179 | self.previewPipe = openPipe( |
| 161 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 180 | command, |
| 162 | stderr=logf, bufsize=10**8 | 181 | stdin=subprocess.DEVNULL, |
| 182 | stdout=subprocess.PIPE, | ||
| 183 | stderr=logf, | ||
| 184 | bufsize=10**8, | ||
| 163 | ) | 185 | ) |
| 164 | else: | 186 | else: |
| 165 | self.previewPipe = openPipe( | 187 | self.previewPipe = openPipe( |
| 166 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 188 | command, |
| 167 | stderr=subprocess.DEVNULL, bufsize=10**8 | 189 | stdin=subprocess.DEVNULL, |
| 190 | stdout=subprocess.PIPE, | ||
| 191 | stderr=subprocess.DEVNULL, | ||
| 192 | bufsize=10**8, | ||
| 168 | ) | 193 | ) |
| 169 | byteFrame = self.previewPipe.stdout.read(self.chunkSize) | 194 | byteFrame = self.previewPipe.stdout.read(self.chunkSize) |
| 170 | closePipe(self.previewPipe) | 195 | closePipe(self.previewPipe) |
| @@ -173,132 +198,151 @@ class Component(Component): | |||
| 173 | return frame | 198 | return frame |
| 174 | 199 | ||
| 175 | def makeFfmpegFilter(self, preview=False, startPt=0): | 200 | def makeFfmpegFilter(self, preview=False, startPt=0): |
| 176 | '''Makes final FFmpeg filter command''' | 201 | """Makes final FFmpeg filter command""" |
| 177 | 202 | ||
| 178 | def getFilterComplexCommand(): | 203 | def getFilterComplexCommand(): |
| 179 | '''Inner function that creates the final, complex part of the filter command''' | 204 | """Inner function that creates the final, complex part of the filter command""" |
| 180 | nonlocal self | 205 | nonlocal self |
| 181 | genericPreview = self.settings.value("pref_genericPreview") | 206 | genericPreview = self.settings.value("pref_genericPreview") |
| 182 | 207 | ||
| 183 | def getFilterComplexCommandForType(): | 208 | def getFilterComplexCommandForType(): |
| 184 | '''Determine portion of filter command that changes depending on selected type''' | 209 | """Determine portion of filter command that changes depending on selected type""" |
| 185 | nonlocal self | 210 | nonlocal self |
| 186 | if preview: | 211 | if preview: |
| 187 | w, h = self.previewSize | 212 | w, h = self.previewSize |
| 188 | else: | 213 | else: |
| 189 | w, h = (self.width, self.height) | 214 | w, h = (self.width, self.height) |
| 190 | color = self.page.comboBox_color.currentText().lower() | 215 | color = self.page.comboBox_color.currentText().lower() |
| 191 | 216 | ||
| 192 | if self.filterType == 0: # Spectrum | 217 | if self.filterType == 0: # Spectrum |
| 193 | if self.amplitude == 0: | 218 | if self.amplitude == 0: |
| 194 | amplitude = 'sqrt' | 219 | amplitude = "sqrt" |
| 195 | elif self.amplitude == 1: | 220 | elif self.amplitude == 1: |
| 196 | amplitude = 'cbrt' | 221 | amplitude = "cbrt" |
| 197 | elif self.amplitude == 2: | 222 | elif self.amplitude == 2: |
| 198 | amplitude = '4thrt' | 223 | amplitude = "4thrt" |
| 199 | elif self.amplitude == 3: | 224 | elif self.amplitude == 3: |
| 200 | amplitude = '5thrt' | 225 | amplitude = "5thrt" |
| 201 | elif self.amplitude == 4: | 226 | elif self.amplitude == 4: |
| 202 | amplitude = 'lin' | 227 | amplitude = "lin" |
| 203 | elif self.amplitude == 5: | 228 | elif self.amplitude == 5: |
| 204 | amplitude = 'log' | 229 | amplitude = "log" |
| 205 | filter_ = ( | 230 | filter_ = ( |
| 206 | f'showspectrum=s={w}x{h}:' | 231 | f"showspectrum=s={w}x{h}:" |
| 207 | 'slide=scroll:' | 232 | "slide=scroll:" |
| 208 | f'win_func={self.page.comboBox_window.currentText()}:' | 233 | f"win_func={self.page.comboBox_window.currentText()}:" |
| 209 | f'color={color}:' | 234 | f"color={color}:" |
| 210 | f'scale={amplitude},' | 235 | f"scale={amplitude}," |
| 211 | 'colorkey=color=black:' | 236 | "colorkey=color=black:" |
| 212 | 'similarity=0.1:blend=0.5' | 237 | "similarity=0.1:blend=0.5" |
| 213 | ) | 238 | ) |
| 214 | elif self.filterType == 1: # Histogram | 239 | elif self.filterType == 1: # Histogram |
| 215 | if self.amplitude1 == 0: | 240 | if self.amplitude1 == 0: |
| 216 | amplitude = 'log' | 241 | amplitude = "log" |
| 217 | elif self.amplitude1 == 1: | 242 | elif self.amplitude1 == 1: |
| 218 | amplitude = 'lin' | 243 | amplitude = "lin" |
| 219 | if self.display == 0: | 244 | if self.display == 0: |
| 220 | display = 'log' | 245 | display = "log" |
| 221 | elif self.display == 1: | 246 | elif self.display == 1: |
| 222 | display = 'sqrt' | 247 | display = "sqrt" |
| 223 | elif self.display == 2: | 248 | elif self.display == 2: |
| 224 | display = 'cbrt' | 249 | display = "cbrt" |
| 225 | elif self.display == 3: | 250 | elif self.display == 3: |
| 226 | display = 'lin' | 251 | display = "lin" |
| 227 | elif self.display == 4: | 252 | elif self.display == 4: |
| 228 | display = 'rlog' | 253 | display = "rlog" |
| 229 | filter_ = ( | 254 | filter_ = ( |
| 230 | f'ahistogram=r={str(self.settings.value("outputFrameRate"))}:' | 255 | f'ahistogram=r={str(self.settings.value("outputFrameRate"))}:' |
| 231 | f's={w}x{h}:' | 256 | f"s={w}x{h}:" |
| 232 | 'dmode=separate:' | 257 | "dmode=separate:" |
| 233 | f'ascale={amplitude}:' | 258 | f"ascale={amplitude}:" |
| 234 | f'scale={display}' | 259 | f"scale={display}" |
| 235 | ) | 260 | ) |
| 236 | elif self.filterType == 2: # Vector Scope | 261 | elif self.filterType == 2: # Vector Scope |
| 237 | if self.amplitude2 == 0: | 262 | if self.amplitude2 == 0: |
| 238 | amplitude = 'log' | 263 | amplitude = "log" |
| 239 | elif self.amplitude2 == 1: | 264 | elif self.amplitude2 == 1: |
| 240 | amplitude = 'sqrt' | 265 | amplitude = "sqrt" |
| 241 | elif self.amplitude2 == 2: | 266 | elif self.amplitude2 == 2: |
| 242 | amplitude = 'cbrt' | 267 | amplitude = "cbrt" |
| 243 | elif self.amplitude2 == 3: | 268 | elif self.amplitude2 == 3: |
| 244 | amplitude = 'lin' | 269 | amplitude = "lin" |
| 245 | m = self.page.comboBox_mode.currentText() | 270 | m = self.page.comboBox_mode.currentText() |
| 246 | filter_ = ( | 271 | filter_ = ( |
| 247 | f'avectorscope=s={w}x{h}:' | 272 | f"avectorscope=s={w}x{h}:" |
| 248 | f'draw={"line" if self.draw else "dot"}:' | 273 | f'draw={"line" if self.draw else "dot"}:' |
| 249 | f'm={m}:' | 274 | f"m={m}:" |
| 250 | f'scale={amplitude}:' | 275 | f"scale={amplitude}:" |
| 251 | f'zoom={str(self.zoom)}' | 276 | f"zoom={str(self.zoom)}" |
| 252 | ) | 277 | ) |
| 253 | elif self.filterType == 3: # Musical Scale | 278 | elif self.filterType == 3: # Musical Scale |
| 254 | filter_ = ( | 279 | filter_ = ( |
| 255 | f'showcqt=r={str(self.settings.value("outputFrameRate"))}:' | 280 | f'showcqt=r={str(self.settings.value("outputFrameRate"))}:' |
| 256 | f's={w}x{h}:' | 281 | f"s={w}x{h}:" |
| 257 | 'count=30:' | 282 | "count=30:" |
| 258 | 'text=0:' | 283 | "text=0:" |
| 259 | f'tc={str(self.tc)},' | 284 | f"tc={str(self.tc)}," |
| 260 | 'colorkey=color=black:' | 285 | "colorkey=color=black:" |
| 261 | 'similarity=0.1:blend=0.5' | 286 | "similarity=0.1:blend=0.5" |
| 262 | ) | 287 | ) |
| 263 | elif self.filterType == 4: # Phase | 288 | elif self.filterType == 4: # Phase |
| 264 | filter_ = ( | 289 | filter_ = ( |
| 265 | f'aphasemeter=r={str(self.settings.value("outputFrameRate"))}:' | 290 | f'aphasemeter=r={str(self.settings.value("outputFrameRate"))}:' |
| 266 | f's={w}x{h}:' | 291 | f"s={w}x{h}:" |
| 267 | 'video=1 [atrash][vtmp1]; ' | 292 | "video=1 [atrash][vtmp1]; " |
| 268 | '[atrash] anullsink; ' | 293 | "[atrash] anullsink; " |
| 269 | '[vtmp1] colorkey=color=black:' | 294 | "[vtmp1] colorkey=color=black:" |
| 270 | 'similarity=0.1:blend=0.5, ' | 295 | "similarity=0.1:blend=0.5, " |
| 271 | 'crop=in_w/8:in_h:(in_w/8)*7:0 ' | 296 | "crop=in_w/8:in_h:(in_w/8)*7:0 " |
| 272 | ) | 297 | ) |
| 273 | return filter_ | 298 | return filter_ |
| 274 | 299 | ||
| 275 | |||
| 276 | if self.filterType < 2: | 300 | if self.filterType < 2: |
| 277 | exampleSnd = exampleSound('freq') | 301 | exampleSnd = exampleSound("freq") |
| 278 | elif self.filterType == 2 or self.filterType == 4: | 302 | elif self.filterType == 2 or self.filterType == 4: |
| 279 | exampleSnd = exampleSound('stereo') | 303 | exampleSnd = exampleSound("stereo") |
| 280 | elif self.filterType == 3: | 304 | elif self.filterType == 3: |
| 281 | exampleSnd = exampleSound('white') | 305 | exampleSnd = exampleSound("white") |
| 282 | compression = 'compand=gain=4,' if self.compress else '' | 306 | compression = "compand=gain=4," if self.compress else "" |
| 283 | aformat = 'aformat=channel_layouts=mono,' if self.mono and self.filterType not in (2, 4) else '' | 307 | aformat = ( |
| 308 | "aformat=channel_layouts=mono," | ||
| 309 | if self.mono and self.filterType not in (2, 4) | ||
| 310 | else "" | ||
| 311 | ) | ||
| 284 | filter_ = getFilterComplexCommandForType() | 312 | filter_ = getFilterComplexCommandForType() |
| 285 | hflip = 'hflip, ' if self.mirror else '' | 313 | hflip = "hflip, " if self.mirror else "" |
| 286 | trim = 'trim=start=%s:end=%s, ' % ("{0:.3f}".format(startPt + 12), "{0:.3f}".format(startPt + 12.5)) if preview else '' | 314 | trim = ( |
| 287 | scale_ = 'scale=%sx%s' % scale(self.scale, self.width, self.height, str) | 315 | "trim=start=%s:end=%s, " |
| 288 | hue = ', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 and self.filterType != 3 else '' | 316 | % ( |
| 289 | convolution = ', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2' if self.filterType == 3 else '' | 317 | "{0:.3f}".format(startPt + 12), |
| 290 | 318 | "{0:.3f}".format(startPt + 12.5), | |
| 319 | ) | ||
| 320 | if preview | ||
| 321 | else "" | ||
| 322 | ) | ||
| 323 | scale_ = "scale=%sx%s" % scale(self.scale, self.width, self.height, str) | ||
| 324 | hue = ( | ||
| 325 | ", hue=h=%s:s=10" % str(self.hue) | ||
| 326 | if self.hue > 0 and self.filterType != 3 | ||
| 327 | else "" | ||
| 328 | ) | ||
| 329 | convolution = ( | ||
| 330 | ", convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2" | ||
| 331 | if self.filterType == 3 | ||
| 332 | else "" | ||
| 333 | ) | ||
| 334 | |||
| 291 | return ( | 335 | return ( |
| 292 | f"{exampleSnd if preview and genericPreview else '[0:a] '}" | 336 | f"{exampleSnd if preview and genericPreview else '[0:a] '}" |
| 293 | f"{compression}{aformat}{filter_} [v1]; " | 337 | f"{compression}{aformat}{filter_} [v1]; " |
| 294 | f"[v1] {hflip}{trim}{scale_}{hue}{convolution} [v]" | 338 | f"[v1] {hflip}{trim}{scale_}{hue}{convolution} [v]" |
| 295 | ) | 339 | ) |
| 296 | 340 | ||
| 297 | |||
| 298 | return [ | 341 | return [ |
| 299 | '-filter_complex', | 342 | "-filter_complex", |
| 300 | getFilterComplexCommand(), | 343 | getFilterComplexCommand(), |
| 301 | '-map', '[v]', | 344 | "-map", |
| 345 | "[v]", | ||
| 302 | ] | 346 | ] |
| 303 | 347 | ||
| 304 | def updateChunksize(self): | 348 | def updateChunksize(self): |
| @@ -311,9 +355,9 @@ class Component(Component): | |||
| 311 | def finalizeFrame(self, imageData): | 355 | def finalizeFrame(self, imageData): |
| 312 | try: | 356 | try: |
| 313 | image = Image.frombytes( | 357 | image = Image.frombytes( |
| 314 | 'RGBA', | 358 | "RGBA", |
| 315 | scale(self.scale, self.width, self.height, int), | 359 | scale(self.scale, self.width, self.height, int), |
| 316 | imageData | 360 | imageData, |
| 317 | ) | 361 | ) |
| 318 | self._image = image | 362 | self._image = image |
| 319 | except ValueError: | 363 | except ValueError: |
diff --git a/src/components/text.py b/src/components/text.py index 3238d2a..40c981a 100644 --- a/src/components/text.py +++ b/src/components/text.py | |||
| @@ -1,22 +1,22 @@ | |||
| 1 | from PIL import ImageEnhance, ImageFilter, ImageChops | 1 | from PIL import ImageEnhance, ImageFilter, ImageChops |
| 2 | from PyQt5.QtGui import QColor, QFont | 2 | from PyQt6.QtGui import QColor, QFont |
| 3 | from PyQt5 import QtGui, QtCore, QtWidgets | 3 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 4 | import os | 4 | import os |
| 5 | import logging | 5 | import logging |
| 6 | 6 | ||
| 7 | from ..component import Component | 7 | from ..component import Component |
| 8 | from ..toolkit.frame import FramePainter, PaintColor | 8 | from ..toolkit.frame import FramePainter, PaintColor |
| 9 | 9 | ||
| 10 | log = logging.getLogger('AVP.Components.Text') | 10 | log = logging.getLogger("AVP.Components.Text") |
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | class Component(Component): | 13 | class Component(Component): |
| 14 | name = 'Title Text' | 14 | name = "Title Text" |
| 15 | version = '1.0.1' | 15 | version = "1.0.1" |
| 16 | 16 | ||
| 17 | def widget(self, *args): | 17 | def widget(self, *args): |
| 18 | super().widget(*args) | 18 | super().widget(*args) |
| 19 | self.title = 'Text' | 19 | self.title = "Text" |
| 20 | self.alignment = 1 | 20 | self.alignment = 1 |
| 21 | self.titleFont = QFont() | 21 | self.titleFont = QFont() |
| 22 | self.fontSize = self.height / 13.5 | 22 | self.fontSize = self.height / 13.5 |
| @@ -29,33 +29,44 @@ class Component(Component): | |||
| 29 | self.page.lineEdit_title.setText(self.title) | 29 | self.page.lineEdit_title.setText(self.title) |
| 30 | self.page.pushButton_center.clicked.connect(self.centerXY) | 30 | self.page.pushButton_center.clicked.connect(self.centerXY) |
| 31 | 31 | ||
| 32 | self.page.fontComboBox_titleFont.currentFontChanged.connect(self._sendUpdateSignal) | 32 | self.page.fontComboBox_titleFont.currentFontChanged.connect( |
| 33 | self._sendUpdateSignal | ||
| 34 | ) | ||
| 33 | # The QFontComboBox must be connected directly to the Qt Signal | 35 | # The QFontComboBox must be connected directly to the Qt Signal |
| 34 | # which triggers the preview to update. | 36 | # which triggers the preview to update. |
| 35 | # This unfortunately makes changing the font into a non-undoable action. | 37 | # This unfortunately makes changing the font into a non-undoable action. |
| 36 | # Must be something broken in the conversion to a ComponentAction | 38 | # Must be something broken in the conversion to a ComponentAction |
| 37 | 39 | ||
| 38 | self.trackWidgets({ | 40 | self.trackWidgets( |
| 39 | 'textColor': self.page.lineEdit_textColor, | 41 | { |
| 40 | 'title': self.page.lineEdit_title, | 42 | "textColor": self.page.lineEdit_textColor, |
| 41 | 'alignment': self.page.comboBox_textAlign, | 43 | "title": self.page.lineEdit_title, |
| 42 | 'fontSize': self.page.spinBox_fontSize, | 44 | "alignment": self.page.comboBox_textAlign, |
| 43 | 'xPosition': self.page.spinBox_xTextAlign, | 45 | "fontSize": self.page.spinBox_fontSize, |
| 44 | 'yPosition': self.page.spinBox_yTextAlign, | 46 | "xPosition": self.page.spinBox_xTextAlign, |
| 45 | 'fontStyle': self.page.comboBox_fontStyle, | 47 | "yPosition": self.page.spinBox_yTextAlign, |
| 46 | 'stroke': self.page.spinBox_stroke, | 48 | "fontStyle": self.page.comboBox_fontStyle, |
| 47 | 'strokeColor': self.page.lineEdit_strokeColor, | 49 | "stroke": self.page.spinBox_stroke, |
| 48 | 'shadow': self.page.checkBox_shadow, | 50 | "strokeColor": self.page.lineEdit_strokeColor, |
| 49 | 'shadX': self.page.spinBox_shadX, | 51 | "shadow": self.page.checkBox_shadow, |
| 50 | 'shadY': self.page.spinBox_shadY, | 52 | "shadX": self.page.spinBox_shadX, |
| 51 | 'shadBlur': self.page.spinBox_shadBlur, | 53 | "shadY": self.page.spinBox_shadY, |
| 52 | }, colorWidgets={ | 54 | "shadBlur": self.page.spinBox_shadBlur, |
| 53 | 'textColor': self.page.pushButton_textColor, | 55 | }, |
| 54 | 'strokeColor': self.page.pushButton_strokeColor, | 56 | colorWidgets={ |
| 55 | }, relativeWidgets=[ | 57 | "textColor": self.page.pushButton_textColor, |
| 56 | 'xPosition', 'yPosition', 'fontSize', | 58 | "strokeColor": self.page.pushButton_strokeColor, |
| 57 | 'stroke', 'shadX', 'shadY', 'shadBlur' | 59 | }, |
| 58 | ]) | 60 | relativeWidgets=[ |
| 61 | "xPosition", | ||
| 62 | "yPosition", | ||
| 63 | "fontSize", | ||
| 64 | "stroke", | ||
| 65 | "shadX", | ||
| 66 | "shadY", | ||
| 67 | "shadBlur", | ||
| 68 | ], | ||
| 69 | ) | ||
| 59 | self.centerXY() | 70 | self.centerXY() |
| 60 | 71 | ||
| 61 | def update(self): | 72 | def update(self): |
| @@ -74,20 +85,23 @@ class Component(Component): | |||
| 74 | self.page.spinBox_shadBlur.setHidden(True) | 85 | self.page.spinBox_shadBlur.setHidden(True) |
| 75 | 86 | ||
| 76 | def centerXY(self): | 87 | def centerXY(self): |
| 77 | self.setRelativeWidget('xPosition', 0.5) | 88 | self.setRelativeWidget("xPosition", 0.5) |
| 78 | self.setRelativeWidget('yPosition', 0.521) | 89 | self.setRelativeWidget("yPosition", 0.521) |
| 79 | 90 | ||
| 80 | def getXY(self): | 91 | def getXY(self): |
| 81 | '''Returns true x, y after considering alignment settings''' | 92 | """Returns true x, y after considering alignment settings""" |
| 82 | fm = QtGui.QFontMetrics(self.titleFont) | 93 | fm = QtGui.QFontMetrics(self.titleFont) |
| 83 | x = self.pixelValForAttr('xPosition') | 94 | text_width = fm.boundingRect(self.title).width() |
| 95 | x = self.pixelValForAttr("xPosition") | ||
| 84 | 96 | ||
| 85 | if self.alignment == 1: # Middle | 97 | if self.alignment == 1: # Middle |
| 86 | offset = int(fm.width(self.title)/2) | 98 | offset = int(text_width / 2) |
| 87 | x -= offset | 99 | elif self.alignment == 2: # Right |
| 88 | if self.alignment == 2: # Right | 100 | offset = text_width |
| 89 | offset = fm.width(self.title) | 101 | else: |
| 90 | x -= offset | 102 | raise ValueError(f"Alignment value {self.alignment} unknown") |
| 103 | |||
| 104 | x -= offset | ||
| 91 | 105 | ||
| 92 | return x, self.yPosition | 106 | return x, self.yPosition |
| 93 | 107 | ||
| @@ -95,21 +109,21 @@ class Component(Component): | |||
| 95 | super().loadPreset(pr, *args) | 109 | super().loadPreset(pr, *args) |
| 96 | 110 | ||
| 97 | font = QFont() | 111 | font = QFont() |
| 98 | font.fromString(pr['titleFont']) | 112 | font.fromString(pr["titleFont"]) |
| 99 | self.page.fontComboBox_titleFont.setCurrentFont(font) | 113 | self.page.fontComboBox_titleFont.setCurrentFont(font) |
| 100 | 114 | ||
| 101 | def savePreset(self): | 115 | def savePreset(self): |
| 102 | saveValueStore = super().savePreset() | 116 | saveValueStore = super().savePreset() |
| 103 | saveValueStore['titleFont'] = self.titleFont.toString() | 117 | saveValueStore["titleFont"] = self.titleFont.toString() |
| 104 | return saveValueStore | 118 | return saveValueStore |
| 105 | 119 | ||
| 106 | def previewRender(self): | 120 | def previewRender(self): |
| 107 | return self.addText(self.width, self.height) | 121 | return self.addText(self.width, self.height) |
| 108 | 122 | ||
| 109 | def properties(self): | 123 | def properties(self): |
| 110 | props = ['static'] | 124 | props = ["static"] |
| 111 | if not self.title: | 125 | if not self.title: |
| 112 | props.append('error') | 126 | props.append("error") |
| 113 | return props | 127 | return props |
| 114 | 128 | ||
| 115 | def error(self): | 129 | def error(self): |
| @@ -121,26 +135,26 @@ class Component(Component): | |||
| 121 | def addText(self, width, height): | 135 | def addText(self, width, height): |
| 122 | font = self.titleFont | 136 | font = self.titleFont |
| 123 | font.setPixelSize(self.fontSize) | 137 | font.setPixelSize(self.fontSize) |
| 124 | font.setStyle(QFont.StyleNormal) | 138 | font.setStyle(QFont.Style.StyleNormal) |
| 125 | font.setWeight(QFont.Normal) | 139 | font.setWeight(QFont.Weight.Normal) |
| 126 | font.setCapitalization(QFont.MixedCase) | 140 | font.setCapitalization(QFont.Capitalization.MixedCase) |
| 127 | if self.fontStyle == 1: | 141 | if self.fontStyle == 1: |
| 128 | font.setWeight(QFont.DemiBold) | 142 | font.setWeight(QFont.Weight.DemiBold) |
| 129 | if self.fontStyle == 2: | 143 | if self.fontStyle == 2: |
| 130 | font.setWeight(QFont.Bold) | 144 | font.setWeight(QFont.Weight.Bold) |
| 131 | elif self.fontStyle == 3: | 145 | elif self.fontStyle == 3: |
| 132 | font.setStyle(QFont.StyleItalic) | 146 | font.setStyle(QFont.Style.StyleItalic) |
| 133 | elif self.fontStyle == 4: | 147 | elif self.fontStyle == 4: |
| 134 | font.setWeight(QFont.Bold) | 148 | font.setWeight(QFont.Weight.Bold) |
| 135 | font.setStyle(QFont.StyleItalic) | 149 | font.setStyle(QFont.Style.StyleItalic) |
| 136 | elif self.fontStyle == 5: | 150 | elif self.fontStyle == 5: |
| 137 | font.setStyle(QFont.StyleOblique) | 151 | font.setStyle(QFont.Style.StyleOblique) |
| 138 | elif self.fontStyle == 6: | 152 | elif self.fontStyle == 6: |
| 139 | font.setCapitalization(QFont.SmallCaps) | 153 | font.setCapitalization(QFont.Capitalization.SmallCaps) |
| 140 | 154 | ||
| 141 | image = FramePainter(width, height) | 155 | image = FramePainter(width, height) |
| 142 | x, y = self.getXY() | 156 | x, y = self.getXY() |
| 143 | log.debug('Text position translates to %s, %s', x, y) | 157 | log.debug("Text position translates to %s, %s", x, y) |
| 144 | if self.stroke > 0: | 158 | if self.stroke > 0: |
| 145 | outliner = QtGui.QPainterPathStroker() | 159 | outliner = QtGui.QPainterPathStroker() |
| 146 | outliner.setWidth(self.stroke) | 160 | outliner.setWidth(self.stroke) |
| @@ -149,16 +163,16 @@ class Component(Component): | |||
| 149 | # PathStroker ignores smallcaps so we need this weird hack | 163 | # PathStroker ignores smallcaps so we need this weird hack |
| 150 | path.addText(x, y, font, self.title[0]) | 164 | path.addText(x, y, font, self.title[0]) |
| 151 | fm = QtGui.QFontMetrics(font) | 165 | fm = QtGui.QFontMetrics(font) |
| 152 | newX = x + fm.width(self.title[0]) | 166 | newX = x + fm.boundingRect(self.title[0]).width() |
| 153 | strokeFont = self.page.fontComboBox_titleFont.currentFont() | 167 | strokeFont = self.page.fontComboBox_titleFont.currentFont() |
| 154 | strokeFont.setCapitalization(QFont.SmallCaps) | 168 | strokeFont.setCapitalization(QFont.Capitalization.SmallCaps) |
| 155 | strokeFont.setPixelSize(int((self.fontSize / 7) * 5)) | 169 | strokeFont.setPixelSize(int((self.fontSize / 7) * 5)) |
| 156 | strokeFont.setLetterSpacing(QFont.PercentageSpacing, 139) | 170 | strokeFont.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 139) |
| 157 | path.addText(newX, y, strokeFont, self.title[1:]) | 171 | path.addText(newX, y, strokeFont, self.title[1:]) |
| 158 | else: | 172 | else: |
| 159 | path.addText(x, y, font, self.title) | 173 | path.addText(x, y, font, self.title) |
| 160 | path = outliner.createStroke(path) | 174 | path = outliner.createStroke(path) |
| 161 | image.setPen(QtCore.Qt.NoPen) | 175 | image.setPen(QtCore.Qt.PenStyle.NoPen) |
| 162 | image.setBrush(PaintColor(*self.strokeColor)) | 176 | image.setBrush(PaintColor(*self.strokeColor)) |
| 163 | image.drawPath(path) | 177 | image.drawPath(path) |
| 164 | 178 | ||
| @@ -178,27 +192,27 @@ class Component(Component): | |||
| 178 | return frame | 192 | return frame |
| 179 | 193 | ||
| 180 | def commandHelp(self): | 194 | def commandHelp(self): |
| 181 | print('Enter a string to use as centred white text:') | 195 | print("Enter a string to use as centred white text:") |
| 182 | print(' "title=User Error"') | 196 | print(' "title=User Error"') |
| 183 | print('Specify a text color:\n color=255,255,255') | 197 | print("Specify a text color:\n color=255,255,255") |
| 184 | print('Set custom x, y position:\n x=500 y=500') | 198 | print("Set custom x, y position:\n x=500 y=500") |
| 185 | 199 | ||
| 186 | def command(self, arg): | 200 | def command(self, arg): |
| 187 | if '=' in arg: | 201 | if "=" in arg: |
| 188 | key, arg = arg.split('=', 1) | 202 | key, arg = arg.split("=", 1) |
| 189 | if key == 'color': | 203 | if key == "color": |
| 190 | self.page.lineEdit_textColor.setText(arg) | 204 | self.page.lineEdit_textColor.setText(arg) |
| 191 | return | 205 | return |
| 192 | elif key == 'size': | 206 | elif key == "size": |
| 193 | self.page.spinBox_fontSize.setValue(int(arg)) | 207 | self.page.spinBox_fontSize.setValue(int(arg)) |
| 194 | return | 208 | return |
| 195 | elif key == 'x': | 209 | elif key == "x": |
| 196 | self.page.spinBox_xTextAlign.setValue(int(arg)) | 210 | self.page.spinBox_xTextAlign.setValue(int(arg)) |
| 197 | return | 211 | return |
| 198 | elif key == 'y': | 212 | elif key == "y": |
| 199 | self.page.spinBox_yTextAlign.setValue(int(arg)) | 213 | self.page.spinBox_yTextAlign.setValue(int(arg)) |
| 200 | return | 214 | return |
| 201 | elif key == 'title': | 215 | elif key == "title": |
| 202 | self.page.lineEdit_title.setText(arg) | 216 | self.page.lineEdit_title.setText(arg) |
| 203 | return | 217 | return |
| 204 | super().command(arg) | 218 | super().command(arg) |
diff --git a/src/components/video.py b/src/components/video.py index 60ca800..65a05af 100644 --- a/src/components/video.py +++ b/src/components/video.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | from PIL import Image | 1 | from PIL import Image |
| 2 | from PyQt5 import QtGui, QtCore, QtWidgets | 2 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 3 | import os | 3 | import os |
| 4 | import math | 4 | import math |
| 5 | import subprocess | 5 | import subprocess |
| @@ -11,15 +11,15 @@ from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo | |||
| 11 | from ..toolkit import checkOutput | 11 | from ..toolkit import checkOutput |
| 12 | 12 | ||
| 13 | 13 | ||
| 14 | log = logging.getLogger('AVP.Components.Video') | 14 | log = logging.getLogger("AVP.Components.Video") |
| 15 | 15 | ||
| 16 | 16 | ||
| 17 | class Component(Component): | 17 | class Component(Component): |
| 18 | name = 'Video' | 18 | name = "Video" |
| 19 | version = '1.0.0' | 19 | version = "1.0.0" |
| 20 | 20 | ||
| 21 | def widget(self, *args): | 21 | def widget(self, *args): |
| 22 | self.videoPath = '' | 22 | self.videoPath = "" |
| 23 | self.badAudio = False | 23 | self.badAudio = False |
| 24 | self.x = 0 | 24 | self.x = 0 |
| 25 | self.y = 0 | 25 | self.y = 0 |
| @@ -27,23 +27,28 @@ class Component(Component): | |||
| 27 | super().widget(*args) | 27 | super().widget(*args) |
| 28 | self._image = BlankFrame(self.width, self.height) | 28 | self._image = BlankFrame(self.width, self.height) |
| 29 | self.page.pushButton_video.clicked.connect(self.pickVideo) | 29 | self.page.pushButton_video.clicked.connect(self.pickVideo) |
| 30 | self.trackWidgets({ | 30 | self.trackWidgets( |
| 31 | 'videoPath': self.page.lineEdit_video, | 31 | { |
| 32 | 'loopVideo': self.page.checkBox_loop, | 32 | "videoPath": self.page.lineEdit_video, |
| 33 | 'useAudio': self.page.checkBox_useAudio, | 33 | "loopVideo": self.page.checkBox_loop, |
| 34 | 'distort': self.page.checkBox_distort, | 34 | "useAudio": self.page.checkBox_useAudio, |
| 35 | 'scale': self.page.spinBox_scale, | 35 | "distort": self.page.checkBox_distort, |
| 36 | 'volume': self.page.spinBox_volume, | 36 | "scale": self.page.spinBox_scale, |
| 37 | 'xPosition': self.page.spinBox_x, | 37 | "volume": self.page.spinBox_volume, |
| 38 | 'yPosition': self.page.spinBox_y, | 38 | "xPosition": self.page.spinBox_x, |
| 39 | }, presetNames={ | 39 | "yPosition": self.page.spinBox_y, |
| 40 | 'videoPath': 'video', | 40 | }, |
| 41 | 'loopVideo': 'loop', | 41 | presetNames={ |
| 42 | 'xPosition': 'x', | 42 | "videoPath": "video", |
| 43 | 'yPosition': 'y', | 43 | "loopVideo": "loop", |
| 44 | }, relativeWidgets=[ | 44 | "xPosition": "x", |
| 45 | 'xPosition', 'yPosition', | 45 | "yPosition": "y", |
| 46 | ]) | 46 | }, |
| 47 | relativeWidgets=[ | ||
| 48 | "xPosition", | ||
| 49 | "yPosition", | ||
| 50 | ], | ||
| 51 | ) | ||
| 47 | 52 | ||
| 48 | def update(self): | 53 | def update(self): |
| 49 | if self.page.checkBox_useAudio.isChecked(): | 54 | if self.page.checkBox_useAudio.isChecked(): |
| @@ -64,7 +69,7 @@ class Component(Component): | |||
| 64 | def properties(self): | 69 | def properties(self): |
| 65 | props = [] | 70 | props = [] |
| 66 | outputFile = None | 71 | outputFile = None |
| 67 | if hasattr(self.parent, 'lineEdit_outputFile'): | 72 | if hasattr(self.parent, "lineEdit_outputFile"): |
| 68 | # check only happens in GUI mode | 73 | # check only happens in GUI mode |
| 69 | outputFile = self.parent.lineEdit_outputFile.text() | 74 | outputFile = self.parent.lineEdit_outputFile.text() |
| 70 | 75 | ||
| @@ -72,34 +77,42 @@ class Component(Component): | |||
| 72 | self.lockError("There is no video selected.") | 77 | self.lockError("There is no video selected.") |
| 73 | elif not os.path.exists(self.videoPath): | 78 | elif not os.path.exists(self.videoPath): |
| 74 | self.lockError("The video selected does not exist!") | 79 | self.lockError("The video selected does not exist!") |
| 75 | elif outputFile and os.path.realpath(self.videoPath) == os.path.realpath(outputFile): | 80 | elif outputFile and os.path.realpath(self.videoPath) == os.path.realpath( |
| 81 | outputFile | ||
| 82 | ): | ||
| 76 | self.lockError("Input and output paths match.") | 83 | self.lockError("Input and output paths match.") |
| 77 | 84 | ||
| 78 | if self.useAudio: | 85 | if self.useAudio: |
| 79 | props.append('audio') | 86 | props.append("audio") |
| 80 | if not testAudioStream(self.videoPath) \ | 87 | if not testAudioStream(self.videoPath) and self.error() is None: |
| 81 | and self.error() is None: | 88 | self.lockError("Could not identify an audio stream in this video.") |
| 82 | self.lockError( | ||
| 83 | "Could not identify an audio stream in this video.") | ||
| 84 | 89 | ||
| 85 | return props | 90 | return props |
| 86 | 91 | ||
| 87 | def audio(self): | 92 | def audio(self): |
| 88 | params = {} | 93 | params = {} |
| 89 | if self.volume != 1.0: | 94 | if self.volume != 1.0: |
| 90 | params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume) | 95 | params["volume"] = "=%s:replaygain_noclip=0" % str(self.volume) |
| 91 | return (self.videoPath, params) | 96 | return (self.videoPath, params) |
| 92 | 97 | ||
| 93 | def preFrameRender(self, **kwargs): | 98 | def preFrameRender(self, **kwargs): |
| 94 | super().preFrameRender(**kwargs) | 99 | super().preFrameRender(**kwargs) |
| 95 | self.updateChunksize() | 100 | self.updateChunksize() |
| 96 | self.video = FfmpegVideo( | 101 | self.video = ( |
| 97 | inputPath=self.videoPath, filter_=self.makeFfmpegFilter(), | 102 | FfmpegVideo( |
| 98 | width=self.width, height=self.height, chunkSize=self.chunkSize, | 103 | inputPath=self.videoPath, |
| 99 | frameRate=int(self.settings.value("outputFrameRate")), | 104 | filter_=self.makeFfmpegFilter(), |
| 100 | parent=self.parent, loopVideo=self.loopVideo, | 105 | width=self.width, |
| 101 | component=self | 106 | height=self.height, |
| 102 | ) if os.path.exists(self.videoPath) else None | 107 | chunkSize=self.chunkSize, |
| 108 | frameRate=int(self.settings.value("outputFrameRate")), | ||
| 109 | parent=self.parent, | ||
| 110 | loopVideo=self.loopVideo, | ||
| 111 | component=self, | ||
| 112 | ) | ||
| 113 | if os.path.exists(self.videoPath) | ||
| 114 | else None | ||
| 115 | ) | ||
| 103 | 116 | ||
| 104 | def frameRender(self, frameNo): | 117 | def frameRender(self, frameNo): |
| 105 | if FfmpegVideo.threadError is not None: | 118 | if FfmpegVideo.threadError is not None: |
| @@ -112,8 +125,10 @@ class Component(Component): | |||
| 112 | def pickVideo(self): | 125 | def pickVideo(self): |
| 113 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) | 126 | imgDir = self.settings.value("componentDir", os.path.expanduser("~")) |
| 114 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 127 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 115 | self.page, "Choose Video", | 128 | self.page, |
| 116 | imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats) | 129 | "Choose Video", |
| 130 | imgDir, | ||
| 131 | "Video Files (%s)" % " ".join(self.core.videoFormats), | ||
| 117 | ) | 132 | ) |
| 118 | if filename: | 133 | if filename: |
| 119 | self.settings.setValue("componentDir", os.path.dirname(filename)) | 134 | self.settings.setValue("componentDir", os.path.dirname(filename)) |
| @@ -127,33 +142,50 @@ class Component(Component): | |||
| 127 | 142 | ||
| 128 | command = [ | 143 | command = [ |
| 129 | self.core.FFMPEG_BIN, | 144 | self.core.FFMPEG_BIN, |
| 130 | '-thread_queue_size', '512', | 145 | "-thread_queue_size", |
| 131 | '-i', self.videoPath, | 146 | "512", |
| 132 | '-f', 'image2pipe', | 147 | "-i", |
| 133 | '-pix_fmt', 'rgba', | 148 | self.videoPath, |
| 149 | "-f", | ||
| 150 | "image2pipe", | ||
| 151 | "-pix_fmt", | ||
| 152 | "rgba", | ||
| 134 | ] | 153 | ] |
| 135 | command.extend(self.makeFfmpegFilter()) | 154 | command.extend(self.makeFfmpegFilter()) |
| 136 | command.extend([ | 155 | command.extend( |
| 137 | '-codec:v', 'rawvideo', '-', | 156 | [ |
| 138 | '-ss', '90', | 157 | "-codec:v", |
| 139 | '-frames:v', '1', | 158 | "rawvideo", |
| 140 | ]) | 159 | "-", |
| 160 | "-ss", | ||
| 161 | "90", | ||
| 162 | "-frames:v", | ||
| 163 | "1", | ||
| 164 | ] | ||
| 165 | ) | ||
| 141 | 166 | ||
| 142 | if self.core.logEnabled: | 167 | if self.core.logEnabled: |
| 143 | logFilename = os.path.join( | 168 | logFilename = os.path.join( |
| 144 | self.core.logDir, 'preview_%s.log' % str(self.compPos)) | 169 | self.core.logDir, "preview_%s.log" % str(self.compPos) |
| 145 | log.debug('Creating ffmpeg process (log at %s)' % logFilename) | 170 | ) |
| 146 | with open(logFilename, 'w') as logf: | 171 | log.debug("Creating ffmpeg process (log at %s)" % logFilename) |
| 147 | logf.write(" ".join(command) + '\n\n') | 172 | with open(logFilename, "w") as logf: |
| 148 | with open(logFilename, 'a') as logf: | 173 | logf.write(" ".join(command) + "\n\n") |
| 174 | with open(logFilename, "a") as logf: | ||
| 149 | pipe = openPipe( | 175 | pipe = openPipe( |
| 150 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 176 | command, |
| 151 | stderr=logf, bufsize=10**8 | 177 | stdin=subprocess.DEVNULL, |
| 178 | stdout=subprocess.PIPE, | ||
| 179 | stderr=logf, | ||
| 180 | bufsize=10**8, | ||
| 152 | ) | 181 | ) |
| 153 | else: | 182 | else: |
| 154 | pipe = openPipe( | 183 | pipe = openPipe( |
| 155 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 184 | command, |
| 156 | stderr=subprocess.DEVNULL, bufsize=10**8 | 185 | stdin=subprocess.DEVNULL, |
| 186 | stdout=subprocess.PIPE, | ||
| 187 | stderr=subprocess.DEVNULL, | ||
| 188 | bufsize=10**8, | ||
| 157 | ) | 189 | ) |
| 158 | 190 | ||
| 159 | byteFrame = pipe.stdout.read(self.chunkSize) | 191 | byteFrame = pipe.stdout.read(self.chunkSize) |
| @@ -164,9 +196,8 @@ class Component(Component): | |||
| 164 | 196 | ||
| 165 | def makeFfmpegFilter(self): | 197 | def makeFfmpegFilter(self): |
| 166 | return [ | 198 | return [ |
| 167 | '-filter_complex', | 199 | "-filter_complex", |
| 168 | '[0:v] scale=%s:%s' % scale( | 200 | "[0:v] scale=%s:%s" % scale(self.scale, self.width, self.height, str), |
| 169 | self.scale, self.width, self.height, str), | ||
| 170 | ] | 201 | ] |
| 171 | 202 | ||
| 172 | def updateChunksize(self): | 203 | def updateChunksize(self): |
| @@ -177,10 +208,10 @@ class Component(Component): | |||
| 177 | self.chunkSize = 4 * width * height | 208 | self.chunkSize = 4 * width * height |
| 178 | 209 | ||
| 179 | def command(self, arg): | 210 | def command(self, arg): |
| 180 | if '=' in arg: | 211 | if "=" in arg: |
| 181 | key, arg = arg.split('=', 1) | 212 | key, arg = arg.split("=", 1) |
| 182 | if key == 'path' and os.path.exists(arg): | 213 | if key == "path" and os.path.exists(arg): |
| 183 | if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats: | 214 | if "*%s" % os.path.splitext(arg)[1] in self.core.videoFormats: |
| 184 | self.page.lineEdit_video.setText(arg) | 215 | self.page.lineEdit_video.setText(arg) |
| 185 | self.page.spinBox_scale.setValue(100) | 216 | self.page.spinBox_scale.setValue(100) |
| 186 | self.page.checkBox_loop.setChecked(True) | 217 | self.page.checkBox_loop.setChecked(True) |
| @@ -188,7 +219,7 @@ class Component(Component): | |||
| 188 | else: | 219 | else: |
| 189 | print("Not a supported video format") | 220 | print("Not a supported video format") |
| 190 | quit(1) | 221 | quit(1) |
| 191 | elif arg == 'audio': | 222 | elif arg == "audio": |
| 192 | if not self.page.lineEdit_video.text(): | 223 | if not self.page.lineEdit_video.text(): |
| 193 | print("'audio' option must follow a video selection") | 224 | print("'audio' option must follow a video selection") |
| 194 | quit(1) | 225 | quit(1) |
| @@ -197,28 +228,25 @@ class Component(Component): | |||
| 197 | super().command(arg) | 228 | super().command(arg) |
| 198 | 229 | ||
| 199 | def commandHelp(self): | 230 | def commandHelp(self): |
| 200 | print('Load a video:\n path=/filepath/to/video.mp4') | 231 | print("Load a video:\n path=/filepath/to/video.mp4") |
| 201 | print('Using audio:\n path=/filepath/to/video.mp4 audio') | 232 | print("Using audio:\n path=/filepath/to/video.mp4 audio") |
| 202 | 233 | ||
| 203 | def finalizeFrame(self, imageData): | 234 | def finalizeFrame(self, imageData): |
| 204 | try: | 235 | try: |
| 205 | if self.distort: | 236 | if self.distort: |
| 206 | image = Image.frombytes( | 237 | image = Image.frombytes("RGBA", (self.width, self.height), imageData) |
| 207 | 'RGBA', | ||
| 208 | (self.width, self.height), | ||
| 209 | imageData) | ||
| 210 | else: | 238 | else: |
| 211 | image = Image.frombytes( | 239 | image = Image.frombytes( |
| 212 | 'RGBA', | 240 | "RGBA", |
| 213 | scale(self.scale, self.width, self.height, int), | 241 | scale(self.scale, self.width, self.height, int), |
| 214 | imageData) | 242 | imageData, |
| 243 | ) | ||
| 215 | self._image = image | 244 | self._image = image |
| 216 | except ValueError: | 245 | except ValueError: |
| 217 | # use last good frame | 246 | # use last good frame |
| 218 | image = self._image | 247 | image = self._image |
| 219 | 248 | ||
| 220 | if self.scale != 100 \ | 249 | if self.scale != 100 or self.xPosition != 0 or self.yPosition != 0: |
| 221 | or self.xPosition != 0 or self.yPosition != 0: | ||
| 222 | frame = BlankFrame(self.width, self.height) | 250 | frame = BlankFrame(self.width, self.height) |
| 223 | frame.paste(image, box=(self.xPosition, self.yPosition)) | 251 | frame.paste(image, box=(self.xPosition, self.yPosition)) |
| 224 | else: | 252 | else: |
diff --git a/src/components/waveform.py b/src/components/waveform.py index eef6de0..7dc0b99 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | from PIL import Image | 1 | from PIL import Image |
| 2 | from PyQt5 import QtGui, QtCore, QtWidgets | 2 | from PyQt6 import QtGui, QtCore, QtWidgets |
| 3 | from PyQt5.QtGui import QColor | 3 | from PyQt6.QtGui import QColor |
| 4 | import os | 4 | import os |
| 5 | import math | 5 | import math |
| 6 | import subprocess | 6 | import subprocess |
| @@ -10,44 +10,51 @@ from ..component import Component | |||
| 10 | from ..toolkit.frame import BlankFrame, scale | 10 | from ..toolkit.frame import BlankFrame, scale |
| 11 | from ..toolkit import checkOutput | 11 | from ..toolkit import checkOutput |
| 12 | from ..toolkit.ffmpeg import ( | 12 | from ..toolkit.ffmpeg import ( |
| 13 | openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound | 13 | openPipe, |
| 14 | closePipe, | ||
| 15 | getAudioDuration, | ||
| 16 | FfmpegVideo, | ||
| 17 | exampleSound, | ||
| 14 | ) | 18 | ) |
| 15 | 19 | ||
| 16 | 20 | ||
| 17 | log = logging.getLogger('AVP.Components.Waveform') | 21 | log = logging.getLogger("AVP.Components.Waveform") |
| 18 | 22 | ||
| 19 | 23 | ||
| 20 | class Component(Component): | 24 | class Component(Component): |
| 21 | name = 'Waveform' | 25 | name = "Waveform" |
| 22 | version = '1.0.0' | 26 | version = "1.0.0" |
| 23 | 27 | ||
| 24 | def widget(self, *args): | 28 | def widget(self, *args): |
| 25 | super().widget(*args) | 29 | super().widget(*args) |
| 26 | self._image = BlankFrame(self.width, self.height) | 30 | self._image = BlankFrame(self.width, self.height) |
| 27 | 31 | ||
| 28 | self.page.lineEdit_color.setText('255,255,255') | 32 | self.page.lineEdit_color.setText("255,255,255") |
| 29 | 33 | ||
| 30 | if hasattr(self.parent, 'lineEdit_audioFile'): | 34 | if hasattr(self.parent, "lineEdit_audioFile"): |
| 31 | self.parent.lineEdit_audioFile.textChanged.connect( | 35 | self.parent.lineEdit_audioFile.textChanged.connect(self.update) |
| 32 | self.update | 36 | |
| 33 | ) | 37 | self.trackWidgets( |
| 34 | 38 | { | |
| 35 | self.trackWidgets({ | 39 | "color": self.page.lineEdit_color, |
| 36 | 'color': self.page.lineEdit_color, | 40 | "mode": self.page.comboBox_mode, |
| 37 | 'mode': self.page.comboBox_mode, | 41 | "amplitude": self.page.comboBox_amplitude, |
| 38 | 'amplitude': self.page.comboBox_amplitude, | 42 | "x": self.page.spinBox_x, |
| 39 | 'x': self.page.spinBox_x, | 43 | "y": self.page.spinBox_y, |
| 40 | 'y': self.page.spinBox_y, | 44 | "mirror": self.page.checkBox_mirror, |
| 41 | 'mirror': self.page.checkBox_mirror, | 45 | "scale": self.page.spinBox_scale, |
| 42 | 'scale': self.page.spinBox_scale, | 46 | "opacity": self.page.spinBox_opacity, |
| 43 | 'opacity': self.page.spinBox_opacity, | 47 | "compress": self.page.checkBox_compress, |
| 44 | 'compress': self.page.checkBox_compress, | 48 | "mono": self.page.checkBox_mono, |
| 45 | 'mono': self.page.checkBox_mono, | 49 | }, |
| 46 | }, colorWidgets={ | 50 | colorWidgets={ |
| 47 | 'color': self.page.pushButton_color, | 51 | "color": self.page.pushButton_color, |
| 48 | }, relativeWidgets=[ | 52 | }, |
| 49 | 'x', 'y', | 53 | relativeWidgets=[ |
| 50 | ]) | 54 | "x", |
| 55 | "y", | ||
| 56 | ], | ||
| 57 | ) | ||
| 51 | 58 | ||
| 52 | def previewRender(self): | 59 | def previewRender(self): |
| 53 | self.updateChunksize() | 60 | self.updateChunksize() |
| @@ -64,10 +71,13 @@ class Component(Component): | |||
| 64 | self.video = FfmpegVideo( | 71 | self.video = FfmpegVideo( |
| 65 | inputPath=self.audioFile, | 72 | inputPath=self.audioFile, |
| 66 | filter_=self.makeFfmpegFilter(), | 73 | filter_=self.makeFfmpegFilter(), |
| 67 | width=w, height=h, | 74 | width=w, |
| 75 | height=h, | ||
| 68 | chunkSize=self.chunkSize, | 76 | chunkSize=self.chunkSize, |
| 69 | frameRate=int(self.settings.value("outputFrameRate")), | 77 | frameRate=int(self.settings.value("outputFrameRate")), |
| 70 | parent=self.parent, component=self, debug=True, | 78 | parent=self.parent, |
| 79 | component=self, | ||
| 80 | debug=True, | ||
| 71 | ) | 81 | ) |
| 72 | 82 | ||
| 73 | def frameRender(self, frameNo): | 83 | def frameRender(self, frameNo): |
| @@ -94,37 +104,54 @@ class Component(Component): | |||
| 94 | 104 | ||
| 95 | command = [ | 105 | command = [ |
| 96 | self.core.FFMPEG_BIN, | 106 | self.core.FFMPEG_BIN, |
| 97 | '-thread_queue_size', '512', | 107 | "-thread_queue_size", |
| 98 | '-r', str(self.settings.value("outputFrameRate")), | 108 | "512", |
| 99 | '-ss', "{0:.3f}".format(startPt), | 109 | "-r", |
| 100 | '-i', | 110 | str(self.settings.value("outputFrameRate")), |
| 101 | self.core.junkStream | 111 | "-ss", |
| 102 | if genericPreview else inputFile, | 112 | "{0:.3f}".format(startPt), |
| 103 | '-f', 'image2pipe', | 113 | "-i", |
| 104 | '-pix_fmt', 'rgba', | 114 | self.core.junkStream if genericPreview else inputFile, |
| 115 | "-f", | ||
| 116 | "image2pipe", | ||
| 117 | "-pix_fmt", | ||
| 118 | "rgba", | ||
| 105 | ] | 119 | ] |
| 106 | command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) | 120 | command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) |
| 107 | command.extend([ | 121 | command.extend( |
| 108 | '-an', | 122 | [ |
| 109 | '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), | 123 | "-an", |
| 110 | '-codec:v', 'rawvideo', '-', | 124 | "-s:v", |
| 111 | '-frames:v', '1', | 125 | "%sx%s" % scale(self.scale, self.width, self.height, str), |
| 112 | ]) | 126 | "-codec:v", |
| 127 | "rawvideo", | ||
| 128 | "-", | ||
| 129 | "-frames:v", | ||
| 130 | "1", | ||
| 131 | ] | ||
| 132 | ) | ||
| 113 | if self.core.logEnabled: | 133 | if self.core.logEnabled: |
| 114 | logFilename = os.path.join( | 134 | logFilename = os.path.join( |
| 115 | self.core.logDir, 'preview_%s.log' % str(self.compPos)) | 135 | self.core.logDir, "preview_%s.log" % str(self.compPos) |
| 116 | log.debug('Creating ffmpeg log at %s', logFilename) | 136 | ) |
| 117 | with open(logFilename, 'w') as logf: | 137 | log.debug("Creating ffmpeg log at %s", logFilename) |
| 118 | logf.write(" ".join(command) + '\n\n') | 138 | with open(logFilename, "w") as logf: |
| 119 | with open(logFilename, 'a') as logf: | 139 | logf.write(" ".join(command) + "\n\n") |
| 140 | with open(logFilename, "a") as logf: | ||
| 120 | pipe = openPipe( | 141 | pipe = openPipe( |
| 121 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 142 | command, |
| 122 | stderr=logf, bufsize=10**8 | 143 | stdin=subprocess.DEVNULL, |
| 144 | stdout=subprocess.PIPE, | ||
| 145 | stderr=logf, | ||
| 146 | bufsize=10**8, | ||
| 123 | ) | 147 | ) |
| 124 | else: | 148 | else: |
| 125 | pipe = openPipe( | 149 | pipe = openPipe( |
| 126 | command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 150 | command, |
| 127 | stderr=subprocess.DEVNULL, bufsize=10**8 | 151 | stdin=subprocess.DEVNULL, |
| 152 | stdout=subprocess.PIPE, | ||
| 153 | stderr=subprocess.DEVNULL, | ||
| 154 | bufsize=10**8, | ||
| 128 | ) | 155 | ) |
| 129 | byteFrame = pipe.stdout.read(self.chunkSize) | 156 | byteFrame = pipe.stdout.read(self.chunkSize) |
| 130 | closePipe(pipe) | 157 | closePipe(pipe) |
| @@ -135,35 +162,35 @@ class Component(Component): | |||
| 135 | def makeFfmpegFilter(self, preview=False, startPt=0): | 162 | def makeFfmpegFilter(self, preview=False, startPt=0): |
| 136 | w, h = scale(self.scale, self.width, self.height, str) | 163 | w, h = scale(self.scale, self.width, self.height, str) |
| 137 | if self.amplitude == 0: | 164 | if self.amplitude == 0: |
| 138 | amplitude = 'lin' | 165 | amplitude = "lin" |
| 139 | elif self.amplitude == 1: | 166 | elif self.amplitude == 1: |
| 140 | amplitude = 'log' | 167 | amplitude = "log" |
| 141 | elif self.amplitude == 2: | 168 | elif self.amplitude == 2: |
| 142 | amplitude = 'sqrt' | 169 | amplitude = "sqrt" |
| 143 | elif self.amplitude == 3: | 170 | elif self.amplitude == 3: |
| 144 | amplitude = 'cbrt' | 171 | amplitude = "cbrt" |
| 145 | hexcolor = QColor(*self.color).name() | 172 | hexcolor = QColor(*self.color).name() |
| 146 | opacity = "{0:.1f}".format(self.opacity / 100) | 173 | opacity = "{0:.1f}".format(self.opacity / 100) |
| 147 | genericPreview = self.settings.value("pref_genericPreview") | 174 | genericPreview = self.settings.value("pref_genericPreview") |
| 148 | if self.mode < 3: | 175 | if self.mode < 3: |
| 149 | filter_ = ( | 176 | filter_ = ( |
| 150 | 'showwaves=' | 177 | "showwaves=" |
| 151 | f'r={str(self.settings.value("outputFrameRate"))}:' | 178 | f'r={str(self.settings.value("outputFrameRate"))}:' |
| 152 | f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:' | 179 | f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:' |
| 153 | f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:' | 180 | f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:' |
| 154 | f'colors={hexcolor}@{opacity}:scale={amplitude}' | 181 | f"colors={hexcolor}@{opacity}:scale={amplitude}" |
| 155 | ) | 182 | ) |
| 156 | elif self.mode > 2: | 183 | elif self.mode > 2: |
| 157 | filter_ = ( | 184 | filter_ = ( |
| 158 | f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:' | 185 | f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:' |
| 159 | f'mode={"line" if self.mode == 4 else "bar"}:' | 186 | f'mode={"line" if self.mode == 4 else "bar"}:' |
| 160 | f'colors={hexcolor}@{opacity}' | 187 | f"colors={hexcolor}@{opacity}" |
| 161 | f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}" | 188 | f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}" |
| 162 | ) | 189 | ) |
| 163 | 190 | ||
| 164 | baselineHeight = int(self.height * (4 / 1080)) | 191 | baselineHeight = int(self.height * (4 / 1080)) |
| 165 | return [ | 192 | return [ |
| 166 | '-filter_complex', | 193 | "-filter_complex", |
| 167 | f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}" | 194 | f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}" |
| 168 | f"{'compand=gain=4,' if self.compress else ''}" | 195 | f"{'compand=gain=4,' if self.compress else ''}" |
| 169 | f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}" | 196 | f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}" |
| @@ -171,12 +198,14 @@ class Component(Component): | |||
| 171 | f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}" | 198 | f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}" |
| 172 | f"{', hflip' if self.mirror else''}" | 199 | f"{', hflip' if self.mirror else''}" |
| 173 | " [v1]; " | 200 | " [v1]; " |
| 174 | '[v1] scale=%s:%s%s [v]' % ( | 201 | "[v1] scale=%s:%s%s [v]" |
| 175 | w, h, | 202 | % ( |
| 176 | ', trim=duration=%s' % "{0:.3f}".format(startPt + 3) | 203 | w, |
| 177 | if preview else '', | 204 | h, |
| 205 | ", trim=duration=%s" % "{0:.3f}".format(startPt + 3) if preview else "", | ||
| 178 | ), | 206 | ), |
| 179 | '-map', '[v]', | 207 | "-map", |
| 208 | "[v]", | ||
| 180 | ] | 209 | ] |
| 181 | 210 | ||
| 182 | def updateChunksize(self): | 211 | def updateChunksize(self): |
| @@ -186,15 +215,14 @@ class Component(Component): | |||
| 186 | def finalizeFrame(self, imageData): | 215 | def finalizeFrame(self, imageData): |
| 187 | try: | 216 | try: |
| 188 | image = Image.frombytes( | 217 | image = Image.frombytes( |
| 189 | 'RGBA', | 218 | "RGBA", |
| 190 | scale(self.scale, self.width, self.height, int), | 219 | scale(self.scale, self.width, self.height, int), |
| 191 | imageData | 220 | imageData, |
| 192 | ) | 221 | ) |
| 193 | self._image = image | 222 | self._image = image |
| 194 | except ValueError: | 223 | except ValueError: |
| 195 | image = self._image | 224 | image = self._image |
| 196 | if self.scale != 100 \ | 225 | if self.scale != 100 or self.x != 0 or self.y != 0: |
| 197 | or self.x != 0 or self.y != 0: | ||
| 198 | frame = BlankFrame(self.width, self.height) | 226 | frame = BlankFrame(self.width, self.height) |
| 199 | frame.paste(image, box=(self.x, self.y)) | 227 | frame.paste(image, box=(self.x, self.y)) |
| 200 | else: | 228 | else: |
diff --git a/src/core.py b/src/core.py index 1ad4a67..df6ff63 100644 --- a/src/core.py +++ b/src/core.py | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Home to the Core class which tracks program state. Used by GUI & commandline | 2 | Home to the Core class which tracks program state. Used by GUI & commandline |
| 3 | to create a list of components and create a video thread to export. | 3 | to create a list of components and create a video thread to export. |
| 4 | ''' | 4 | """ |
| 5 | from PyQt5 import QtCore, QtGui, uic | 5 | |
| 6 | from PyQt6 import QtCore, QtGui, uic | ||
| 6 | import sys | 7 | import sys |
| 7 | import os | 8 | import os |
| 8 | import json | 9 | import json |
| @@ -12,20 +13,20 @@ import logging | |||
| 12 | from . import toolkit | 13 | from . import toolkit |
| 13 | 14 | ||
| 14 | 15 | ||
| 15 | log = logging.getLogger('AVP.Core') | 16 | log = logging.getLogger("AVP.Core") |
| 16 | STDOUT_LOGLVL = logging.WARNING | 17 | STDOUT_LOGLVL = logging.WARNING |
| 17 | FILE_LIBLOGLVL = logging.WARNING | 18 | FILE_LIBLOGLVL = logging.WARNING |
| 18 | FILE_LOGLVL = logging.INFO | 19 | FILE_LOGLVL = logging.INFO |
| 19 | 20 | ||
| 20 | 21 | ||
| 21 | class Core: | 22 | class Core: |
| 22 | ''' | 23 | """ |
| 23 | MainWindow and Command module both use an instance of this class | 24 | MainWindow and Command module both use an instance of this class |
| 24 | to store the core program state. This object tracks the components, | 25 | to store the core program state. This object tracks the components, |
| 25 | talks to the components, handles opening/creating project files | 26 | talks to the components, handles opening/creating project files |
| 26 | and presets, and creates the video thread to export. | 27 | and presets, and creates the video thread to export. |
| 27 | This class also stores constants as class variables. | 28 | This class also stores constants as class variables. |
| 28 | ''' | 29 | """ |
| 29 | 30 | ||
| 30 | def __init__(self): | 31 | def __init__(self): |
| 31 | self.importComponents() | 32 | self.importComponents() |
| @@ -34,9 +35,7 @@ class Core: | |||
| 34 | self.openingProject = False | 35 | self.openingProject = False |
| 35 | 36 | ||
| 36 | def __repr__(self): | 37 | def __repr__(self): |
| 37 | return "\n=~=~=~=\n".join( | 38 | return "\n=~=~=~=\n".join([repr(comp) for comp in self.selectedComponents]) |
| 38 | [repr(comp) for comp in self.selectedComponents] | ||
| 39 | ) | ||
| 40 | 39 | ||
| 41 | def importComponents(self): | 40 | def importComponents(self): |
| 42 | def findComponents(): | 41 | def findComponents(): |
| @@ -44,11 +43,12 @@ class Core: | |||
| 44 | name, ext = os.path.splitext(f) | 43 | name, ext = os.path.splitext(f) |
| 45 | if name.startswith("__"): | 44 | if name.startswith("__"): |
| 46 | continue | 45 | continue |
| 47 | elif ext == '.py': | 46 | elif ext == ".py": |
| 48 | yield name | 47 | yield name |
| 49 | log.debug('Importing component modules') | 48 | |
| 49 | log.debug("Importing component modules") | ||
| 50 | self.modules = [ | 50 | self.modules = [ |
| 51 | import_module('.components.%s' % name, __package__) | 51 | import_module(".components.%s" % name, __package__) |
| 52 | for name in findComponents() | 52 | for name in findComponents() |
| 53 | ] | 53 | ] |
| 54 | # store canonical module names and indexes | 54 | # store canonical module names and indexes |
| @@ -62,7 +62,7 @@ class Core: | |||
| 62 | # store alternative names for modules | 62 | # store alternative names for modules |
| 63 | self.altCompNames = [] | 63 | self.altCompNames = [] |
| 64 | for i, mod in enumerate(self.modules): | 64 | for i, mod in enumerate(self.modules): |
| 65 | if hasattr(mod.Component, 'names'): | 65 | if hasattr(mod.Component, "names"): |
| 66 | for name in mod.Component.names(): | 66 | for name in mod.Component.names(): |
| 67 | self.altCompNames.append((name, i)) | 67 | self.altCompNames.append((name, i)) |
| 68 | 68 | ||
| @@ -71,10 +71,10 @@ class Core: | |||
| 71 | component.compPos = i | 71 | component.compPos = i |
| 72 | 72 | ||
| 73 | def insertComponent(self, compPos, component, loader): | 73 | def insertComponent(self, compPos, component, loader): |
| 74 | ''' | 74 | """ |
| 75 | Creates a new component using these args: | 75 | Creates a new component using these args: |
| 76 | (compPos, component obj or moduleIndex, MWindow/Command/Core obj) | 76 | (compPos, component obj or moduleIndex, MWindow/Command/Core obj) |
| 77 | ''' | 77 | """ |
| 78 | if compPos < 0 or compPos > len(self.selectedComponents): | 78 | if compPos < 0 or compPos > len(self.selectedComponents): |
| 79 | compPos = len(self.selectedComponents) | 79 | compPos = len(self.selectedComponents) |
| 80 | if len(self.selectedComponents) > 50: | 80 | if len(self.selectedComponents) > 50: |
| @@ -82,25 +82,16 @@ class Core: | |||
| 82 | if type(component) is int: | 82 | if type(component) is int: |
| 83 | # create component using module index in self.modules | 83 | # create component using module index in self.modules |
| 84 | moduleIndex = int(component) | 84 | moduleIndex = int(component) |
| 85 | log.debug( | 85 | log.debug("Creating new component from module #%s", str(moduleIndex)) |
| 86 | 'Creating new component from module #%s', str(moduleIndex)) | 86 | component = self.modules[moduleIndex].Component(moduleIndex, compPos, self) |
| 87 | component = self.modules[moduleIndex].Component( | ||
| 88 | moduleIndex, compPos, self | ||
| 89 | ) | ||
| 90 | component.widget(loader) | 87 | component.widget(loader) |
| 91 | else: | 88 | else: |
| 92 | moduleIndex = -1 | 89 | moduleIndex = -1 |
| 93 | log.debug( | 90 | log.debug("Inserting previously-created %s component", component.name) |
| 94 | 'Inserting previously-created %s component', component.name) | ||
| 95 | 91 | ||
| 96 | component._error.connect( | 92 | component._error.connect(loader.videoThreadError) |
| 97 | loader.videoThreadError | 93 | self.selectedComponents.insert(compPos, component) |
| 98 | ) | 94 | if hasattr(loader, "insertComponent"): |
| 99 | self.selectedComponents.insert( | ||
| 100 | compPos, | ||
| 101 | component | ||
| 102 | ) | ||
| 103 | if hasattr(loader, 'insertComponent'): | ||
| 104 | loader.insertComponent(compPos) | 95 | loader.insertComponent(compPos) |
| 105 | 96 | ||
| 106 | self.componentListChanged() | 97 | self.componentListChanged() |
| @@ -123,9 +114,7 @@ class Core: | |||
| 123 | self.componentListChanged() | 114 | self.componentListChanged() |
| 124 | 115 | ||
| 125 | def updateComponent(self, i): | 116 | def updateComponent(self, i): |
| 126 | log.debug( | 117 | log.debug("Auto-updating %s #%s", self.selectedComponents[i], str(i)) |
| 127 | 'Auto-updating %s #%s', | ||
| 128 | self.selectedComponents[i], str(i)) | ||
| 129 | self.selectedComponents[i].update(auto=True) | 118 | self.selectedComponents[i].update(auto=True) |
| 130 | 119 | ||
| 131 | def moduleIndexFor(self, compName): | 120 | def moduleIndexFor(self, compName): |
| @@ -141,63 +130,59 @@ class Core: | |||
| 141 | self.selectedComponents[compIndex].currentPreset = None | 130 | self.selectedComponents[compIndex].currentPreset = None |
| 142 | 131 | ||
| 143 | def openPreset(self, filepath, compIndex, presetName): | 132 | def openPreset(self, filepath, compIndex, presetName): |
| 144 | '''Applies a preset to a specific component''' | 133 | """Applies a preset to a specific component""" |
| 145 | saveValueStore = self.getPreset(filepath) | 134 | saveValueStore = self.getPreset(filepath) |
| 146 | if not saveValueStore: | 135 | if not saveValueStore: |
| 147 | return False | 136 | return False |
| 148 | comp = self.selectedComponents[compIndex] | 137 | comp = self.selectedComponents[compIndex] |
| 149 | comp.loadPreset( | 138 | comp.loadPreset(saveValueStore, presetName) |
| 150 | saveValueStore, | ||
| 151 | presetName | ||
| 152 | ) | ||
| 153 | 139 | ||
| 154 | self.savedPresets[presetName] = dict(saveValueStore) | 140 | self.savedPresets[presetName] = dict(saveValueStore) |
| 155 | return True | 141 | return True |
| 156 | 142 | ||
| 157 | def getPreset(self, filepath): | 143 | def getPreset(self, filepath): |
| 158 | '''Returns the preset dict stored at this filepath''' | 144 | """Returns the preset dict stored at this filepath""" |
| 159 | if not os.path.exists(filepath): | 145 | if not os.path.exists(filepath): |
| 160 | return False | 146 | return False |
| 161 | with open(filepath, 'r') as f: | 147 | with open(filepath, "r") as f: |
| 162 | for line in f: | 148 | for line in f: |
| 163 | saveValueStore = toolkit.presetFromString(line.strip()) | 149 | saveValueStore = toolkit.presetFromString(line.strip()) |
| 164 | break | 150 | break |
| 165 | return saveValueStore | 151 | return saveValueStore |
| 166 | 152 | ||
| 167 | def getPresetDir(self, comp): | 153 | def getPresetDir(self, comp): |
| 168 | '''Get the preset subdir for a particular version of a component''' | 154 | """Get the preset subdir for a particular version of a component""" |
| 169 | return os.path.join(Core.presetDir, comp.name, str(comp.version)) | 155 | return os.path.join(Core.presetDir, comp.name, str(comp.version)) |
| 170 | 156 | ||
| 171 | def openProject(self, loader, filepath): | 157 | def openProject(self, loader, filepath): |
| 172 | ''' loader is the object calling this method which must have | 158 | """loader is the object calling this method which must have |
| 173 | its own showMessage(**kwargs) method for displaying errors. | 159 | its own showMessage(**kwargs) method for displaying errors. |
| 174 | ''' | 160 | """ |
| 175 | if not os.path.exists(filepath): | 161 | if not os.path.exists(filepath): |
| 176 | loader.showMessage(msg='Project file not found.') | 162 | loader.showMessage(msg="Project file not found.") |
| 177 | return | 163 | return |
| 178 | 164 | ||
| 179 | errcode, data = self.parseAvFile(filepath) | 165 | errcode, data = self.parseAvFile(filepath) |
| 180 | if errcode == 0: | 166 | if errcode == 0: |
| 181 | self.openingProject = True | 167 | self.openingProject = True |
| 182 | try: | 168 | try: |
| 183 | if hasattr(loader, 'window'): | 169 | if hasattr(loader, "window"): |
| 184 | for widget, value in data['WindowFields']: | 170 | for widget, value in data["WindowFields"]: |
| 185 | widget = eval('loader.%s' % widget) | 171 | widget = eval("loader.%s" % widget) |
| 186 | with toolkit.blockSignals(widget): | 172 | with toolkit.blockSignals(widget): |
| 187 | toolkit.setWidgetValue(widget, value) | 173 | toolkit.setWidgetValue(widget, value) |
| 188 | 174 | ||
| 189 | for key, value in data['Settings']: | 175 | for key, value in data["Settings"]: |
| 190 | Core.settings.setValue(key, value) | 176 | Core.settings.setValue(key, value) |
| 191 | for tup in data['Components']: | 177 | for tup in data["Components"]: |
| 192 | name, vers, preset = tup | 178 | name, vers, preset = tup |
| 193 | clearThis = False | 179 | clearThis = False |
| 194 | modified = False | 180 | modified = False |
| 195 | 181 | ||
| 196 | # add loaded named presets to savedPresets dict | 182 | # add loaded named presets to savedPresets dict |
| 197 | if 'preset' in preset and preset['preset'] is not None: | 183 | if "preset" in preset and preset["preset"] is not None: |
| 198 | nam = preset['preset'] | 184 | nam = preset["preset"] |
| 199 | filepath2 = os.path.join( | 185 | filepath2 = os.path.join(Core.presetDir, name, str(vers), nam) |
| 200 | Core.presetDir, name, str(vers), nam) | ||
| 201 | origSaveValueStore = self.getPreset(filepath2) | 186 | origSaveValueStore = self.getPreset(filepath2) |
| 202 | if origSaveValueStore: | 187 | if origSaveValueStore: |
| 203 | self.savedPresets[nam] = dict(origSaveValueStore) | 188 | self.savedPresets[nam] = dict(origSaveValueStore) |
| @@ -207,33 +192,31 @@ class Core: | |||
| 207 | clearThis = True | 192 | clearThis = True |
| 208 | 193 | ||
| 209 | # create the actual component object & get its index | 194 | # create the actual component object & get its index |
| 210 | i = self.insertComponent( | 195 | i = self.insertComponent(-1, self.moduleIndexFor(name), loader) |
| 211 | -1, | 196 | if i is None: |
| 212 | self.moduleIndexFor(name), | 197 | loader.showMessage( |
| 213 | loader | 198 | msg=f"Component '{name}' didn't initialize correctly and had to be removed." |
| 214 | ) | 199 | ) |
| 200 | continue | ||
| 215 | if i == -1: | 201 | if i == -1: |
| 216 | loader.showMessage(msg="Too many components!") | 202 | loader.showMessage(msg="Too many components!") |
| 217 | break | 203 | break |
| 218 | 204 | ||
| 219 | try: | 205 | try: |
| 220 | if 'preset' in preset and preset['preset'] is not None: | 206 | if "preset" in preset and preset["preset"] is not None: |
| 221 | self.selectedComponents[i].loadPreset( | 207 | self.selectedComponents[i].loadPreset(preset) |
| 222 | preset | ||
| 223 | ) | ||
| 224 | else: | 208 | else: |
| 225 | self.selectedComponents[i].loadPreset( | 209 | self.selectedComponents[i].loadPreset( |
| 226 | preset, | 210 | preset, preset["preset"] |
| 227 | preset['preset'] | ||
| 228 | ) | 211 | ) |
| 229 | except KeyError as e: | 212 | except KeyError as e: |
| 230 | log.warning('%s missing value: %s' % ( | 213 | log.warning( |
| 231 | self.selectedComponents[i], e) | 214 | "%s missing value: %s" % (self.selectedComponents[i], e) |
| 232 | ) | 215 | ) |
| 233 | 216 | ||
| 234 | if clearThis: | 217 | if clearThis: |
| 235 | self.clearPreset(i) | 218 | self.clearPreset(i) |
| 236 | if hasattr(loader, 'updateComponentTitle'): | 219 | if hasattr(loader, "updateComponentTitle"): |
| 237 | loader.updateComponentTitle(i, modified) | 220 | loader.updateComponentTitle(i, modified) |
| 238 | self.openingProject = False | 221 | self.openingProject = False |
| 239 | return True | 222 | return True |
| @@ -243,56 +226,57 @@ class Core: | |||
| 243 | 226 | ||
| 244 | if errcode == 1: | 227 | if errcode == 1: |
| 245 | typ, value, tb = data | 228 | typ, value, tb = data |
| 246 | if typ.__name__ == 'KeyError': | 229 | if typ.__name__ == "KeyError": |
| 247 | # probably just an old version, still loadable | 230 | # probably just an old version, still loadable |
| 248 | log.warning('Project file missing value: %s' % value) | 231 | log.warning("Project file missing value: %s" % value) |
| 249 | return | 232 | return |
| 250 | if hasattr(loader, 'createNewProject'): | 233 | if hasattr(loader, "createNewProject"): |
| 251 | loader.createNewProject(prompt=False) | 234 | loader.createNewProject(prompt=False) |
| 252 | msg = '%s: %s\n\n' % (typ.__name__, value) | 235 | msg = "%s: %s\n\n" % (typ.__name__, value) |
| 253 | msg += toolkit.formatTraceback(tb) | 236 | msg += toolkit.formatTraceback(tb) |
| 254 | loader.showMessage( | 237 | loader.showMessage( |
| 255 | msg="Project file '%s' is corrupted." % filepath, | 238 | msg="Project file '%s' is corrupted." % filepath, |
| 256 | showCancel=False, | 239 | showCancel=False, |
| 257 | icon='Warning', | 240 | icon="Warning", |
| 258 | detail=msg) | 241 | detail=msg, |
| 242 | ) | ||
| 259 | self.openingProject = False | 243 | self.openingProject = False |
| 260 | return False | 244 | return False |
| 261 | 245 | ||
| 262 | def parseAvFile(self, filepath): | 246 | def parseAvFile(self, filepath): |
| 263 | ''' | 247 | """ |
| 264 | Parses an avp (project) or avl (preset package) file. | 248 | Parses an avp (project) or avl (preset package) file. |
| 265 | Returns dictionary with section names as the keys, each one | 249 | Returns dictionary with section names as the keys, each one |
| 266 | contains a list of tuples: (compName, version, compPresetDict) | 250 | contains a list of tuples: (compName, version, compPresetDict) |
| 267 | ''' | 251 | """ |
| 268 | log.debug('Parsing av file: %s', filepath) | 252 | log.debug("Parsing av file: %s", filepath) |
| 269 | validSections = ( | 253 | validSections = ("Components", "Settings", "WindowFields") |
| 270 | 'Components', | ||
| 271 | 'Settings', | ||
| 272 | 'WindowFields' | ||
| 273 | ) | ||
| 274 | data = {sect: [] for sect in validSections} | 254 | data = {sect: [] for sect in validSections} |
| 275 | try: | 255 | try: |
| 276 | with open(filepath, 'r') as f: | 256 | with open(filepath, "r") as f: |
| 257 | |||
| 277 | def parseLine(line): | 258 | def parseLine(line): |
| 278 | '''Decides if a file line is a section header''' | 259 | """Decides if a file line is a section header""" |
| 279 | line = line.strip() | 260 | line = line.strip() |
| 280 | newSection = '' | 261 | newSection = "" |
| 281 | 262 | ||
| 282 | if line.startswith('[') and line.endswith(']') \ | 263 | if ( |
| 283 | and line[1:-1] in validSections: | 264 | line.startswith("[") |
| 265 | and line.endswith("]") | ||
| 266 | and line[1:-1] in validSections | ||
| 267 | ): | ||
| 284 | newSection = line[1:-1] | 268 | newSection = line[1:-1] |
| 285 | 269 | ||
| 286 | return line, newSection | 270 | return line, newSection |
| 287 | 271 | ||
| 288 | section = '' | 272 | section = "" |
| 289 | i = 0 | 273 | i = 0 |
| 290 | for line in f: | 274 | for line in f: |
| 291 | line, newSection = parseLine(line) | 275 | line, newSection = parseLine(line) |
| 292 | if newSection: | 276 | if newSection: |
| 293 | section = str(newSection) | 277 | section = str(newSection) |
| 294 | continue | 278 | continue |
| 295 | if line and section == 'Components': | 279 | if line and section == "Components": |
| 296 | if i == 0: | 280 | if i == 0: |
| 297 | lastCompName = str(line) | 281 | lastCompName = str(line) |
| 298 | i += 1 | 282 | i += 1 |
| @@ -301,14 +285,12 @@ class Core: | |||
| 301 | i += 1 | 285 | i += 1 |
| 302 | elif i == 2: | 286 | elif i == 2: |
| 303 | lastCompPreset = toolkit.presetFromString(line) | 287 | lastCompPreset = toolkit.presetFromString(line) |
| 304 | data[section].append(( | 288 | data[section].append( |
| 305 | lastCompName, | 289 | (lastCompName, lastCompVers, lastCompPreset) |
| 306 | lastCompVers, | 290 | ) |
| 307 | lastCompPreset | ||
| 308 | )) | ||
| 309 | i = 0 | 291 | i = 0 |
| 310 | elif line and section: | 292 | elif line and section: |
| 311 | key, value = line.split('=', 1) | 293 | key, value = line.split("=", 1) |
| 312 | data[section].append((key, value.strip())) | 294 | data[section].append((key, value.strip())) |
| 313 | 295 | ||
| 314 | return 0, data | 296 | return 0, data |
| @@ -319,51 +301,40 @@ class Core: | |||
| 319 | errcode, data = self.parseAvFile(filepath) | 301 | errcode, data = self.parseAvFile(filepath) |
| 320 | returnList = [] | 302 | returnList = [] |
| 321 | if errcode == 0: | 303 | if errcode == 0: |
| 322 | name, vers, preset = data['Components'][0] | 304 | name, vers, preset = data["Components"][0] |
| 323 | presetName = preset['preset'] \ | 305 | presetName = ( |
| 324 | if preset['preset'] else os.path.basename(filepath)[:-4] | 306 | preset["preset"] |
| 325 | newPath = os.path.join( | 307 | if preset["preset"] |
| 326 | Core.presetDir, | 308 | else os.path.basename(filepath)[:-4] |
| 327 | name, | ||
| 328 | vers, | ||
| 329 | presetName | ||
| 330 | ) | 309 | ) |
| 310 | newPath = os.path.join(Core.presetDir, name, vers, presetName) | ||
| 331 | if os.path.exists(newPath): | 311 | if os.path.exists(newPath): |
| 332 | return False, newPath | 312 | return False, newPath |
| 333 | preset['preset'] = presetName | 313 | preset["preset"] = presetName |
| 334 | self.createPresetFile( | 314 | self.createPresetFile(name, vers, presetName, preset) |
| 335 | name, vers, presetName, preset | ||
| 336 | ) | ||
| 337 | return True, presetName | 315 | return True, presetName |
| 338 | elif errcode == 1: | 316 | elif errcode == 1: |
| 339 | # TODO: an error message | 317 | # TODO: an error message |
| 340 | return False, '' | 318 | return False, "" |
| 341 | 319 | ||
| 342 | def exportPreset(self, exportPath, compName, vers, origName): | 320 | def exportPreset(self, exportPath, compName, vers, origName): |
| 343 | internalPath = os.path.join( | 321 | internalPath = os.path.join(Core.presetDir, compName, str(vers), origName) |
| 344 | Core.presetDir, compName, str(vers), origName | ||
| 345 | ) | ||
| 346 | if not os.path.exists(internalPath): | 322 | if not os.path.exists(internalPath): |
| 347 | return | 323 | return |
| 348 | if os.path.exists(exportPath): | 324 | if os.path.exists(exportPath): |
| 349 | os.remove(exportPath) | 325 | os.remove(exportPath) |
| 350 | with open(internalPath, 'r') as f: | 326 | with open(internalPath, "r") as f: |
| 351 | internalData = [line for line in f] | 327 | internalData = [line for line in f] |
| 352 | try: | 328 | try: |
| 353 | saveValueStore = toolkit.presetFromString(internalData[0].strip()) | 329 | saveValueStore = toolkit.presetFromString(internalData[0].strip()) |
| 354 | self.createPresetFile( | 330 | self.createPresetFile(compName, vers, origName, saveValueStore, exportPath) |
| 355 | compName, vers, | ||
| 356 | origName, saveValueStore, | ||
| 357 | exportPath | ||
| 358 | ) | ||
| 359 | return True | 331 | return True |
| 360 | except Exception: | 332 | except Exception: |
| 361 | return False | 333 | return False |
| 362 | 334 | ||
| 363 | def createPresetFile( | 335 | def createPresetFile(self, compName, vers, presetName, saveValueStore, filepath=""): |
| 364 | self, compName, vers, presetName, saveValueStore, filepath=''): | 336 | """Create a preset file (.avl) at filepath using args. |
| 365 | '''Create a preset file (.avl) at filepath using args. | 337 | Or if filepath is empty, create an internal preset using args""" |
| 366 | Or if filepath is empty, create an internal preset using args''' | ||
| 367 | if not filepath: | 338 | if not filepath: |
| 368 | dirname = os.path.join(Core.presetDir, compName, str(vers)) | 339 | dirname = os.path.join(Core.presetDir, compName, str(vers)) |
| 369 | if not os.path.exists(dirname): | 340 | if not os.path.exists(dirname): |
| @@ -371,54 +342,55 @@ class Core: | |||
| 371 | filepath = os.path.join(dirname, presetName) | 342 | filepath = os.path.join(dirname, presetName) |
| 372 | internal = True | 343 | internal = True |
| 373 | else: | 344 | else: |
| 374 | if not filepath.endswith('.avl'): | 345 | if not filepath.endswith(".avl"): |
| 375 | filepath += '.avl' | 346 | filepath += ".avl" |
| 376 | internal = False | 347 | internal = False |
| 377 | 348 | ||
| 378 | with open(filepath, 'w') as f: | 349 | with open(filepath, "w") as f: |
| 379 | if not internal: | 350 | if not internal: |
| 380 | f.write('[Components]\n') | 351 | f.write("[Components]\n") |
| 381 | f.write('%s\n' % compName) | 352 | f.write("%s\n" % compName) |
| 382 | f.write('%s\n' % str(vers)) | 353 | f.write("%s\n" % str(vers)) |
| 383 | f.write(toolkit.presetToString(saveValueStore)) | 354 | f.write(toolkit.presetToString(saveValueStore)) |
| 384 | 355 | ||
| 385 | def createProjectFile(self, filepath, window=None): | 356 | def createProjectFile(self, filepath, window=None): |
| 386 | '''Create a project file (.avp) using the current program state''' | 357 | """Create a project file (.avp) using the current program state""" |
| 387 | log.info('Creating %s', filepath) | 358 | log.info("Creating %s", filepath) |
| 388 | settingsKeys = [ | 359 | settingsKeys = [ |
| 389 | 'componentDir', | 360 | "componentDir", |
| 390 | 'inputDir', | 361 | "inputDir", |
| 391 | 'outputDir', | 362 | "outputDir", |
| 392 | 'presetDir', | 363 | "presetDir", |
| 393 | 'projectDir', | 364 | "projectDir", |
| 394 | ] | 365 | ] |
| 395 | try: | 366 | try: |
| 396 | if not filepath.endswith(".avp"): | 367 | if not filepath.endswith(".avp"): |
| 397 | filepath += '.avp' | 368 | filepath += ".avp" |
| 398 | if os.path.exists(filepath): | 369 | if os.path.exists(filepath): |
| 399 | os.remove(filepath) | 370 | os.remove(filepath) |
| 400 | 371 | ||
| 401 | with open(filepath, 'w') as f: | 372 | with open(filepath, "w") as f: |
| 402 | f.write('[Components]\n') | 373 | f.write("[Components]\n") |
| 403 | for comp in self.selectedComponents: | 374 | for comp in self.selectedComponents: |
| 404 | saveValueStore = comp.savePreset() | 375 | saveValueStore = comp.savePreset() |
| 405 | saveValueStore['preset'] = comp.currentPreset | 376 | saveValueStore["preset"] = comp.currentPreset |
| 406 | f.write('%s\n' % str(comp)) | 377 | f.write("%s\n" % str(comp)) |
| 407 | f.write('%s\n' % str(comp.version)) | 378 | f.write("%s\n" % str(comp.version)) |
| 408 | f.write('%s\n' % toolkit.presetToString(saveValueStore)) | 379 | f.write("%s\n" % toolkit.presetToString(saveValueStore)) |
| 409 | 380 | ||
| 410 | f.write('\n[Settings]\n') | 381 | f.write("\n[Settings]\n") |
| 411 | for key in Core.settings.allKeys(): | 382 | for key in Core.settings.allKeys(): |
| 412 | if key in settingsKeys: | 383 | if key in settingsKeys: |
| 413 | f.write('%s=%s\n' % (key, Core.settings.value(key))) | 384 | f.write("%s=%s\n" % (key, Core.settings.value(key))) |
| 414 | 385 | ||
| 415 | if window: | 386 | if window: |
| 416 | f.write('\n[WindowFields]\n') | 387 | f.write("\n[WindowFields]\n") |
| 417 | f.write( | 388 | f.write( |
| 418 | 'lineEdit_audioFile=%s\n' | 389 | "lineEdit_audioFile=%s\n" |
| 419 | 'lineEdit_outputFile=%s\n' % ( | 390 | "lineEdit_outputFile=%s\n" |
| 391 | % ( | ||
| 420 | window.lineEdit_audioFile.text(), | 392 | window.lineEdit_audioFile.text(), |
| 421 | window.lineEdit_outputFile.text() | 393 | window.lineEdit_outputFile.text(), |
| 422 | ) | 394 | ) |
| 423 | ) | 395 | ) |
| 424 | return True | 396 | return True |
| @@ -426,8 +398,9 @@ class Core: | |||
| 426 | return False | 398 | return False |
| 427 | 399 | ||
| 428 | def newVideoWorker(self, loader, audioFile, outputPath): | 400 | def newVideoWorker(self, loader, audioFile, outputPath): |
| 429 | '''loader is MainWindow or Command object which must own the thread''' | 401 | """loader is MainWindow or Command object which must own the thread""" |
| 430 | from . import video_thread | 402 | from . import video_thread |
| 403 | |||
| 431 | self.videoThread = QtCore.QThread(loader) | 404 | self.videoThread = QtCore.QThread(loader) |
| 432 | videoWorker = video_thread.Worker( | 405 | videoWorker = video_thread.Worker( |
| 433 | loader, audioFile, outputPath, self.selectedComponents | 406 | loader, audioFile, outputPath, self.selectedComponents |
| @@ -450,18 +423,18 @@ class Core: | |||
| 450 | 423 | ||
| 451 | @classmethod | 424 | @classmethod |
| 452 | def storeSettings(cls): | 425 | def storeSettings(cls): |
| 453 | '''Store settings/paths to directories as class variables''' | 426 | """Store settings/paths to directories as class variables""" |
| 454 | from .__init__ import wd | 427 | from .__init__ import wd |
| 455 | from .toolkit.ffmpeg import findFfmpeg | 428 | from .toolkit.ffmpeg import findFfmpeg |
| 456 | 429 | ||
| 457 | cls.wd = wd | 430 | cls.wd = wd |
| 458 | dataDir = QtCore.QStandardPaths.writableLocation( | 431 | dataDir = QtCore.QStandardPaths.writableLocation( |
| 459 | QtCore.QStandardPaths.AppConfigLocation | 432 | QtCore.QStandardPaths.StandardLocation.AppConfigLocation |
| 460 | ) | 433 | ) |
| 461 | # Windows: C:/Users/<USER>/AppData/Local/audio-visualizer | 434 | # Windows: C:/Users/<USER>/AppData/Local/audio-visualizer |
| 462 | # macOS: ~/Library/Preferences/audio-visualizer | 435 | # macOS: ~/Library/Preferences/audio-visualizer |
| 463 | # Linux: ~/.config/audio-visualizer | 436 | # Linux: ~/.config/audio-visualizer |
| 464 | with open(os.path.join(wd, 'encoder-options.json')) as json_file: | 437 | with open(os.path.join(wd, "encoder-options.json")) as json_file: |
| 465 | encoderOptions = json.load(json_file) | 438 | encoderOptions = json.load(json_file) |
| 466 | 439 | ||
| 467 | # Locate FFmpeg | 440 | # Locate FFmpeg |
| @@ -470,53 +443,60 @@ class Core: | |||
| 470 | print("Could not find FFmpeg") | 443 | print("Could not find FFmpeg") |
| 471 | 444 | ||
| 472 | settings = { | 445 | settings = { |
| 473 | 'canceled': False, | 446 | "canceled": False, |
| 474 | 'FFMPEG_BIN': ffmpegBin, | 447 | "FFMPEG_BIN": ffmpegBin, |
| 475 | 'dataDir': dataDir, | 448 | "dataDir": dataDir, |
| 476 | 'settings': QtCore.QSettings( | 449 | "settings": QtCore.QSettings( |
| 477 | os.path.join(dataDir, 'settings.ini'), | 450 | os.path.join(dataDir, "settings.ini"), |
| 478 | QtCore.QSettings.IniFormat), | 451 | QtCore.QSettings.Format.IniFormat, |
| 479 | 'presetDir': os.path.join(dataDir, 'presets'), | 452 | ), |
| 480 | 'componentsPath': os.path.join(wd, 'components'), | 453 | "presetDir": os.path.join(dataDir, "presets"), |
| 481 | 'junkStream': os.path.join(wd, 'gui', 'background.png'), | 454 | "componentsPath": os.path.join(wd, "components"), |
| 482 | 'encoderOptions': encoderOptions, | 455 | "junkStream": os.path.join(wd, "gui", "background.png"), |
| 483 | 'resolutions': [ | 456 | "encoderOptions": encoderOptions, |
| 484 | '1920x1080', | 457 | "resolutions": [ |
| 485 | '1280x720', | 458 | "1920x1080", |
| 486 | '854x480', | 459 | "1280x720", |
| 460 | "854x480", | ||
| 487 | ], | 461 | ], |
| 488 | 'logDir': os.path.join(dataDir, 'log'), | 462 | "logDir": os.path.join(dataDir, "log"), |
| 489 | 'logEnabled': False, | 463 | "logEnabled": False, |
| 490 | 'previewEnabled': True, | 464 | "previewEnabled": True, |
| 491 | } | 465 | } |
| 492 | 466 | ||
| 493 | settings['videoFormats'] = toolkit.appendUppercase([ | 467 | settings["videoFormats"] = toolkit.appendUppercase( |
| 494 | '*.mp4', | 468 | [ |
| 495 | '*.mov', | 469 | "*.mp4", |
| 496 | '*.mkv', | 470 | "*.mov", |
| 497 | '*.avi', | 471 | "*.mkv", |
| 498 | '*.webm', | 472 | "*.avi", |
| 499 | '*.flv', | 473 | "*.webm", |
| 500 | ]) | 474 | "*.flv", |
| 501 | settings['audioFormats'] = toolkit.appendUppercase([ | 475 | ] |
| 502 | '*.mp3', | 476 | ) |
| 503 | '*.wav', | 477 | settings["audioFormats"] = toolkit.appendUppercase( |
| 504 | '*.ogg', | 478 | [ |
| 505 | '*.fla', | 479 | "*.mp3", |
| 506 | '*.flac', | 480 | "*.wav", |
| 507 | '*.aac', | 481 | "*.ogg", |
| 508 | ]) | 482 | "*.fla", |
| 509 | settings['imageFormats'] = toolkit.appendUppercase([ | 483 | "*.flac", |
| 510 | '*.png', | 484 | "*.aac", |
| 511 | '*.jpg', | 485 | ] |
| 512 | '*.tif', | 486 | ) |
| 513 | '*.tiff', | 487 | settings["imageFormats"] = toolkit.appendUppercase( |
| 514 | '*.gif', | 488 | [ |
| 515 | '*.bmp', | 489 | "*.png", |
| 516 | '*.ico', | 490 | "*.jpg", |
| 517 | '*.xbm', | 491 | "*.tif", |
| 518 | '*.xpm', | 492 | "*.tiff", |
| 519 | ]) | 493 | "*.gif", |
| 494 | "*.bmp", | ||
| 495 | "*.ico", | ||
| 496 | "*.xbm", | ||
| 497 | "*.xpm", | ||
| 498 | ] | ||
| 499 | ) | ||
| 520 | 500 | ||
| 521 | # Register all settings as class variables | 501 | # Register all settings as class variables |
| 522 | for classvar, val in settings.items(): | 502 | for classvar, val in settings.items(): |
| @@ -526,7 +506,10 @@ class Core: | |||
| 526 | if not os.path.exists(cls.dataDir): | 506 | if not os.path.exists(cls.dataDir): |
| 527 | os.makedirs(cls.dataDir) | 507 | os.makedirs(cls.dataDir) |
| 528 | for neededDirectory in ( | 508 | for neededDirectory in ( |
| 529 | cls.presetDir, cls.logDir, cls.settings.value("projectDir")): | 509 | cls.presetDir, |
| 510 | cls.logDir, | ||
| 511 | cls.settings.value("projectDir"), | ||
| 512 | ): | ||
| 530 | if not os.path.exists(neededDirectory): | 513 | if not os.path.exists(neededDirectory): |
| 531 | os.mkdir(neededDirectory) | 514 | os.mkdir(neededDirectory) |
| 532 | cls.makeLogger(deleteOldLogs=True) | 515 | cls.makeLogger(deleteOldLogs=True) |
| @@ -546,7 +529,7 @@ class Core: | |||
| 546 | "outputPreset": "medium", | 529 | "outputPreset": "medium", |
| 547 | "outputFormat": "mp4", | 530 | "outputFormat": "mp4", |
| 548 | "outputContainer": "MP4", | 531 | "outputContainer": "MP4", |
| 549 | "projectDir": os.path.join(cls.dataDir, 'projects'), | 532 | "projectDir": os.path.join(cls.dataDir, "projects"), |
| 550 | "pref_insertCompAtTop": True, | 533 | "pref_insertCompAtTop": True, |
| 551 | "pref_genericPreview": True, | 534 | "pref_genericPreview": True, |
| 552 | "pref_undoLimit": 10, | 535 | "pref_undoLimit": 10, |
| @@ -559,15 +542,15 @@ class Core: | |||
| 559 | # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to | 542 | # Allow manual editing of prefs. (Surprisingly necessary as Qt seems to |
| 560 | # store True as 'true' but interprets a manually-added 'true' as str.) | 543 | # store True as 'true' but interprets a manually-added 'true' as str.) |
| 561 | for key in cls.settings.allKeys(): | 544 | for key in cls.settings.allKeys(): |
| 562 | if not key.startswith('pref_'): | 545 | if not key.startswith("pref_"): |
| 563 | continue | 546 | continue |
| 564 | val = cls.settings.value(key) | 547 | val = cls.settings.value(key) |
| 565 | try: | 548 | try: |
| 566 | val = int(val) | 549 | val = int(val) |
| 567 | except ValueError: | 550 | except ValueError: |
| 568 | if val == 'true': | 551 | if val == "true": |
| 569 | val = True | 552 | val = True |
| 570 | elif val == 'false': | 553 | elif val == "false": |
| 571 | val = False | 554 | val = False |
| 572 | cls.settings.setValue(key, val) | 555 | cls.settings.setValue(key, val) |
| 573 | 556 | ||
| @@ -576,18 +559,16 @@ class Core: | |||
| 576 | # send critical log messages to stdout | 559 | # send critical log messages to stdout |
| 577 | logStream = logging.StreamHandler() | 560 | logStream = logging.StreamHandler() |
| 578 | logStream.setLevel(STDOUT_LOGLVL) | 561 | logStream.setLevel(STDOUT_LOGLVL) |
| 579 | streamFormatter = logging.Formatter( | 562 | streamFormatter = logging.Formatter("<%(name)s> %(levelname)s: %(message)s") |
| 580 | '<%(name)s> %(levelname)s: %(message)s' | ||
| 581 | ) | ||
| 582 | logStream.setFormatter(streamFormatter) | 563 | logStream.setFormatter(streamFormatter) |
| 583 | log = logging.getLogger('AVP') | 564 | log = logging.getLogger("AVP") |
| 584 | log.addHandler(logStream) | 565 | log.addHandler(logStream) |
| 585 | 566 | ||
| 586 | if FILE_LOGLVL is not None: | 567 | if FILE_LOGLVL is not None: |
| 587 | # write log files as well! | 568 | # write log files as well! |
| 588 | Core.logEnabled = True | 569 | Core.logEnabled = True |
| 589 | logFilename = os.path.join(Core.logDir, 'avp_debug.log') | 570 | logFilename = os.path.join(Core.logDir, "avp_debug.log") |
| 590 | libLogFilename = os.path.join(Core.logDir, 'global_debug.log') | 571 | libLogFilename = os.path.join(Core.logDir, "global_debug.log") |
| 591 | 572 | ||
| 592 | if deleteOldLogs: | 573 | if deleteOldLogs: |
| 593 | for log_ in (logFilename, libLogFilename): | 574 | for log_ in (logFilename, libLogFilename): |
| @@ -599,8 +580,8 @@ class Core: | |||
| 599 | libLogFile = logging.FileHandler(libLogFilename, delay=True) | 580 | libLogFile = logging.FileHandler(libLogFilename, delay=True) |
| 600 | libLogFile.setLevel(FILE_LIBLOGLVL) | 581 | libLogFile.setLevel(FILE_LIBLOGLVL) |
| 601 | fileFormatter = logging.Formatter( | 582 | fileFormatter = logging.Formatter( |
| 602 | '[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: ' | 583 | "[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: " |
| 603 | '%(message)s' | 584 | "%(message)s" |
| 604 | ) | 585 | ) |
| 605 | logFile.setFormatter(fileFormatter) | 586 | logFile.setFormatter(fileFormatter) |
| 606 | libLogFile.setFormatter(fileFormatter) | 587 | libLogFile.setFormatter(fileFormatter) |
| @@ -611,5 +592,6 @@ class Core: | |||
| 611 | # lowest level must be explicitly set on the root Logger | 592 | # lowest level must be explicitly set on the root Logger |
| 612 | libLog.setLevel(0) | 593 | libLog.setLevel(0) |
| 613 | 594 | ||
| 595 | |||
| 614 | # always store settings in class variables even if a Core object is not created | 596 | # always store settings in class variables even if a Core object is not created |
| 615 | Core.storeSettings() | 597 | Core.storeSettings() |
diff --git a/src/gui/actions.py b/src/gui/actions.py index afb980a..654b2a0 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py | |||
| @@ -1,53 +1,61 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | QCommand classes for every undoable user action performed in the MainWindow | 2 | QCommand classes for every undoable user action performed in the MainWindow |
| 3 | ''' | 3 | """ |
| 4 | from PyQt5.QtWidgets import QUndoCommand | 4 | |
| 5 | from PyQt6.QtGui import QUndoCommand | ||
| 5 | import os | 6 | import os |
| 7 | import logging | ||
| 6 | from copy import copy | 8 | from copy import copy |
| 7 | 9 | ||
| 8 | from ..core import Core | 10 | from ..core import Core |
| 9 | 11 | ||
| 10 | 12 | ||
| 13 | log = logging.getLogger("AVP.Gui.Actions") | ||
| 14 | |||
| 15 | |||
| 11 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 16 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 12 | # COMPONENT ACTIONS | 17 | # COMPONENT ACTIONS |
| 13 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 18 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 14 | 19 | ||
| 20 | |||
| 15 | class AddComponent(QUndoCommand): | 21 | class AddComponent(QUndoCommand): |
| 16 | def __init__(self, parent, compI, moduleI): | 22 | def __init__(self, parent, compI, moduleI): |
| 17 | super().__init__( | 23 | super().__init__( |
| 18 | "create new %s component" % | 24 | "create new %s component" % parent.core.modules[moduleI].Component.name |
| 19 | parent.core.modules[moduleI].Component.name | ||
| 20 | ) | 25 | ) |
| 21 | self.parent = parent | 26 | self.parent = parent |
| 22 | self.moduleI = moduleI | 27 | self.moduleI = moduleI |
| 23 | self.compI = compI | 28 | self.compI = compI |
| 24 | self.comp = None | 29 | self.comp = None |
| 30 | self.valid = True | ||
| 25 | 31 | ||
| 26 | def redo(self): | 32 | def redo(self): |
| 27 | if self.comp is None: | 33 | if self.comp is None: |
| 28 | self.parent.core.insertComponent( | 34 | i = self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) |
| 29 | self.compI, self.moduleI, self.parent) | 35 | if i != self.compI: |
| 36 | self.valid = False | ||
| 37 | if i is not None: | ||
| 38 | log.error( | ||
| 39 | f"Expected new component index to be {self.compI} but received {i}" | ||
| 40 | ) | ||
| 30 | else: | 41 | else: |
| 31 | # inserting previously-created component | 42 | # inserting previously-created component |
| 32 | self.parent.core.insertComponent( | 43 | self.parent.core.insertComponent(self.compI, self.comp, self.parent) |
| 33 | self.compI, self.comp, self.parent) | ||
| 34 | 44 | ||
| 35 | def undo(self): | 45 | def undo(self): |
| 46 | if not self.valid: | ||
| 47 | return | ||
| 36 | self.comp = self.parent.core.selectedComponents[self.compI] | 48 | self.comp = self.parent.core.selectedComponents[self.compI] |
| 37 | self.parent._removeComponent(self.compI) | 49 | self.parent._removeComponent(self.compI) |
| 38 | 50 | ||
| 39 | 51 | ||
| 40 | class RemoveComponent(QUndoCommand): | 52 | class RemoveComponent(QUndoCommand): |
| 41 | def __init__(self, parent, selectedRows): | 53 | def __init__(self, parent, selectedRows): |
| 42 | super().__init__('remove component') | 54 | super().__init__("remove component") |
| 43 | self.parent = parent | 55 | self.parent = parent |
| 44 | componentList = self.parent.listWidget_componentList | 56 | componentList = self.parent.listWidget_componentList |
| 45 | self.selectedRows = [ | 57 | self.selectedRows = [componentList.row(selected) for selected in selectedRows] |
| 46 | componentList.row(selected) for selected in selectedRows | 58 | self.components = [parent.core.selectedComponents[i] for i in self.selectedRows] |
| 47 | ] | ||
| 48 | self.components = [ | ||
| 49 | parent.core.selectedComponents[i] for i in self.selectedRows | ||
| 50 | ] | ||
| 51 | 59 | ||
| 52 | def redo(self): | 60 | def redo(self): |
| 53 | self.parent._removeComponent(self.selectedRows[0]) | 61 | self.parent._removeComponent(self.selectedRows[0]) |
| @@ -55,9 +63,7 @@ class RemoveComponent(QUndoCommand): | |||
| 55 | def undo(self): | 63 | def undo(self): |
| 56 | componentList = self.parent.listWidget_componentList | 64 | componentList = self.parent.listWidget_componentList |
| 57 | for index, comp in zip(self.selectedRows, self.components): | 65 | for index, comp in zip(self.selectedRows, self.components): |
| 58 | self.parent.core.insertComponent( | 66 | self.parent.core.insertComponent(index, comp, self.parent) |
| 59 | index, comp, self.parent | ||
| 60 | ) | ||
| 61 | self.parent.drawPreview() | 67 | self.parent.drawPreview() |
| 62 | 68 | ||
| 63 | 69 | ||
| @@ -70,7 +76,7 @@ class MoveComponent(QUndoCommand): | |||
| 70 | self.id_ = ord(tag[0]) | 76 | self.id_ = ord(tag[0]) |
| 71 | 77 | ||
| 72 | def id(self): | 78 | def id(self): |
| 73 | '''If 2 consecutive updates have same id, Qt will call mergeWith()''' | 79 | """If 2 consecutive updates have same id, Qt will call mergeWith()""" |
| 74 | return self.id_ | 80 | return self.id_ |
| 75 | 81 | ||
| 76 | def mergeWith(self, other): | 82 | def mergeWith(self, other): |
| @@ -105,6 +111,7 @@ class MoveComponent(QUndoCommand): | |||
| 105 | # PRESET ACTIONS | 111 | # PRESET ACTIONS |
| 106 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 112 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 107 | 113 | ||
| 114 | |||
| 108 | class ClearPreset(QUndoCommand): | 115 | class ClearPreset(QUndoCommand): |
| 109 | def __init__(self, parent, compI): | 116 | def __init__(self, parent, compI): |
| 110 | super().__init__("clear preset") | 117 | super().__init__("clear preset") |
| @@ -112,7 +119,7 @@ class ClearPreset(QUndoCommand): | |||
| 112 | self.compI = compI | 119 | self.compI = compI |
| 113 | self.component = self.parent.core.selectedComponents[compI] | 120 | self.component = self.parent.core.selectedComponents[compI] |
| 114 | self.store = self.component.savePreset() | 121 | self.store = self.component.savePreset() |
| 115 | self.store['preset'] = self.component.currentPreset | 122 | self.store["preset"] = self.component.currentPreset |
| 116 | 123 | ||
| 117 | def redo(self): | 124 | def redo(self): |
| 118 | self.parent.core.clearPreset(self.compI) | 125 | self.parent.core.clearPreset(self.compI) |
| @@ -132,20 +139,19 @@ class OpenPreset(QUndoCommand): | |||
| 132 | 139 | ||
| 133 | comp = self.parent.core.selectedComponents[compI] | 140 | comp = self.parent.core.selectedComponents[compI] |
| 134 | self.store = comp.savePreset() | 141 | self.store = comp.savePreset() |
| 135 | self.store['preset'] = copy(comp.currentPreset) | 142 | self.store["preset"] = copy(comp.currentPreset) |
| 136 | 143 | ||
| 137 | def redo(self): | 144 | def redo(self): |
| 138 | self.parent._openPreset(self.presetName, self.compI) | 145 | self.parent._openPreset(self.presetName, self.compI) |
| 139 | 146 | ||
| 140 | def undo(self): | 147 | def undo(self): |
| 141 | self.parent.core.selectedComponents[self.compI].loadPreset( | 148 | self.parent.core.selectedComponents[self.compI].loadPreset(self.store) |
| 142 | self.store) | ||
| 143 | self.parent.parent.updateComponentTitle(self.compI, self.store) | 149 | self.parent.parent.updateComponentTitle(self.compI, self.store) |
| 144 | 150 | ||
| 145 | 151 | ||
| 146 | class RenamePreset(QUndoCommand): | 152 | class RenamePreset(QUndoCommand): |
| 147 | def __init__(self, parent, path, oldName, newName): | 153 | def __init__(self, parent, path, oldName, newName): |
| 148 | super().__init__('rename preset') | 154 | super().__init__("rename preset") |
| 149 | self.parent = parent | 155 | self.parent = parent |
| 150 | self.path = path | 156 | self.path = path |
| 151 | self.oldName = oldName | 157 | self.oldName = oldName |
| @@ -162,14 +168,13 @@ class DeletePreset(QUndoCommand): | |||
| 162 | def __init__(self, parent, compName, vers, presetFile): | 168 | def __init__(self, parent, compName, vers, presetFile): |
| 163 | self.parent = parent | 169 | self.parent = parent |
| 164 | self.preset = (compName, vers, presetFile) | 170 | self.preset = (compName, vers, presetFile) |
| 165 | self.path = os.path.join( | 171 | self.path = os.path.join(Core.presetDir, compName, str(vers), presetFile) |
| 166 | Core.presetDir, compName, str(vers), presetFile | ||
| 167 | ) | ||
| 168 | self.store = self.parent.core.getPreset(self.path) | 172 | self.store = self.parent.core.getPreset(self.path) |
| 169 | self.presetName = self.store['preset'] | 173 | self.presetName = self.store["preset"] |
| 170 | super().__init__('delete %s preset (%s)' % (self.presetName, compName)) | 174 | super().__init__("delete %s preset (%s)" % (self.presetName, compName)) |
| 171 | self.loadedPresets = [ | 175 | self.loadedPresets = [ |
| 172 | i for i, comp in enumerate(self.parent.core.selectedComponents) | 176 | i |
| 177 | for i, comp in enumerate(self.parent.core.selectedComponents) | ||
| 173 | if self.presetName == str(comp.currentPreset) | 178 | if self.presetName == str(comp.currentPreset) |
| 174 | ] | 179 | ] |
| 175 | 180 | ||
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 159dc02..b0a564b 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | When using GUI mode, this module's object (the main window) takes | 2 | When using GUI mode, this module's object (the main window) takes |
| 3 | user input to construct a program state (stored in the Core object). | 3 | user input to construct a program state (stored in the Core object). |
| 4 | This shows a preview of the video being created and allows for saving | 4 | This shows a preview of the video being created and allows for saving |
| 5 | projects and exporting the video at a later time. | 5 | projects and exporting the video at a later time. |
| 6 | ''' | 6 | """ |
| 7 | from PyQt5 import QtCore, QtWidgets, uic | 7 | |
| 8 | import PyQt5.QtWidgets as QtWidgets | 8 | from PyQt6 import QtCore, QtWidgets, uic |
| 9 | import PyQt6.QtWidgets as QtWidgets | ||
| 10 | from PyQt6.QtGui import QUndoStack, QShortcut | ||
| 9 | from PIL import Image | 11 | from PIL import Image |
| 10 | from queue import Queue | 12 | from queue import Queue |
| 11 | import sys | 13 | import sys |
| @@ -21,46 +23,59 @@ from .preview_win import PreviewWindow | |||
| 21 | from .presetmanager import PresetManager | 23 | from .presetmanager import PresetManager |
| 22 | from .actions import * | 24 | from .actions import * |
| 23 | from ..toolkit import ( | 25 | from ..toolkit import ( |
| 24 | disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals | 26 | disableWhenEncoding, |
| 27 | disableWhenOpeningProject, | ||
| 28 | checkOutput, | ||
| 29 | blockSignals, | ||
| 25 | ) | 30 | ) |
| 26 | 31 | ||
| 27 | 32 | ||
| 28 | appName = 'Audio Visualizer' | 33 | appName = "Audio Visualizer" |
| 29 | log = logging.getLogger('AVP.Gui.MainWindow') | 34 | log = logging.getLogger("AVP.Gui.MainWindow") |
| 35 | |||
| 36 | |||
| 37 | class MyQUndoStack(QUndoStack): | ||
| 38 | # FIXME move this class | ||
| 39 | @property | ||
| 40 | def encoding(self): | ||
| 41 | return self.parent().encoding | ||
| 42 | |||
| 43 | @disableWhenEncoding | ||
| 44 | def undo(self, *args, **kwargs): | ||
| 45 | super().undo(*args, **kwargs) | ||
| 46 | |||
| 47 | @disableWhenEncoding | ||
| 48 | def redo(self, *args, **kwargs): | ||
| 49 | super().redo(*args, **kwargs) | ||
| 30 | 50 | ||
| 31 | 51 | ||
| 32 | class MainWindow(QtWidgets.QMainWindow): | 52 | class MainWindow(QtWidgets.QMainWindow): |
| 33 | ''' | 53 | """ |
| 34 | The MainWindow wraps many Core methods in order to update the GUI | 54 | The MainWindow wraps many Core methods in order to update the GUI |
| 35 | accordingly. E.g., instead of self.core.openProject(), it will use | 55 | accordingly. E.g., instead of self.core.openProject(), it will use |
| 36 | self.openProject() and update the window titlebar within the wrapper. | 56 | self.openProject() and update the window titlebar within the wrapper. |
| 37 | 57 | ||
| 38 | MainWindow manages the autosave feature, although Core has the | 58 | MainWindow manages the autosave feature, although Core has the |
| 39 | primary functions for opening and creating project files. | 59 | primary functions for opening and creating project files. |
| 40 | ''' | 60 | """ |
| 41 | 61 | ||
| 42 | createVideo = QtCore.pyqtSignal() | 62 | createVideo = QtCore.pyqtSignal() |
| 43 | newTask = QtCore.pyqtSignal(list) # for the preview window | 63 | newTask = QtCore.pyqtSignal(list) # for the preview window |
| 44 | processTask = QtCore.pyqtSignal() | 64 | processTask = QtCore.pyqtSignal() |
| 45 | 65 | ||
| 46 | def __init__(self, project): | 66 | def __init__(self, project, dpi): |
| 47 | super().__init__() | 67 | super().__init__() |
| 48 | log.debug( | 68 | log.debug("Main thread id: {}".format(int(QtCore.QThread.currentThreadId()))) |
| 49 | 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) | ||
| 50 | uic.loadUi(os.path.join(Core.wd, "gui", "mainwindow.ui"), self) | 69 | uic.loadUi(os.path.join(Core.wd, "gui", "mainwindow.ui"), self) |
| 51 | desk = QtWidgets.QDesktopWidget() | 70 | |
| 52 | dpi = desk.physicalDpiX() | 71 | if dpi: |
| 53 | log.info("Detected screen DPI: %s", dpi) | 72 | self.resize( |
| 54 | 73 | int(self.width() * (dpi / 144)), | |
| 55 | self.resize( | 74 | int(self.height() * (dpi / 144)), |
| 56 | int(self.width() * | 75 | ) |
| 57 | (dpi / 144)), | ||
| 58 | int(self.height() * | ||
| 59 | (dpi / 144)) | ||
| 60 | ) | ||
| 61 | 76 | ||
| 62 | self.core = Core() | 77 | self.core = Core() |
| 63 | Core.mode = 'GUI' | 78 | Core.mode = "GUI" |
| 64 | # widgets of component settings | 79 | # widgets of component settings |
| 65 | self.pages = [] | 80 | self.pages = [] |
| 66 | self.lastAutosave = time.time() | 81 | self.lastAutosave = time.time() |
| @@ -72,15 +87,13 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 72 | # Find settings created by Core object | 87 | # Find settings created by Core object |
| 73 | self.dataDir = Core.dataDir | 88 | self.dataDir = Core.dataDir |
| 74 | self.presetDir = Core.presetDir | 89 | self.presetDir = Core.presetDir |
| 75 | self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') | 90 | self.autosavePath = os.path.join(self.dataDir, "autosave.avp") |
| 76 | self.settings = Core.settings | 91 | self.settings = Core.settings |
| 77 | 92 | ||
| 78 | # Create stack of undoable user actions | 93 | # Create stack of undoable user actions |
| 79 | self.undoStack = QtWidgets.QUndoStack(self) | 94 | self.undoStack = MyQUndoStack(self) |
| 80 | undoLimit = self.settings.value("pref_undoLimit") | 95 | undoLimit = self.settings.value("pref_undoLimit") |
| 81 | self.undoStack.setUndoLimit(undoLimit) | 96 | self.undoStack.setUndoLimit(undoLimit) |
| 82 | self.undoStack.undo = disableWhenEncoding(self.undoStack.undo) | ||
| 83 | self.undoStack.redo = disableWhenEncoding(self.undoStack.redo) | ||
| 84 | 97 | ||
| 85 | # Create Undo Dialog - A standard QUndoView on a standard QDialog | 98 | # Create Undo Dialog - A standard QUndoView on a standard QDialog |
| 86 | self.undoDialog = QtWidgets.QDialog(self) | 99 | self.undoDialog = QtWidgets.QDialog(self) |
| @@ -94,18 +107,17 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 94 | self.presetManager = PresetManager(self) | 107 | self.presetManager = PresetManager(self) |
| 95 | 108 | ||
| 96 | # Create the preview window and its thread, queues, and timers | 109 | # Create the preview window and its thread, queues, and timers |
| 97 | log.debug('Creating preview window') | 110 | log.debug("Creating preview window") |
| 98 | self.previewWindow = PreviewWindow(self, os.path.join( | 111 | self.previewWindow = PreviewWindow( |
| 99 | Core.wd, 'gui', "background.png")) | 112 | self, os.path.join(Core.wd, "gui", "background.png") |
| 113 | ) | ||
| 100 | self.verticalLayout_previewWrapper.addWidget(self.previewWindow) | 114 | self.verticalLayout_previewWrapper.addWidget(self.previewWindow) |
| 101 | 115 | ||
| 102 | log.debug('Starting preview thread') | 116 | log.debug("Starting preview thread") |
| 103 | self.previewQueue = Queue() | 117 | self.previewQueue = Queue() |
| 104 | self.previewThread = QtCore.QThread(self) | 118 | self.previewThread = QtCore.QThread(self) |
| 105 | self.previewWorker = preview_thread.Worker( | 119 | self.previewWorker = preview_thread.Worker( |
| 106 | self.core, | 120 | self.core, self.settings, self.previewQueue |
| 107 | self.settings, | ||
| 108 | self.previewQueue | ||
| 109 | ) | 121 | ) |
| 110 | self.previewWorker.moveToThread(self.previewThread) | 122 | self.previewWorker.moveToThread(self.previewThread) |
| 111 | self.newTask.connect(self.previewWorker.createPreviewImage) | 123 | self.newTask.connect(self.previewWorker.createPreviewImage) |
| @@ -113,12 +125,12 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 113 | self.previewWorker.error.connect(self.previewWindow.threadError) | 125 | self.previewWorker.error.connect(self.previewWindow.threadError) |
| 114 | self.previewWorker.imageCreated.connect(self.showPreviewImage) | 126 | self.previewWorker.imageCreated.connect(self.showPreviewImage) |
| 115 | self.previewThread.start() | 127 | self.previewThread.start() |
| 116 | self.previewThread.finished.connect(lambda: log.info('Preview thread finished.')) | 128 | self.previewThread.finished.connect( |
| 129 | lambda: log.info("Preview thread finished.") | ||
| 130 | ) | ||
| 117 | 131 | ||
| 118 | timeout = 500 | 132 | timeout = 500 |
| 119 | log.debug( | 133 | log.debug("Preview timer set to trigger when idle for %sms" % str(timeout)) |
| 120 | 'Preview timer set to trigger when idle for %sms' % str(timeout) | ||
| 121 | ) | ||
| 122 | self.timer = QtCore.QTimer(self) | 134 | self.timer = QtCore.QTimer(self) |
| 123 | self.timer.timeout.connect(self.processTask.emit) | 135 | self.timer.timeout.connect(self.processTask.emit) |
| 124 | self.timer.start(timeout) | 136 | self.timer.start(timeout) |
| @@ -128,7 +140,7 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 128 | 140 | ||
| 129 | # Undo Feature | 141 | # Undo Feature |
| 130 | def toggleUndoButtonEnabled(*_): | 142 | def toggleUndoButtonEnabled(*_): |
| 131 | """ Enable/disable undo button depending on whether UndoStack contains Actions """ | 143 | """Enable/disable undo button depending on whether UndoStack contains Actions""" |
| 132 | try: | 144 | try: |
| 133 | undoButton.setEnabled(self.undoStack.count()) | 145 | undoButton.setEnabled(self.undoStack.count()) |
| 134 | except RuntimeError: | 146 | except RuntimeError: |
| @@ -138,50 +150,41 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 138 | style = self.pushButton_undo.style() | 150 | style = self.pushButton_undo.style() |
| 139 | undoButton = self.pushButton_undo | 151 | undoButton = self.pushButton_undo |
| 140 | undoButton.setIcon( | 152 | undoButton.setIcon( |
| 141 | style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack) | 153 | style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_FileDialogBack) |
| 142 | ) | 154 | ) |
| 143 | undoButton.clicked.connect(self.undoStack.undo) | 155 | undoButton.clicked.connect(self.undoStack.undo) |
| 144 | undoButton.setEnabled(False) | 156 | undoButton.setEnabled(False) |
| 145 | self.undoStack.cleanChanged.connect(toggleUndoButtonEnabled) | 157 | self.undoStack.cleanChanged.connect(toggleUndoButtonEnabled) |
| 146 | self.undoMenu = QtWidgets.QMenu() | 158 | self.undoMenu = QtWidgets.QMenu() |
| 147 | self.undoMenu.addAction( | 159 | self.undoMenu.addAction(self.undoStack.createUndoAction(self)) |
| 148 | self.undoStack.createUndoAction(self) | 160 | self.undoMenu.addAction(self.undoStack.createRedoAction(self)) |
| 149 | ) | 161 | action = self.undoMenu.addAction("Show History...") |
| 150 | self.undoMenu.addAction( | 162 | action.triggered.connect(lambda _: self.showUndoStack()) |
| 151 | self.undoStack.createRedoAction(self) | ||
| 152 | ) | ||
| 153 | action = self.undoMenu.addAction('Show History...') | ||
| 154 | action.triggered.connect( | ||
| 155 | lambda _: self.showUndoStack() | ||
| 156 | ) | ||
| 157 | undoButton.setMenu(self.undoMenu) | 163 | undoButton.setMenu(self.undoMenu) |
| 158 | # end of Undo Feature | 164 | # end of Undo Feature |
| 159 | 165 | ||
| 160 | style = self.pushButton_listMoveUp.style() | 166 | style = self.pushButton_listMoveUp.style() |
| 161 | self.pushButton_listMoveUp.setIcon( | 167 | self.pushButton_listMoveUp.setIcon( |
| 162 | style.standardIcon(QtWidgets.QStyle.SP_ArrowUp) | 168 | style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowUp) |
| 163 | ) | 169 | ) |
| 164 | style = self.pushButton_listMoveDown.style() | 170 | style = self.pushButton_listMoveDown.style() |
| 165 | self.pushButton_listMoveDown.setIcon( | 171 | self.pushButton_listMoveDown.setIcon( |
| 166 | style.standardIcon(QtWidgets.QStyle.SP_ArrowDown) | 172 | style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowDown) |
| 167 | ) | 173 | ) |
| 168 | style = self.pushButton_removeComponent.style() | 174 | style = self.pushButton_removeComponent.style() |
| 169 | self.pushButton_removeComponent.setIcon( | 175 | self.pushButton_removeComponent.setIcon( |
| 170 | style.standardIcon(QtWidgets.QStyle.SP_DialogDiscardButton) | 176 | style.standardIcon(QtWidgets.QStyle.StandardPixmap.SP_DialogDiscardButton) |
| 171 | ) | 177 | ) |
| 172 | 178 | ||
| 173 | if sys.platform == 'darwin': | 179 | if sys.platform == "darwin": |
| 174 | log.debug( | 180 | log.debug("Darwin detected: showing progress label below progress bar") |
| 175 | 'Darwin detected: showing progress label below progress bar') | ||
| 176 | self.progressBar_createVideo.setTextVisible(False) | 181 | self.progressBar_createVideo.setTextVisible(False) |
| 177 | else: | 182 | else: |
| 178 | self.progressLabel.setHidden(True) | 183 | self.progressLabel.setHidden(True) |
| 179 | 184 | ||
| 180 | self.toolButton_selectAudioFile.clicked.connect( | 185 | self.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) |
| 181 | self.openInputFileDialog) | ||
| 182 | 186 | ||
| 183 | self.toolButton_selectOutputFile.clicked.connect( | 187 | self.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog) |
| 184 | self.openOutputFileDialog) | ||
| 185 | 188 | ||
| 186 | def changedField(): | 189 | def changedField(): |
| 187 | self.autosave() | 190 | self.autosave() |
| @@ -192,43 +195,36 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 192 | 195 | ||
| 193 | self.progressBar_createVideo.setValue(0) | 196 | self.progressBar_createVideo.setValue(0) |
| 194 | 197 | ||
| 195 | self.pushButton_createVideo.clicked.connect( | 198 | self.pushButton_createVideo.clicked.connect(self.createAudioVisualization) |
| 196 | self.createAudioVisualization) | ||
| 197 | 199 | ||
| 198 | self.pushButton_Cancel.clicked.connect(self.stopVideo) | 200 | self.pushButton_Cancel.clicked.connect(self.stopVideo) |
| 199 | 201 | ||
| 200 | for i, container in enumerate(Core.encoderOptions['containers']): | 202 | for i, container in enumerate(Core.encoderOptions["containers"]): |
| 201 | self.comboBox_videoContainer.addItem(container['name']) | 203 | self.comboBox_videoContainer.addItem(container["name"]) |
| 202 | if container['name'] == self.settings.value('outputContainer'): | 204 | if container["name"] == self.settings.value("outputContainer"): |
| 203 | selectedContainer = i | 205 | selectedContainer = i |
| 204 | 206 | ||
| 205 | self.comboBox_videoContainer.setCurrentIndex(selectedContainer) | 207 | self.comboBox_videoContainer.setCurrentIndex(selectedContainer) |
| 206 | self.comboBox_videoContainer.currentIndexChanged.connect( | 208 | self.comboBox_videoContainer.currentIndexChanged.connect(self.updateCodecs) |
| 207 | self.updateCodecs | ||
| 208 | ) | ||
| 209 | 209 | ||
| 210 | self.updateCodecs() | 210 | self.updateCodecs() |
| 211 | 211 | ||
| 212 | for i in range(self.comboBox_videoCodec.count()): | 212 | for i in range(self.comboBox_videoCodec.count()): |
| 213 | codec = self.comboBox_videoCodec.itemText(i) | 213 | codec = self.comboBox_videoCodec.itemText(i) |
| 214 | if codec == self.settings.value('outputVideoCodec'): | 214 | if codec == self.settings.value("outputVideoCodec"): |
| 215 | self.comboBox_videoCodec.setCurrentIndex(i) | 215 | self.comboBox_videoCodec.setCurrentIndex(i) |
| 216 | 216 | ||
| 217 | for i in range(self.comboBox_audioCodec.count()): | 217 | for i in range(self.comboBox_audioCodec.count()): |
| 218 | codec = self.comboBox_audioCodec.itemText(i) | 218 | codec = self.comboBox_audioCodec.itemText(i) |
| 219 | if codec == self.settings.value('outputAudioCodec'): | 219 | if codec == self.settings.value("outputAudioCodec"): |
| 220 | self.comboBox_audioCodec.setCurrentIndex(i) | 220 | self.comboBox_audioCodec.setCurrentIndex(i) |
| 221 | 221 | ||
| 222 | self.comboBox_videoCodec.currentIndexChanged.connect( | 222 | self.comboBox_videoCodec.currentIndexChanged.connect(self.updateCodecSettings) |
| 223 | self.updateCodecSettings | ||
| 224 | ) | ||
| 225 | 223 | ||
| 226 | self.comboBox_audioCodec.currentIndexChanged.connect( | 224 | self.comboBox_audioCodec.currentIndexChanged.connect(self.updateCodecSettings) |
| 227 | self.updateCodecSettings | ||
| 228 | ) | ||
| 229 | 225 | ||
| 230 | vBitrate = int(self.settings.value('outputVideoBitrate')) | 226 | vBitrate = int(self.settings.value("outputVideoBitrate")) |
| 231 | aBitrate = int(self.settings.value('outputAudioBitrate')) | 227 | aBitrate = int(self.settings.value("outputAudioBitrate")) |
| 232 | 228 | ||
| 233 | self.spinBox_vBitrate.setValue(vBitrate) | 229 | self.spinBox_vBitrate.setValue(vBitrate) |
| 234 | self.spinBox_aBitrate.setValue(aBitrate) | 230 | self.spinBox_aBitrate.setValue(aBitrate) |
| @@ -239,30 +235,27 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 239 | self.compMenu = QtWidgets.QMenu() | 235 | self.compMenu = QtWidgets.QMenu() |
| 240 | for i, comp in enumerate(self.core.modules): | 236 | for i, comp in enumerate(self.core.modules): |
| 241 | action = self.compMenu.addAction(comp.Component.name) | 237 | action = self.compMenu.addAction(comp.Component.name) |
| 242 | action.triggered.connect( | 238 | action.triggered.connect(lambda _, item=i: self.addComponent(0, item)) |
| 243 | lambda _, item=i: self.addComponent(0, item) | ||
| 244 | ) | ||
| 245 | 239 | ||
| 246 | self.pushButton_addComponent.setMenu(self.compMenu) | 240 | self.pushButton_addComponent.setMenu(self.compMenu) |
| 247 | 241 | ||
| 248 | componentList.dropEvent = self.dragComponent | 242 | componentList.dropEvent = self.dragComponent |
| 249 | componentList.itemSelectionChanged.connect( | 243 | componentList.itemSelectionChanged.connect(self.changeComponentWidget) |
| 250 | self.changeComponentWidget | ||
| 251 | ) | ||
| 252 | componentList.itemSelectionChanged.connect( | 244 | componentList.itemSelectionChanged.connect( |
| 253 | self.presetManager.clearPresetListSelection | 245 | self.presetManager.clearPresetListSelection |
| 254 | ) | 246 | ) |
| 255 | self.pushButton_removeComponent.clicked.connect( | 247 | self.pushButton_removeComponent.clicked.connect(lambda: self.removeComponent()) |
| 256 | lambda: self.removeComponent() | ||
| 257 | ) | ||
| 258 | 248 | ||
| 259 | componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) | 249 | componentList.setContextMenuPolicy( |
| 260 | componentList.customContextMenuRequested.connect( | 250 | QtCore.Qt.ContextMenuPolicy.CustomContextMenu |
| 261 | self.componentContextMenu | ||
| 262 | ) | 251 | ) |
| 252 | componentList.customContextMenuRequested.connect(self.componentContextMenu) | ||
| 263 | 253 | ||
| 264 | currentRes = str(self.settings.value('outputWidth'))+'x' + \ | 254 | currentRes = ( |
| 265 | str(self.settings.value('outputHeight')) | 255 | str(self.settings.value("outputWidth")) |
| 256 | + "x" | ||
| 257 | + str(self.settings.value("outputHeight")) | ||
| 258 | ) | ||
| 266 | for i, res in enumerate(Core.resolutions): | 259 | for i, res in enumerate(Core.resolutions): |
| 267 | self.comboBox_resolution.addItem(res) | 260 | self.comboBox_resolution.addItem(res) |
| 268 | if res == currentRes: | 261 | if res == currentRes: |
| @@ -272,24 +265,14 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 272 | self.updateResolution | 265 | self.updateResolution |
| 273 | ) | 266 | ) |
| 274 | 267 | ||
| 275 | self.pushButton_listMoveUp.clicked.connect( | 268 | self.pushButton_listMoveUp.clicked.connect(lambda: self.moveComponent(-1)) |
| 276 | lambda: self.moveComponent(-1) | 269 | self.pushButton_listMoveDown.clicked.connect(lambda: self.moveComponent(1)) |
| 277 | ) | ||
| 278 | self.pushButton_listMoveDown.clicked.connect( | ||
| 279 | lambda: self.moveComponent(1) | ||
| 280 | ) | ||
| 281 | 270 | ||
| 282 | # Configure the Projects Menu | 271 | # Configure the Projects Menu |
| 283 | self.projectMenu = QtWidgets.QMenu() | 272 | self.projectMenu = QtWidgets.QMenu() |
| 284 | self.menuButton_newProject = self.projectMenu.addAction( | 273 | self.menuButton_newProject = self.projectMenu.addAction("New Project") |
| 285 | "New Project" | 274 | self.menuButton_newProject.triggered.connect(lambda: self.createNewProject()) |
| 286 | ) | 275 | self.menuButton_openProject = self.projectMenu.addAction("Open Project") |
| 287 | self.menuButton_newProject.triggered.connect( | ||
| 288 | lambda: self.createNewProject() | ||
| 289 | ) | ||
| 290 | self.menuButton_openProject = self.projectMenu.addAction( | ||
| 291 | "Open Project" | ||
| 292 | ) | ||
| 293 | self.menuButton_openProject.triggered.connect( | 276 | self.menuButton_openProject.triggered.connect( |
| 294 | lambda: self.openOpenProjectDialog() | 277 | lambda: self.openOpenProjectDialog() |
| 295 | ) | 278 | ) |
| @@ -303,22 +286,18 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 303 | self.pushButton_projects.setMenu(self.projectMenu) | 286 | self.pushButton_projects.setMenu(self.projectMenu) |
| 304 | 287 | ||
| 305 | # Configure the Presets Button | 288 | # Configure the Presets Button |
| 306 | self.pushButton_presets.clicked.connect( | 289 | self.pushButton_presets.clicked.connect(self.openPresetManager) |
| 307 | self.openPresetManager | ||
| 308 | ) | ||
| 309 | 290 | ||
| 310 | self.updateWindowTitle() | 291 | self.updateWindowTitle() |
| 311 | log.debug('Showing main window') | 292 | log.debug("Showing main window") |
| 312 | self.show() | 293 | self.show() |
| 313 | 294 | ||
| 314 | if project and project != self.autosavePath: | 295 | if project and project != self.autosavePath: |
| 315 | if not project.endswith('.avp'): | 296 | if not project.endswith(".avp"): |
| 316 | project += '.avp' | 297 | project += ".avp" |
| 317 | # open a project from the commandline | 298 | # open a project from the commandline |
| 318 | if not os.path.dirname(project): | 299 | if not os.path.dirname(project): |
| 319 | project = os.path.join( | 300 | project = os.path.join(self.settings.value("projectDir"), project) |
| 320 | self.settings.value("projectDir"), project | ||
| 321 | ) | ||
| 322 | self.currentProject = project | 301 | self.currentProject = project |
| 323 | self.settings.setValue("currentProject", project) | 302 | self.settings.setValue("currentProject", project) |
| 324 | if os.path.exists(self.autosavePath): | 303 | if os.path.exists(self.autosavePath): |
| @@ -335,7 +314,8 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 335 | ch = self.showMessage( | 314 | ch = self.showMessage( |
| 336 | msg="Restore unsaved changes in project '%s'?" | 315 | msg="Restore unsaved changes in project '%s'?" |
| 337 | % os.path.basename(self.currentProject)[:-4], | 316 | % os.path.basename(self.currentProject)[:-4], |
| 338 | showCancel=True) | 317 | showCancel=True, |
| 318 | ) | ||
| 339 | if ch: | 319 | if ch: |
| 340 | self.saveProjectChanges() | 320 | self.saveProjectChanges() |
| 341 | else: | 321 | else: |
| @@ -352,16 +332,16 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 352 | msg="FFmpeg could not be found. This is a critical error. " | 332 | msg="FFmpeg could not be found. This is a critical error. " |
| 353 | "Install FFmpeg, or download it and place the program executable " | 333 | "Install FFmpeg, or download it and place the program executable " |
| 354 | "in the same folder as this program.", | 334 | "in the same folder as this program.", |
| 355 | icon='Critical' | 335 | icon="Critical", |
| 356 | ) | 336 | ) |
| 357 | else: | 337 | else: |
| 358 | if not self.settings.value("ffmpegMsgShown"): | 338 | if not self.settings.value("ffmpegMsgShown"): |
| 359 | try: | 339 | try: |
| 360 | with open(os.devnull, "w") as f: | 340 | with open(os.devnull, "w") as f: |
| 361 | ffmpegVers = checkOutput( | 341 | ffmpegVers = checkOutput( |
| 362 | [self.core.FFMPEG_BIN, '-version'], stderr=f | 342 | [self.core.FFMPEG_BIN, "-version"], stderr=f |
| 363 | ) | 343 | ) |
| 364 | goodVersion = str(ffmpegVers).split()[2].startswith('4') | 344 | goodVersion = str(ffmpegVers).split()[2].startswith("4") |
| 365 | except Exception: | 345 | except Exception: |
| 366 | goodVersion = False | 346 | goodVersion = False |
| 367 | else: | 347 | else: |
| @@ -375,70 +355,61 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 375 | self.settings.setValue("ffmpegMsgShown", True) | 355 | self.settings.setValue("ffmpegMsgShown", True) |
| 376 | 356 | ||
| 377 | # Hotkeys for projects | 357 | # Hotkeys for projects |
| 378 | QtWidgets.QShortcut("Ctrl+S", self, self.saveCurrentProject) | 358 | |
| 379 | QtWidgets.QShortcut("Ctrl+A", self, self.openSaveProjectDialog) | 359 | QShortcut("Ctrl+S", self, self.saveCurrentProject) |
| 380 | QtWidgets.QShortcut("Ctrl+O", self, self.openOpenProjectDialog) | 360 | QShortcut("Ctrl+A", self, self.openSaveProjectDialog) |
| 381 | QtWidgets.QShortcut("Ctrl+N", self, self.createNewProject) | 361 | QShortcut("Ctrl+O", self, self.openOpenProjectDialog) |
| 362 | QShortcut("Ctrl+N", self, self.createNewProject) | ||
| 382 | 363 | ||
| 383 | # Hotkeys for undo/redo | 364 | # Hotkeys for undo/redo |
| 384 | QtWidgets.QShortcut("Ctrl+Z", self, self.undoStack.undo) | 365 | QShortcut("Ctrl+Z", self, self.undoStack.undo) |
| 385 | QtWidgets.QShortcut("Ctrl+Y", self, self.undoStack.redo) | 366 | QShortcut("Ctrl+Y", self, self.undoStack.redo) |
| 386 | QtWidgets.QShortcut("Ctrl+Shift+Z", self, self.undoStack.redo) | 367 | QShortcut("Ctrl+Shift+Z", self, self.undoStack.redo) |
| 387 | 368 | ||
| 388 | # Hotkeys for component list | 369 | # Hotkeys for component list |
| 389 | for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): | 370 | for inskey in ("Ctrl+T", QtCore.Qt.Key.Key_Insert): |
| 390 | QtWidgets.QShortcut( | 371 | QShortcut( |
| 391 | inskey, self, | 372 | inskey, |
| 392 | activated=lambda: self.pushButton_addComponent.click() | 373 | self, |
| 393 | ) | 374 | activated=lambda: self.pushButton_addComponent.click(), |
| 394 | for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete): | ||
| 395 | QtWidgets.QShortcut( | ||
| 396 | delkey, self.listWidget_componentList, | ||
| 397 | self.removeComponent | ||
| 398 | ) | 375 | ) |
| 399 | QtWidgets.QShortcut( | 376 | for delkey in ("Ctrl+R", QtCore.Qt.Key.Key_Delete): |
| 400 | "Ctrl+Space", self, | 377 | QShortcut(delkey, self.listWidget_componentList, self.removeComponent) |
| 401 | activated=lambda: self.listWidget_componentList.setFocus() | 378 | QShortcut( |
| 402 | ) | 379 | "Ctrl+Space", |
| 403 | QtWidgets.QShortcut( | 380 | self, |
| 404 | "Ctrl+Shift+S", self, | 381 | activated=lambda: self.listWidget_componentList.setFocus(), |
| 405 | self.presetManager.openSavePresetDialog | ||
| 406 | ) | ||
| 407 | QtWidgets.QShortcut( | ||
| 408 | "Ctrl+Shift+C", self, self.presetManager.clearPreset | ||
| 409 | ) | 382 | ) |
| 383 | QShortcut("Ctrl+Shift+S", self, self.presetManager.openSavePresetDialog) | ||
| 384 | QShortcut("Ctrl+Shift+C", self, self.presetManager.clearPreset) | ||
| 410 | 385 | ||
| 411 | QtWidgets.QShortcut( | 386 | QShortcut( |
| 412 | "Ctrl+Up", self.listWidget_componentList, | 387 | "Ctrl+Up", |
| 413 | activated=lambda: self.moveComponent(-1) | 388 | self.listWidget_componentList, |
| 389 | activated=lambda: self.moveComponent(-1), | ||
| 414 | ) | 390 | ) |
| 415 | QtWidgets.QShortcut( | 391 | QShortcut( |
| 416 | "Ctrl+Down", self.listWidget_componentList, | 392 | "Ctrl+Down", |
| 417 | activated=lambda: self.moveComponent(1) | 393 | self.listWidget_componentList, |
| 394 | activated=lambda: self.moveComponent(1), | ||
| 418 | ) | 395 | ) |
| 419 | QtWidgets.QShortcut( | 396 | QShortcut( |
| 420 | "Ctrl+Home", self.listWidget_componentList, | 397 | "Ctrl+Home", |
| 421 | activated=lambda: self.moveComponent('top') | 398 | self.listWidget_componentList, |
| 399 | activated=lambda: self.moveComponent("top"), | ||
| 422 | ) | 400 | ) |
| 423 | QtWidgets.QShortcut( | 401 | QShortcut( |
| 424 | "Ctrl+End", self.listWidget_componentList, | 402 | "Ctrl+End", |
| 425 | activated=lambda: self.moveComponent('bottom') | 403 | self.listWidget_componentList, |
| 404 | activated=lambda: self.moveComponent("bottom"), | ||
| 426 | ) | 405 | ) |
| 427 | 406 | ||
| 428 | QtWidgets.QShortcut( | 407 | QShortcut("Ctrl+Shift+F", self, self.showFfmpegCommand) |
| 429 | "Ctrl+Shift+F", self, self.showFfmpegCommand | 408 | QShortcut("Ctrl+Shift+U", self, self.showUndoStack) |
| 430 | ) | ||
| 431 | QtWidgets.QShortcut( | ||
| 432 | "Ctrl+Shift+U", self, self.showUndoStack | ||
| 433 | ) | ||
| 434 | 409 | ||
| 435 | if log.isEnabledFor(logging.DEBUG): | 410 | if log.isEnabledFor(logging.DEBUG): |
| 436 | QtWidgets.QShortcut( | 411 | QShortcut("Ctrl+Alt+Shift+R", self, self.drawPreview) |
| 437 | "Ctrl+Alt+Shift+R", self, self.drawPreview | 412 | QShortcut("Ctrl+Alt+Shift+A", self, lambda: log.debug(repr(self))) |
| 438 | ) | ||
| 439 | QtWidgets.QShortcut( | ||
| 440 | "Ctrl+Alt+Shift+A", self, lambda: log.debug(repr(self)) | ||
| 441 | ) | ||
| 442 | 413 | ||
| 443 | # Close MainWindow when receiving Ctrl+C from terminal | 414 | # Close MainWindow when receiving Ctrl+C from terminal |
| 444 | signal.signal(signal.SIGINT, lambda *args: self.close()) | 415 | signal.signal(signal.SIGINT, lambda *args: self.close()) |
| @@ -450,18 +421,27 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 450 | 421 | ||
| 451 | def __repr__(self): | 422 | def __repr__(self): |
| 452 | return ( | 423 | return ( |
| 453 | '%s\n' | 424 | "%s\n" |
| 454 | '\n%s\n' | 425 | "\n%s\n" |
| 455 | '#####\n' | 426 | "#####\n" |
| 456 | 'Preview thread is %s\n' % ( | 427 | "Preview thread is %s\n" |
| 428 | % ( | ||
| 457 | super().__repr__(), | 429 | super().__repr__(), |
| 458 | "core not initialized" if not hasattr(self, "core") else repr(self.core), | 430 | ( |
| 459 | 'live' if hasattr(self, "previewThread") and self.previewThread.isRunning() else 'dead', | 431 | "core not initialized" |
| 432 | if not hasattr(self, "core") | ||
| 433 | else repr(self.core) | ||
| 434 | ), | ||
| 435 | ( | ||
| 436 | "live" | ||
| 437 | if hasattr(self, "previewThread") and self.previewThread.isRunning() | ||
| 438 | else "dead" | ||
| 439 | ), | ||
| 460 | ) | 440 | ) |
| 461 | ) | 441 | ) |
| 462 | 442 | ||
| 463 | def closeEvent(self, event): | 443 | def closeEvent(self, event): |
| 464 | log.info('Ending the preview thread') | 444 | log.info("Ending the preview thread") |
| 465 | self.timer.stop() | 445 | self.timer.stop() |
| 466 | self.previewThread.quit() | 446 | self.previewThread.quit() |
| 467 | self.previewThread.wait() | 447 | self.previewThread.wait() |
| @@ -473,11 +453,11 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 473 | windowTitle = appName | 453 | windowTitle = appName |
| 474 | try: | 454 | try: |
| 475 | if self.currentProject: | 455 | if self.currentProject: |
| 476 | windowTitle += ' - %s' % \ | 456 | windowTitle += ( |
| 477 | os.path.splitext( | 457 | " - %s" % os.path.splitext(os.path.basename(self.currentProject))[0] |
| 478 | os.path.basename(self.currentProject))[0] | 458 | ) |
| 479 | if self.autosaveExists(identical=False): | 459 | if self.autosaveExists(identical=False): |
| 480 | windowTitle += '*' | 460 | windowTitle += "*" |
| 481 | except AttributeError: | 461 | except AttributeError: |
| 482 | pass | 462 | pass |
| 483 | log.verbose(f'Window title is "{windowTitle}"') | 463 | log.verbose(f'Window title is "{windowTitle}"') |
| @@ -485,38 +465,38 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 485 | 465 | ||
| 486 | @QtCore.pyqtSlot(int, dict) | 466 | @QtCore.pyqtSlot(int, dict) |
| 487 | def updateComponentTitle(self, pos, presetStore=False): | 467 | def updateComponentTitle(self, pos, presetStore=False): |
| 488 | ''' | 468 | """ |
| 489 | Sets component title to modified or unmodified when given boolean. | 469 | Sets component title to modified or unmodified when given boolean. |
| 490 | If given a preset dict, compares it against the component to | 470 | If given a preset dict, compares it against the component to |
| 491 | determine if it is modified. | 471 | determine if it is modified. |
| 492 | A component with no preset is always unmodified. | 472 | A component with no preset is always unmodified. |
| 493 | ''' | 473 | """ |
| 494 | if type(presetStore) is dict: | 474 | if type(presetStore) is dict: |
| 495 | name = presetStore['preset'] | 475 | name = presetStore["preset"] |
| 496 | if name is None or name not in self.core.savedPresets: | 476 | if name is None or name not in self.core.savedPresets: |
| 497 | modified = False | 477 | modified = False |
| 498 | else: | 478 | else: |
| 499 | modified = (presetStore != self.core.savedPresets[name]) | 479 | modified = presetStore != self.core.savedPresets[name] |
| 500 | 480 | ||
| 501 | modified = bool(presetStore) | 481 | modified = bool(presetStore) |
| 502 | if pos < 0: | 482 | if pos < 0: |
| 503 | pos = len(self.core.selectedComponents)-1 | 483 | pos = len(self.core.selectedComponents) - 1 |
| 504 | name = self.core.selectedComponents[pos].name | 484 | name = self.core.selectedComponents[pos].name |
| 505 | title = str(name) | 485 | title = str(name) |
| 506 | if self.core.selectedComponents[pos].currentPreset: | 486 | if self.core.selectedComponents[pos].currentPreset: |
| 507 | title += ' - %s' % self.core.selectedComponents[pos].currentPreset | 487 | title += " - %s" % self.core.selectedComponents[pos].currentPreset |
| 508 | if modified: | 488 | if modified: |
| 509 | title += '*' | 489 | title += "*" |
| 510 | if type(presetStore) is bool: | 490 | if type(presetStore) is bool: |
| 511 | log.debug( | 491 | log.debug( |
| 512 | 'Forcing %s #%s\'s modified status to %s: %s', | 492 | "Forcing %s #%s's modified status to %s: %s", |
| 513 | name, pos, modified, title | 493 | name, |
| 494 | pos, | ||
| 495 | modified, | ||
| 496 | title, | ||
| 514 | ) | 497 | ) |
| 515 | else: | 498 | else: |
| 516 | log.debug( | 499 | log.debug("Setting %s #%s's title: %s", name, pos, title) |
| 517 | 'Setting %s #%s\'s title: %s', | ||
| 518 | name, pos, title | ||
| 519 | ) | ||
| 520 | self.listWidget_componentList.item(pos).setText(title) | 500 | self.listWidget_componentList.item(pos).setText(title) |
| 521 | 501 | ||
| 522 | def updateCodecs(self): | 502 | def updateCodecs(self): |
| @@ -525,20 +505,20 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 525 | aCodecWidget = self.comboBox_audioCodec | 505 | aCodecWidget = self.comboBox_audioCodec |
| 526 | index = containerWidget.currentIndex() | 506 | index = containerWidget.currentIndex() |
| 527 | name = containerWidget.itemText(index) | 507 | name = containerWidget.itemText(index) |
| 528 | self.settings.setValue('outputContainer', name) | 508 | self.settings.setValue("outputContainer", name) |
| 529 | 509 | ||
| 530 | vCodecWidget.clear() | 510 | vCodecWidget.clear() |
| 531 | aCodecWidget.clear() | 511 | aCodecWidget.clear() |
| 532 | 512 | ||
| 533 | for container in Core.encoderOptions['containers']: | 513 | for container in Core.encoderOptions["containers"]: |
| 534 | if container['name'] == name: | 514 | if container["name"] == name: |
| 535 | for vCodec in container['video-codecs']: | 515 | for vCodec in container["video-codecs"]: |
| 536 | vCodecWidget.addItem(vCodec) | 516 | vCodecWidget.addItem(vCodec) |
| 537 | for aCodec in container['audio-codecs']: | 517 | for aCodec in container["audio-codecs"]: |
| 538 | aCodecWidget.addItem(aCodec) | 518 | aCodecWidget.addItem(aCodec) |
| 539 | 519 | ||
| 540 | def updateCodecSettings(self): | 520 | def updateCodecSettings(self): |
| 541 | '''Updates settings.ini to match encoder option widgets''' | 521 | """Updates settings.ini to match encoder option widgets""" |
| 542 | vCodecWidget = self.comboBox_videoCodec | 522 | vCodecWidget = self.comboBox_videoCodec |
| 543 | vBitrateWidget = self.spinBox_vBitrate | 523 | vBitrateWidget = self.spinBox_vBitrate |
| 544 | aBitrateWidget = self.spinBox_aBitrate | 524 | aBitrateWidget = self.spinBox_aBitrate |
| @@ -549,10 +529,10 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 549 | currentAudioCodec = aCodecWidget.currentIndex() | 529 | currentAudioCodec = aCodecWidget.currentIndex() |
| 550 | currentAudioCodec = aCodecWidget.itemText(currentAudioCodec) | 530 | currentAudioCodec = aCodecWidget.itemText(currentAudioCodec) |
| 551 | currentAudioBitrate = aBitrateWidget.value() | 531 | currentAudioBitrate = aBitrateWidget.value() |
| 552 | self.settings.setValue('outputVideoCodec', currentVideoCodec) | 532 | self.settings.setValue("outputVideoCodec", currentVideoCodec) |
| 553 | self.settings.setValue('outputAudioCodec', currentAudioCodec) | 533 | self.settings.setValue("outputAudioCodec", currentAudioCodec) |
| 554 | self.settings.setValue('outputVideoBitrate', currentVideoBitrate) | 534 | self.settings.setValue("outputVideoBitrate", currentVideoBitrate) |
| 555 | self.settings.setValue('outputAudioBitrate', currentAudioBitrate) | 535 | self.settings.setValue("outputAudioBitrate", currentAudioBitrate) |
| 556 | 536 | ||
| 557 | @disableWhenOpeningProject | 537 | @disableWhenOpeningProject |
| 558 | def autosave(self, force=False): | 538 | def autosave(self, force=False): |
| @@ -567,54 +547,54 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 567 | # curve up to 5 seconds cooldown and maintains that for 30 secs | 547 | # curve up to 5 seconds cooldown and maintains that for 30 secs |
| 568 | # if a component is continuously updated | 548 | # if a component is continuously updated |
| 569 | timeDiff = self.lastAutosave - self.autosaveTimes.pop() | 549 | timeDiff = self.lastAutosave - self.autosaveTimes.pop() |
| 570 | if not force and timeDiff >= 1.0 \ | 550 | if not force and timeDiff >= 1.0 and timeDiff <= 10.0: |
| 571 | and timeDiff <= 10.0: | ||
| 572 | if self.autosaveCooldown / 4.0 < 0.5: | 551 | if self.autosaveCooldown / 4.0 < 0.5: |
| 573 | self.autosaveCooldown += 1.0 | 552 | self.autosaveCooldown += 1.0 |
| 574 | self.autosaveCooldown = ( | 553 | self.autosaveCooldown = (5.0 * (self.autosaveCooldown / 5.0)) + ( |
| 575 | 5.0 * (self.autosaveCooldown / 5.0) | 554 | self.autosaveCooldown / 5.0 |
| 576 | ) + (self.autosaveCooldown / 5.0) * 2 | 555 | ) * 2 |
| 577 | elif force or timeDiff >= self.autosaveCooldown * 5: | 556 | elif force or timeDiff >= self.autosaveCooldown * 5: |
| 578 | self.autosaveCooldown = 0.2 | 557 | self.autosaveCooldown = 0.2 |
| 579 | self.autosaveTimes.insert(0, self.lastAutosave) | 558 | self.autosaveTimes.insert(0, self.lastAutosave) |
| 580 | else: | 559 | else: |
| 581 | log.debug('Autosave rejected by cooldown') | 560 | log.debug("Autosave rejected by cooldown") |
| 582 | 561 | ||
| 583 | def autosaveExists(self, identical=True): | 562 | def autosaveExists(self, identical=True): |
| 584 | '''Determines if creating the autosave should be blocked.''' | 563 | """Determines if creating the autosave should be blocked.""" |
| 585 | try: | 564 | try: |
| 586 | if self.currentProject and os.path.exists(self.autosavePath) \ | 565 | if ( |
| 587 | and filecmp.cmp( | 566 | self.currentProject |
| 588 | self.autosavePath, self.currentProject) == identical: | 567 | and os.path.exists(self.autosavePath) |
| 568 | and filecmp.cmp(self.autosavePath, self.currentProject) == identical | ||
| 569 | ): | ||
| 589 | log.debug( | 570 | log.debug( |
| 590 | 'Autosave found %s to be identical' | 571 | "Autosave found %s to be identical" % "not" if not identical else "" |
| 591 | % 'not' if not identical else '' | ||
| 592 | ) | 572 | ) |
| 593 | return True | 573 | return True |
| 594 | except FileNotFoundError: | 574 | except FileNotFoundError: |
| 595 | log.error( | 575 | log.error("Project file couldn't be located: %s", self.currentProject) |
| 596 | 'Project file couldn\'t be located: %s', self.currentProject) | ||
| 597 | return identical | 576 | return identical |
| 598 | return False | 577 | return False |
| 599 | 578 | ||
| 600 | def saveProjectChanges(self): | 579 | def saveProjectChanges(self): |
| 601 | '''Overwrites project file with autosave file''' | 580 | """Overwrites project file with autosave file""" |
| 602 | try: | 581 | try: |
| 603 | os.remove(self.currentProject) | 582 | os.remove(self.currentProject) |
| 604 | os.rename(self.autosavePath, self.currentProject) | 583 | os.rename(self.autosavePath, self.currentProject) |
| 605 | return True | 584 | return True |
| 606 | except (FileNotFoundError, IsADirectoryError) as e: | 585 | except (FileNotFoundError, IsADirectoryError) as e: |
| 607 | self.showMessage( | 586 | self.showMessage(msg="Project file couldn't be saved.", detail=str(e)) |
| 608 | msg='Project file couldn\'t be saved.', | ||
| 609 | detail=str(e)) | ||
| 610 | return False | 587 | return False |
| 611 | 588 | ||
| 612 | def openInputFileDialog(self): | 589 | def openInputFileDialog(self): |
| 613 | inputDir = self.settings.value("inputDir", os.path.expanduser("~")) | 590 | inputDir = self.settings.value("inputDir", os.path.expanduser("~")) |
| 614 | 591 | ||
| 615 | fileName, _ = QtWidgets.QFileDialog.getOpenFileName( | 592 | fileName, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 616 | self, "Open Audio File", | 593 | self, |
| 617 | inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats)) | 594 | "Open Audio File", |
| 595 | inputDir, | ||
| 596 | "Audio Files (%s)" % " ".join(Core.audioFormats), | ||
| 597 | ) | ||
| 618 | 598 | ||
| 619 | if fileName: | 599 | if fileName: |
| 620 | self.settings.setValue("inputDir", os.path.dirname(fileName)) | 600 | self.settings.setValue("inputDir", os.path.dirname(fileName)) |
| @@ -624,17 +604,18 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 624 | outputDir = self.settings.value("outputDir", os.path.expanduser("~")) | 604 | outputDir = self.settings.value("outputDir", os.path.expanduser("~")) |
| 625 | 605 | ||
| 626 | fileName, _ = QtWidgets.QFileDialog.getSaveFileName( | 606 | fileName, _ = QtWidgets.QFileDialog.getSaveFileName( |
| 627 | self, "Set Output Video File", | 607 | self, |
| 608 | "Set Output Video File", | ||
| 628 | outputDir, | 609 | outputDir, |
| 629 | "Video Files (%s);; All Files (*)" % " ".join( | 610 | "Video Files (%s);; All Files (*)" % " ".join(Core.videoFormats), |
| 630 | Core.videoFormats)) | 611 | ) |
| 631 | 612 | ||
| 632 | if fileName: | 613 | if fileName: |
| 633 | self.settings.setValue("outputDir", os.path.dirname(fileName)) | 614 | self.settings.setValue("outputDir", os.path.dirname(fileName)) |
| 634 | self.lineEdit_outputFile.setText(fileName) | 615 | self.lineEdit_outputFile.setText(fileName) |
| 635 | 616 | ||
| 636 | def stopVideo(self): | 617 | def stopVideo(self): |
| 637 | log.info('Export cancelled') | 618 | log.info("Export cancelled") |
| 638 | self.videoWorker.cancel() | 619 | self.videoWorker.cancel() |
| 639 | self.canceled = True | 620 | self.canceled = True |
| 640 | 621 | ||
| @@ -645,14 +626,13 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 645 | 626 | ||
| 646 | if audioFile and outputPath and self.core.selectedComponents: | 627 | if audioFile and outputPath and self.core.selectedComponents: |
| 647 | if not os.path.dirname(outputPath): | 628 | if not os.path.dirname(outputPath): |
| 648 | outputPath = os.path.join( | 629 | outputPath = os.path.join(os.path.expanduser("~"), outputPath) |
| 649 | os.path.expanduser("~"), outputPath) | ||
| 650 | if outputPath and os.path.isdir(outputPath): | 630 | if outputPath and os.path.isdir(outputPath): |
| 651 | self.showMessage( | 631 | self.showMessage( |
| 652 | msg='Chosen filename matches a directory, which ' | 632 | msg="Chosen filename matches a directory, which " |
| 653 | 'cannot be overwritten. Please choose a different ' | 633 | "cannot be overwritten. Please choose a different " |
| 654 | 'filename or move the directory.', | 634 | "filename or move the directory.", |
| 655 | icon='Warning', | 635 | icon="Warning", |
| 656 | ) | 636 | ) |
| 657 | return | 637 | return |
| 658 | else: | 638 | else: |
| @@ -661,19 +641,14 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 661 | msg="You must select an audio file and output filename." | 641 | msg="You must select an audio file and output filename." |
| 662 | ) | 642 | ) |
| 663 | elif not self.core.selectedComponents: | 643 | elif not self.core.selectedComponents: |
| 664 | self.showMessage( | 644 | self.showMessage(msg="Not enough components.") |
| 665 | msg="Not enough components." | ||
| 666 | ) | ||
| 667 | return | 645 | return |
| 668 | 646 | ||
| 669 | self.canceled = False | 647 | self.canceled = False |
| 670 | self.progressBarUpdated(-1) | 648 | self.progressBarUpdated(-1) |
| 671 | self.videoWorker = self.core.newVideoWorker( | 649 | self.videoWorker = self.core.newVideoWorker(self, audioFile, outputPath) |
| 672 | self, audioFile, outputPath | ||
| 673 | ) | ||
| 674 | self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) | 650 | self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) |
| 675 | self.videoWorker.progressBarSetText.connect( | 651 | self.videoWorker.progressBarSetText.connect(self.progressBarSetText) |
| 676 | self.progressBarSetText) | ||
| 677 | self.videoWorker.imageCreated.connect(self.showPreviewImage) | 652 | self.videoWorker.imageCreated.connect(self.showPreviewImage) |
| 678 | self.videoWorker.encoding.connect(self.changeEncodingStatus) | 653 | self.videoWorker.encoding.connect(self.changeEncodingStatus) |
| 679 | self.createVideo.emit() | 654 | self.createVideo.emit() |
| @@ -683,14 +658,14 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 683 | try: | 658 | try: |
| 684 | self.stopVideo() | 659 | self.stopVideo() |
| 685 | except AttributeError as e: | 660 | except AttributeError as e: |
| 686 | if 'videoWorker' not in str(e): | 661 | if "videoWorker" not in str(e): |
| 687 | raise | 662 | raise |
| 688 | self.showMessage( | 663 | self.showMessage( |
| 689 | msg=msg, | 664 | msg=msg, |
| 690 | detail=detail, | 665 | detail=detail, |
| 691 | icon='Critical', | 666 | icon="Critical", |
| 692 | ) | 667 | ) |
| 693 | log.info('%s', repr(self)) | 668 | log.info("%s", repr(self)) |
| 694 | 669 | ||
| 695 | def changeEncodingStatus(self, status): | 670 | def changeEncodingStatus(self, status): |
| 696 | self.encoding = status | 671 | self.encoding = status |
| @@ -718,7 +693,7 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 718 | # Close undo history dialog if open | 693 | # Close undo history dialog if open |
| 719 | self.undoDialog.close() | 694 | self.undoDialog.close() |
| 720 | # Show label under progress bar on macOS | 695 | # Show label under progress bar on macOS |
| 721 | if sys.platform == 'darwin': | 696 | if sys.platform == "darwin": |
| 722 | self.progressLabel.setHidden(False) | 697 | self.progressLabel.setHidden(False) |
| 723 | else: | 698 | else: |
| 724 | self.pushButton_createVideo.setEnabled(True) | 699 | self.pushButton_createVideo.setEnabled(True) |
| @@ -749,33 +724,33 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 749 | 724 | ||
| 750 | @QtCore.pyqtSlot(str) | 725 | @QtCore.pyqtSlot(str) |
| 751 | def progressBarSetText(self, value): | 726 | def progressBarSetText(self, value): |
| 752 | if sys.platform == 'darwin': | 727 | if sys.platform == "darwin": |
| 753 | self.progressLabel.setText(value) | 728 | self.progressLabel.setText(value) |
| 754 | else: | 729 | else: |
| 755 | self.progressBar_createVideo.setFormat(value) | 730 | self.progressBar_createVideo.setFormat(value) |
| 756 | 731 | ||
| 757 | def updateResolution(self): | 732 | def updateResolution(self): |
| 758 | resIndex = int(self.comboBox_resolution.currentIndex()) | 733 | resIndex = int(self.comboBox_resolution.currentIndex()) |
| 759 | res = Core.resolutions[resIndex].split('x') | 734 | res = Core.resolutions[resIndex].split("x") |
| 760 | changed = res[0] != self.settings.value("outputWidth") | 735 | changed = res[0] != self.settings.value("outputWidth") |
| 761 | self.settings.setValue('outputWidth', res[0]) | 736 | self.settings.setValue("outputWidth", res[0]) |
| 762 | self.settings.setValue('outputHeight', res[1]) | 737 | self.settings.setValue("outputHeight", res[1]) |
| 763 | if changed: | 738 | if changed: |
| 764 | for i in range(len(self.core.selectedComponents)): | 739 | for i in range(len(self.core.selectedComponents)): |
| 765 | self.core.updateComponent(i) | 740 | self.core.updateComponent(i) |
| 766 | 741 | ||
| 767 | def drawPreview(self, force=False, **kwargs): | 742 | def drawPreview(self, force=False, **kwargs): |
| 768 | '''Use autosave keyword arg to force saving or not saving if needed''' | 743 | """Use autosave keyword arg to force saving or not saving if needed""" |
| 769 | self.newTask.emit(self.core.selectedComponents) | 744 | self.newTask.emit(self.core.selectedComponents) |
| 770 | # self.processTask.emit() | 745 | # self.processTask.emit() |
| 771 | if force or 'autosave' in kwargs: | 746 | if force or "autosave" in kwargs: |
| 772 | if force or kwargs['autosave']: | 747 | if force or kwargs["autosave"]: |
| 773 | self.autosave(True) | 748 | self.autosave(True) |
| 774 | else: | 749 | else: |
| 775 | self.autosave() | 750 | self.autosave() |
| 776 | self.updateWindowTitle() | 751 | self.updateWindowTitle() |
| 777 | 752 | ||
| 778 | @QtCore.pyqtSlot('QImage') | 753 | @QtCore.pyqtSlot("QImage") |
| 779 | def showPreviewImage(self, image): | 754 | def showPreviewImage(self, image): |
| 780 | self.previewWindow.changePixmap(image) | 755 | self.previewWindow.changePixmap(image) |
| 781 | 756 | ||
| @@ -786,36 +761,35 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 786 | def showFfmpegCommand(self): | 761 | def showFfmpegCommand(self): |
| 787 | from textwrap import wrap | 762 | from textwrap import wrap |
| 788 | from ..toolkit.ffmpeg import createFfmpegCommand | 763 | from ..toolkit.ffmpeg import createFfmpegCommand |
| 764 | |||
| 789 | command = createFfmpegCommand( | 765 | command = createFfmpegCommand( |
| 790 | self.lineEdit_audioFile.text(), | 766 | self.lineEdit_audioFile.text(), |
| 791 | self.lineEdit_outputFile.text(), | 767 | self.lineEdit_outputFile.text(), |
| 792 | self.core.selectedComponents | 768 | self.core.selectedComponents, |
| 793 | ) | 769 | ) |
| 794 | command = " ".join(command) | 770 | command = " ".join(command) |
| 795 | log.info(f"FFmpeg command: {command}") | 771 | log.info(f"FFmpeg command: {command}") |
| 796 | lines = wrap(command, 49) | 772 | lines = wrap(command, 49) |
| 797 | self.showMessage( | 773 | self.showMessage(msg=f"Current FFmpeg command:\n\n{' '.join(lines)}") |
| 798 | msg=f"Current FFmpeg command:\n\n{' '.join(lines)}" | ||
| 799 | ) | ||
| 800 | 774 | ||
| 801 | def addComponent(self, compPos, moduleIndex): | 775 | def addComponent(self, compPos, moduleIndex): |
| 802 | '''Creates an undoable action that adds a new component.''' | 776 | """Creates an undoable action that adds a new component.""" |
| 803 | action = AddComponent(self, compPos, moduleIndex) | 777 | action = AddComponent(self, compPos, moduleIndex) |
| 804 | self.undoStack.push(action) | 778 | self.undoStack.push(action) |
| 805 | 779 | ||
| 806 | def insertComponent(self, index): | 780 | def insertComponent(self, index): |
| 807 | '''Triggered by Core to finish initializing a new component.''' | 781 | """Triggered by Core to finish initializing a new component.""" |
| 782 | if not hasattr(self.core.selectedComponents[index], "page"): | ||
| 783 | log.error("Component failed to initialize") | ||
| 784 | return | ||
| 808 | componentList = self.listWidget_componentList | 785 | componentList = self.listWidget_componentList |
| 809 | stackedWidget = self.stackedWidget | 786 | stackedWidget = self.stackedWidget |
| 810 | 787 | ||
| 811 | componentList.insertItem( | 788 | componentList.insertItem(index, self.core.selectedComponents[index].name) |
| 812 | index, | ||
| 813 | self.core.selectedComponents[index].name) | ||
| 814 | componentList.setCurrentRow(index) | 789 | componentList.setCurrentRow(index) |
| 815 | 790 | ||
| 816 | # connect to signal that adds an asterisk when modified | 791 | # connect to signal that adds an asterisk when modified |
| 817 | self.core.selectedComponents[index].modified.connect( | 792 | self.core.selectedComponents[index].modified.connect(self.updateComponentTitle) |
| 818 | self.updateComponentTitle) | ||
| 819 | 793 | ||
| 820 | self.pages.insert(index, self.core.selectedComponents[index].page) | 794 | self.pages.insert(index, self.core.selectedComponents[index].page) |
| 821 | stackedWidget.insertWidget(index, self.pages[index]) | 795 | stackedWidget.insertWidget(index, self.pages[index]) |
| @@ -842,15 +816,15 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 842 | 816 | ||
| 843 | @disableWhenEncoding | 817 | @disableWhenEncoding |
| 844 | def moveComponent(self, change): | 818 | def moveComponent(self, change): |
| 845 | '''Moves a component relatively from its current position''' | 819 | """Moves a component relatively from its current position""" |
| 846 | componentList = self.listWidget_componentList | 820 | componentList = self.listWidget_componentList |
| 847 | tag = change | 821 | tag = change |
| 848 | if change == 'top': | 822 | if change == "top": |
| 849 | change = -componentList.currentRow() | 823 | change = -componentList.currentRow() |
| 850 | elif change == 'bottom': | 824 | elif change == "bottom": |
| 851 | change = len(componentList)-componentList.currentRow()-1 | 825 | change = len(componentList) - componentList.currentRow() - 1 |
| 852 | else: | 826 | else: |
| 853 | tag = 'down' if change == 1 else 'up' | 827 | tag = "down" if change == 1 else "up" |
| 854 | 828 | ||
| 855 | row = componentList.currentRow() | 829 | row = componentList.currentRow() |
| 856 | newRow = row + change | 830 | newRow = row + change |
| @@ -859,38 +833,39 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 859 | self.undoStack.push(action) | 833 | self.undoStack.push(action) |
| 860 | 834 | ||
| 861 | def getComponentListMousePos(self, position): | 835 | def getComponentListMousePos(self, position): |
| 862 | ''' | 836 | """ |
| 863 | Given a QPos, returns the component index under the mouse cursor | 837 | Given a QPos, returns the component index under the mouse cursor |
| 864 | or -1 if no component is there. | 838 | or -1 if no component is there. |
| 865 | ''' | 839 | """ |
| 866 | componentList = self.listWidget_componentList | 840 | componentList = self.listWidget_componentList |
| 867 | 841 | ||
| 842 | if hasattr(position, "toPointF"): | ||
| 843 | position = position.toPointF() | ||
| 844 | position = position.toPoint() | ||
| 845 | |||
| 868 | modelIndexes = [ | 846 | modelIndexes = [ |
| 869 | componentList.model().index(i) | 847 | componentList.model().index(i) for i in range(componentList.count()) |
| 870 | for i in range(componentList.count()) | ||
| 871 | ] | ||
| 872 | rects = [ | ||
| 873 | componentList.visualRect(modelIndex) | ||
| 874 | for modelIndex in modelIndexes | ||
| 875 | ] | 848 | ] |
| 849 | rects = [componentList.visualRect(modelIndex) for modelIndex in modelIndexes] | ||
| 876 | mousePos = [rect.contains(position) for rect in rects] | 850 | mousePos = [rect.contains(position) for rect in rects] |
| 877 | if not any(mousePos): | 851 | if not any(mousePos): |
| 878 | # Not clicking a component | 852 | # Not clicking a component |
| 879 | mousePos = -1 | 853 | mousePos = -1 |
| 880 | else: | 854 | else: |
| 881 | mousePos = mousePos.index(True) | 855 | mousePos = mousePos.index(True) |
| 882 | log.debug('Click component list row %s' % mousePos) | 856 | log.debug("Click component list row %s" % mousePos) |
| 883 | return mousePos | 857 | return mousePos |
| 884 | 858 | ||
| 885 | @disableWhenEncoding | 859 | @disableWhenEncoding |
| 886 | def dragComponent(self, event): | 860 | def dragComponent(self, event): |
| 887 | '''Used as Qt drop event for the component listwidget''' | 861 | """Used as Qt drop event for the component listwidget""" |
| 888 | componentList = self.listWidget_componentList | 862 | componentList = self.listWidget_componentList |
| 889 | mousePos = self.getComponentListMousePos(event.pos()) | 863 | mousePos = self.getComponentListMousePos(event.position()) |
| 864 | |||
| 890 | if mousePos > -1: | 865 | if mousePos > -1: |
| 891 | change = (componentList.currentRow() - mousePos) * -1 | 866 | change = (componentList.currentRow() - mousePos) * -1 |
| 892 | else: | 867 | else: |
| 893 | change = (componentList.count() - componentList.currentRow() - 1) | 868 | change = componentList.count() - componentList.currentRow() - 1 |
| 894 | self.moveComponent(change) | 869 | self.moveComponent(change) |
| 895 | 870 | ||
| 896 | def changeComponentWidget(self): | 871 | def changeComponentWidget(self): |
| @@ -900,30 +875,27 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 900 | self.stackedWidget.setCurrentIndex(index) | 875 | self.stackedWidget.setCurrentIndex(index) |
| 901 | 876 | ||
| 902 | def openPresetManager(self): | 877 | def openPresetManager(self): |
| 903 | '''Preset manager for importing, exporting, renaming, deleting''' | 878 | """Preset manager for importing, exporting, renaming, deleting""" |
| 904 | self.presetManager.show_() | 879 | self.presetManager.show_() |
| 905 | 880 | ||
| 906 | def clear(self): | 881 | def clear(self): |
| 907 | '''Get a blank slate''' | 882 | """Get a blank slate""" |
| 908 | self.core.clearComponents() | 883 | self.core.clearComponents() |
| 909 | self.listWidget_componentList.clear() | 884 | self.listWidget_componentList.clear() |
| 910 | for widget in self.pages: | 885 | for widget in self.pages: |
| 911 | self.stackedWidget.removeWidget(widget) | 886 | self.stackedWidget.removeWidget(widget) |
| 912 | self.pages = [] | 887 | self.pages = [] |
| 913 | for field in ( | 888 | for field in (self.lineEdit_audioFile, self.lineEdit_outputFile): |
| 914 | self.lineEdit_audioFile, | ||
| 915 | self.lineEdit_outputFile | ||
| 916 | ): | ||
| 917 | with blockSignals(field): | 889 | with blockSignals(field): |
| 918 | field.setText('') | 890 | field.setText("") |
| 919 | self.progressBarUpdated(0) | 891 | self.progressBarUpdated(0) |
| 920 | self.progressBarSetText('') | 892 | self.progressBarSetText("") |
| 921 | self.undoStack.clear() | 893 | self.undoStack.clear() |
| 922 | 894 | ||
| 923 | @disableWhenEncoding | 895 | @disableWhenEncoding |
| 924 | def createNewProject(self, prompt=True): | 896 | def createNewProject(self, prompt=True): |
| 925 | if prompt: | 897 | if prompt: |
| 926 | self.openSaveChangesDialog('starting a new project') | 898 | self.openSaveChangesDialog("starting a new project") |
| 927 | 899 | ||
| 928 | self.clear() | 900 | self.clear() |
| 929 | self.currentProject = None | 901 | self.currentProject = None |
| @@ -946,11 +918,10 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 946 | if self.autosaveExists(identical=False): | 918 | if self.autosaveExists(identical=False): |
| 947 | ch = self.showMessage( | 919 | ch = self.showMessage( |
| 948 | msg="You have unsaved changes in project '%s'. " | 920 | msg="You have unsaved changes in project '%s'. " |
| 949 | "Save before %s?" % ( | 921 | "Save before %s?" |
| 950 | os.path.basename(self.currentProject)[:-4], | 922 | % (os.path.basename(self.currentProject)[:-4], phrase), |
| 951 | phrase | 923 | showCancel=True, |
| 952 | ), | 924 | ) |
| 953 | showCancel=True) | ||
| 954 | if ch: | 925 | if ch: |
| 955 | success = self.saveProjectChanges() | 926 | success = self.saveProjectChanges() |
| 956 | 927 | ||
| @@ -959,13 +930,15 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 959 | 930 | ||
| 960 | def openSaveProjectDialog(self): | 931 | def openSaveProjectDialog(self): |
| 961 | filename, _ = QtWidgets.QFileDialog.getSaveFileName( | 932 | filename, _ = QtWidgets.QFileDialog.getSaveFileName( |
| 962 | self, "Create Project File", | 933 | self, |
| 934 | "Create Project File", | ||
| 963 | self.settings.value("projectDir"), | 935 | self.settings.value("projectDir"), |
| 964 | "Project Files (*.avp)") | 936 | "Project Files (*.avp)", |
| 937 | ) | ||
| 965 | if not filename: | 938 | if not filename: |
| 966 | return | 939 | return |
| 967 | if not filename.endswith(".avp"): | 940 | if not filename.endswith(".avp"): |
| 968 | filename += '.avp' | 941 | filename += ".avp" |
| 969 | self.settings.setValue("projectDir", os.path.dirname(filename)) | 942 | self.settings.setValue("projectDir", os.path.dirname(filename)) |
| 970 | self.settings.setValue("currentProject", filename) | 943 | self.settings.setValue("currentProject", filename) |
| 971 | self.currentProject = filename | 944 | self.currentProject = filename |
| @@ -975,20 +948,25 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 975 | @disableWhenEncoding | 948 | @disableWhenEncoding |
| 976 | def openOpenProjectDialog(self): | 949 | def openOpenProjectDialog(self): |
| 977 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 950 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 978 | self, "Open Project File", | 951 | self, |
| 952 | "Open Project File", | ||
| 979 | self.settings.value("projectDir"), | 953 | self.settings.value("projectDir"), |
| 980 | "Project Files (*.avp)") | 954 | "Project Files (*.avp)", |
| 955 | ) | ||
| 981 | self.openProject(filename) | 956 | self.openProject(filename) |
| 982 | 957 | ||
| 983 | def openProject(self, filepath, prompt=True): | 958 | def openProject(self, filepath, prompt=True): |
| 984 | if not filepath or not os.path.exists(filepath) \ | 959 | if ( |
| 985 | or not filepath.endswith('.avp'): | 960 | not filepath |
| 961 | or not os.path.exists(filepath) | ||
| 962 | or not filepath.endswith(".avp") | ||
| 963 | ): | ||
| 986 | return | 964 | return |
| 987 | 965 | ||
| 988 | self.clear() | 966 | self.clear() |
| 989 | # ask to save any changes that are about to get deleted | 967 | # ask to save any changes that are about to get deleted |
| 990 | if prompt: | 968 | if prompt: |
| 991 | self.openSaveChangesDialog('opening another project') | 969 | self.openSaveChangesDialog("opening another project") |
| 992 | 970 | ||
| 993 | self.currentProject = filepath | 971 | self.currentProject = filepath |
| 994 | self.settings.setValue("currentProject", filepath) | 972 | self.settings.setValue("currentProject", filepath) |
| @@ -999,29 +977,32 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 999 | self.updateWindowTitle() | 977 | self.updateWindowTitle() |
| 1000 | 978 | ||
| 1001 | def showMessage(self, **kwargs): | 979 | def showMessage(self, **kwargs): |
| 1002 | parent = kwargs['parent'] if 'parent' in kwargs else self | 980 | parent = kwargs["parent"] if "parent" in kwargs else self |
| 1003 | msg = QtWidgets.QMessageBox(parent) | 981 | msg = QtWidgets.QMessageBox(parent) |
| 1004 | msg.setWindowTitle(appName) | 982 | msg.setWindowTitle(appName) |
| 1005 | msg.setModal(True) | 983 | msg.setModal(True) |
| 1006 | msg.setText(kwargs['msg']) | 984 | msg.setText(kwargs["msg"]) |
| 1007 | msg.setIcon( | 985 | msg.setIcon( |
| 1008 | eval('QtWidgets.QMessageBox.%s' % kwargs['icon']) | 986 | eval("QtWidgets.QMessageBox.Icon.%s" % kwargs["icon"]) |
| 1009 | if 'icon' in kwargs else QtWidgets.QMessageBox.Information | 987 | if "icon" in kwargs |
| 988 | else QtWidgets.QMessageBox.Icon.Information | ||
| 1010 | ) | 989 | ) |
| 1011 | msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None) | 990 | msg.setDetailedText(kwargs["detail"] if "detail" in kwargs else None) |
| 1012 | if 'showCancel'in kwargs and kwargs['showCancel']: | 991 | if "showCancel" in kwargs and kwargs["showCancel"]: |
| 1013 | msg.setStandardButtons( | 992 | msg.setStandardButtons( |
| 1014 | QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) | 993 | QtWidgets.QMessageBox.StandardButton.Ok |
| 994 | | QtWidgets.QMessageBox.StandardButton.Cancel | ||
| 995 | ) | ||
| 1015 | else: | 996 | else: |
| 1016 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok) | 997 | msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) |
| 1017 | ch = msg.exec_() | 998 | ch = msg.exec() |
| 1018 | if ch == 1024: | 999 | if ch == 1024: |
| 1019 | return True | 1000 | return True |
| 1020 | return False | 1001 | return False |
| 1021 | 1002 | ||
| 1022 | @disableWhenEncoding | 1003 | @disableWhenEncoding |
| 1023 | def componentContextMenu(self, QPos): | 1004 | def componentContextMenu(self, QPos): |
| 1024 | '''Appears when right-clicking the component list''' | 1005 | """Appears when right-clicking the component list""" |
| 1025 | componentList = self.listWidget_componentList | 1006 | componentList = self.listWidget_componentList |
| 1026 | self.menu = QtWidgets.QMenu() | 1007 | self.menu = QtWidgets.QMenu() |
| 1027 | parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) | 1008 | parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) |
| @@ -1031,9 +1012,7 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 1031 | # Show preset menu if clicking a component | 1012 | # Show preset menu if clicking a component |
| 1032 | self.presetManager.findPresets() | 1013 | self.presetManager.findPresets() |
| 1033 | menuItem = self.menu.addAction("Save Preset") | 1014 | menuItem = self.menu.addAction("Save Preset") |
| 1034 | menuItem.triggered.connect( | 1015 | menuItem.triggered.connect(self.presetManager.openSavePresetDialog) |
| 1035 | self.presetManager.openSavePresetDialog | ||
| 1036 | ) | ||
| 1037 | 1016 | ||
| 1038 | # submenu for opening presets | 1017 | # submenu for opening presets |
| 1039 | try: | 1018 | try: |
| @@ -1046,17 +1025,16 @@ class MainWindow(QtWidgets.QMainWindow): | |||
| 1046 | for version, presetName in presets: | 1025 | for version, presetName in presets: |
| 1047 | menuItem = self.presetSubmenu.addAction(presetName) | 1026 | menuItem = self.presetSubmenu.addAction(presetName) |
| 1048 | menuItem.triggered.connect( | 1027 | menuItem.triggered.connect( |
| 1049 | lambda _, presetName=presetName: | 1028 | lambda _, presetName=presetName: self.presetManager.openPreset( |
| 1050 | self.presetManager.openPreset(presetName) | 1029 | presetName |
| 1030 | ) | ||
| 1051 | ) | 1031 | ) |
| 1052 | except KeyError: | 1032 | except KeyError: |
| 1053 | pass | 1033 | pass |
| 1054 | 1034 | ||
| 1055 | if self.core.selectedComponents[index].currentPreset: | 1035 | if self.core.selectedComponents[index].currentPreset: |
| 1056 | menuItem = self.menu.addAction("Clear Preset") | 1036 | menuItem = self.menu.addAction("Clear Preset") |
| 1057 | menuItem.triggered.connect( | 1037 | menuItem.triggered.connect(self.presetManager.clearPreset) |
| 1058 | self.presetManager.clearPreset | ||
| 1059 | ) | ||
| 1060 | self.menu.addSeparator() | 1038 | self.menu.addSeparator() |
| 1061 | 1039 | ||
| 1062 | # "Add Component" submenu | 1040 | # "Add Component" submenu |
diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 9cf95b4..11a9d9b 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Preset manager object handles all interactions with presets, including | 2 | Preset manager object handles all interactions with presets, including |
| 3 | the context menu accessed from MainWindow. | 3 | the context menu accessed from MainWindow. |
| 4 | ''' | 4 | """ |
| 5 | from PyQt5 import QtCore, QtWidgets, uic | 5 | |
| 6 | from PyQt6 import QtCore, QtWidgets, uic | ||
| 6 | import string | 7 | import string |
| 7 | import os | 8 | import os |
| 8 | import logging | 9 | import logging |
| @@ -12,53 +13,43 @@ from ..core import Core | |||
| 12 | from .actions import * | 13 | from .actions import * |
| 13 | 14 | ||
| 14 | 15 | ||
| 15 | log = logging.getLogger('AVP.Gui.PresetManager') | 16 | log = logging.getLogger("AVP.Gui.PresetManager") |
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | class PresetManager(QtWidgets.QDialog): | 19 | class PresetManager(QtWidgets.QDialog): |
| 19 | def __init__(self, parent): | 20 | def __init__(self, parent): |
| 20 | super().__init__() | 21 | super().__init__() |
| 21 | uic.loadUi( | 22 | uic.loadUi(os.path.join(Core.wd, "gui", "presetmanager.ui"), self) |
| 22 | os.path.join(Core.wd, 'gui', 'presetmanager.ui'), self) | ||
| 23 | self.parent = parent | 23 | self.parent = parent |
| 24 | self.core = parent.core | 24 | self.core = parent.core |
| 25 | self.settings = parent.settings | 25 | self.settings = parent.settings |
| 26 | self.presetDir = parent.presetDir | 26 | self.presetDir = parent.presetDir |
| 27 | if not self.settings.value('presetDir'): | 27 | if not self.settings.value("presetDir"): |
| 28 | self.settings.setValue( | 28 | self.settings.setValue( |
| 29 | "presetDir", | 29 | "presetDir", os.path.join(parent.dataDir, "projects") |
| 30 | os.path.join(parent.dataDir, 'projects')) | 30 | ) |
| 31 | 31 | ||
| 32 | self.findPresets() | 32 | self.findPresets() |
| 33 | 33 | ||
| 34 | # window | 34 | # window |
| 35 | self.lastFilter = '*' | 35 | self.lastFilter = "*" |
| 36 | self.presetRows = [] # list of (comp, vers, name) tuples | 36 | self.presetRows = [] # list of (comp, vers, name) tuples |
| 37 | self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) | 37 | |
| 38 | # FIXME | ||
| 39 | # self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) | ||
| 38 | 40 | ||
| 39 | # connect button signals | 41 | # connect button signals |
| 40 | self.pushButton_delete.clicked.connect( | 42 | self.pushButton_delete.clicked.connect(self.openDeletePresetDialog) |
| 41 | self.openDeletePresetDialog | 43 | self.pushButton_rename.clicked.connect(self.openRenamePresetDialog) |
| 42 | ) | 44 | self.pushButton_import.clicked.connect(self.openImportDialog) |
| 43 | self.pushButton_rename.clicked.connect( | 45 | self.pushButton_export.clicked.connect(self.openExportDialog) |
| 44 | self.openRenamePresetDialog | 46 | self.pushButton_close.clicked.connect(self.close) |
| 45 | ) | ||
| 46 | self.pushButton_import.clicked.connect( | ||
| 47 | self.openImportDialog | ||
| 48 | ) | ||
| 49 | self.pushButton_export.clicked.connect( | ||
| 50 | self.openExportDialog | ||
| 51 | ) | ||
| 52 | self.pushButton_close.clicked.connect( | ||
| 53 | self.close | ||
| 54 | ) | ||
| 55 | 47 | ||
| 56 | # create filter box and preset list | 48 | # create filter box and preset list |
| 57 | self.drawFilterList() | 49 | self.drawFilterList() |
| 58 | self.comboBox_filter.currentIndexChanged.connect( | 50 | self.comboBox_filter.currentIndexChanged.connect( |
| 59 | lambda: self.drawPresetList( | 51 | lambda: self.drawPresetList( |
| 60 | self.comboBox_filter.currentText(), | 52 | self.comboBox_filter.currentText(), self.lineEdit_search.text() |
| 61 | self.lineEdit_search.text() | ||
| 62 | ) | 53 | ) |
| 63 | ) | 54 | ) |
| 64 | 55 | ||
| @@ -69,17 +60,16 @@ class PresetManager(QtWidgets.QDialog): | |||
| 69 | self.lineEdit_search.setCompleter(completer) | 60 | self.lineEdit_search.setCompleter(completer) |
| 70 | self.lineEdit_search.textChanged.connect( | 61 | self.lineEdit_search.textChanged.connect( |
| 71 | lambda: self.drawPresetList( | 62 | lambda: self.drawPresetList( |
| 72 | self.comboBox_filter.currentText(), | 63 | self.comboBox_filter.currentText(), self.lineEdit_search.text() |
| 73 | self.lineEdit_search.text() | ||
| 74 | ) | 64 | ) |
| 75 | ) | 65 | ) |
| 76 | self.drawPresetList('*') | 66 | self.drawPresetList("*") |
| 77 | 67 | ||
| 78 | def show_(self): | 68 | def show_(self): |
| 79 | '''Open a new preset manager window from the mainwindow''' | 69 | """Open a new preset manager window from the mainwindow""" |
| 80 | self.findPresets() | 70 | self.findPresets() |
| 81 | self.drawFilterList() | 71 | self.drawFilterList() |
| 82 | self.drawPresetList('*') | 72 | self.drawPresetList("*") |
| 83 | self.show() | 73 | self.show() |
| 84 | 74 | ||
| 85 | def findPresets(self): | 75 | def findPresets(self): |
| @@ -100,14 +90,12 @@ class PresetManager(QtWidgets.QDialog): | |||
| 100 | continue | 90 | continue |
| 101 | self.presets = { | 91 | self.presets = { |
| 102 | compName: [ | 92 | compName: [ |
| 103 | (vers, preset) | 93 | (vers, preset) for name, vers, preset in parseList if name == compName |
| 104 | for name, vers, preset in parseList | ||
| 105 | if name == compName | ||
| 106 | ] | 94 | ] |
| 107 | for compName, _, __ in parseList | 95 | for compName, _, __ in parseList |
| 108 | } | 96 | } |
| 109 | 97 | ||
| 110 | def drawPresetList(self, compFilter=None, presetFilter=''): | 98 | def drawPresetList(self, compFilter=None, presetFilter=""): |
| 111 | self.listWidget_presets.clear() | 99 | self.listWidget_presets.clear() |
| 112 | if compFilter: | 100 | if compFilter: |
| 113 | self.lastFilter = str(compFilter) | 101 | self.lastFilter = str(compFilter) |
| @@ -116,13 +104,11 @@ class PresetManager(QtWidgets.QDialog): | |||
| 116 | self.presetRows = [] | 104 | self.presetRows = [] |
| 117 | presetNames = [] | 105 | presetNames = [] |
| 118 | for component, presets in self.presets.items(): | 106 | for component, presets in self.presets.items(): |
| 119 | if compFilter != '*' and component != compFilter: | 107 | if compFilter != "*" and component != compFilter: |
| 120 | continue | 108 | continue |
| 121 | for vers, preset in presets: | 109 | for vers, preset in presets: |
| 122 | if not presetFilter or presetFilter in preset: | 110 | if not presetFilter or presetFilter in preset: |
| 123 | self.listWidget_presets.addItem( | 111 | self.listWidget_presets.addItem("%s: %s" % (component, preset)) |
| 124 | '%s: %s' % (component, preset) | ||
| 125 | ) | ||
| 126 | self.presetRows.append((component, vers, preset)) | 112 | self.presetRows.append((component, vers, preset)) |
| 127 | if preset not in presetNames: | 113 | if preset not in presetNames: |
| 128 | presetNames.append(preset) | 114 | presetNames.append(preset) |
| @@ -130,18 +116,18 @@ class PresetManager(QtWidgets.QDialog): | |||
| 130 | 116 | ||
| 131 | def drawFilterList(self): | 117 | def drawFilterList(self): |
| 132 | self.comboBox_filter.clear() | 118 | self.comboBox_filter.clear() |
| 133 | self.comboBox_filter.addItem('*') | 119 | self.comboBox_filter.addItem("*") |
| 134 | for component in self.presets: | 120 | for component in self.presets: |
| 135 | self.comboBox_filter.addItem(component) | 121 | self.comboBox_filter.addItem(component) |
| 136 | 122 | ||
| 137 | def clearPreset(self, compI=None): | 123 | def clearPreset(self, compI=None): |
| 138 | '''Functions on mainwindow level from the context menu''' | 124 | """Functions on mainwindow level from the context menu""" |
| 139 | compI = self.parent.listWidget_componentList.currentRow() | 125 | compI = self.parent.listWidget_componentList.currentRow() |
| 140 | action = ClearPreset(self.parent, compI) | 126 | action = ClearPreset(self.parent, compI) |
| 141 | self.parent.undoStack.push(action) | 127 | self.parent.undoStack.push(action) |
| 142 | 128 | ||
| 143 | def openSavePresetDialog(self): | 129 | def openSavePresetDialog(self): |
| 144 | '''Functions on mainwindow level from the context menu''' | 130 | """Functions on mainwindow level from the context menu""" |
| 145 | selectedComponents = self.core.selectedComponents | 131 | selectedComponents = self.core.selectedComponents |
| 146 | componentList = self.parent.listWidget_componentList | 132 | componentList = self.parent.listWidget_componentList |
| 147 | 133 | ||
| @@ -152,10 +138,10 @@ class PresetManager(QtWidgets.QDialog): | |||
| 152 | currentPreset = selectedComponents[index].currentPreset | 138 | currentPreset = selectedComponents[index].currentPreset |
| 153 | newName, OK = QtWidgets.QInputDialog.getText( | 139 | newName, OK = QtWidgets.QInputDialog.getText( |
| 154 | self.parent, | 140 | self.parent, |
| 155 | 'Audio Visualizer', | 141 | "Audio Visualizer", |
| 156 | 'New Preset Name:', | 142 | "New Preset Name:", |
| 157 | QtWidgets.QLineEdit.Normal, | 143 | QtWidgets.QLineEdit.EchoMode.Normal, |
| 158 | currentPreset | 144 | currentPreset, |
| 159 | ) | 145 | ) |
| 160 | if OK: | 146 | if OK: |
| 161 | if badName(newName): | 147 | if badName(newName): |
| @@ -164,21 +150,23 @@ class PresetManager(QtWidgets.QDialog): | |||
| 164 | if newName: | 150 | if newName: |
| 165 | if index != -1: | 151 | if index != -1: |
| 166 | selectedComponents[index].currentPreset = newName | 152 | selectedComponents[index].currentPreset = newName |
| 167 | saveValueStore = \ | 153 | saveValueStore = selectedComponents[index].savePreset() |
| 168 | selectedComponents[index].savePreset() | 154 | saveValueStore["preset"] = newName |
| 169 | saveValueStore['preset'] = newName | ||
| 170 | componentName = str(selectedComponents[index]).strip() | 155 | componentName = str(selectedComponents[index]).strip() |
| 171 | vers = selectedComponents[index].version | 156 | vers = selectedComponents[index].version |
| 172 | self.createNewPreset( | 157 | self.createNewPreset( |
| 173 | componentName, vers, newName, | 158 | componentName, |
| 174 | saveValueStore, window=self.parent) | 159 | vers, |
| 160 | newName, | ||
| 161 | saveValueStore, | ||
| 162 | window=self.parent, | ||
| 163 | ) | ||
| 175 | self.findPresets() | 164 | self.findPresets() |
| 176 | self.drawPresetList() | 165 | self.drawPresetList() |
| 177 | self.openPreset(newName, index) | 166 | self.openPreset(newName, index) |
| 178 | break | 167 | break |
| 179 | 168 | ||
| 180 | def createNewPreset( | 169 | def createNewPreset(self, compName, vers, filename, saveValueStore, **kwargs): |
| 181 | self, compName, vers, filename, saveValueStore, **kwargs): | ||
| 182 | path = os.path.join(self.presetDir, compName, str(vers), filename) | 170 | path = os.path.join(self.presetDir, compName, str(vers), filename) |
| 183 | if self.presetExists(path, **kwargs): | 171 | if self.presetExists(path, **kwargs): |
| 184 | return | 172 | return |
| @@ -188,11 +176,11 @@ class PresetManager(QtWidgets.QDialog): | |||
| 188 | if os.path.exists(path): | 176 | if os.path.exists(path): |
| 189 | window = kwargs.get("window", self) | 177 | window = kwargs.get("window", self) |
| 190 | ch = self.parent.showMessage( | 178 | ch = self.parent.showMessage( |
| 191 | msg="%s already exists! Overwrite it?" % | 179 | msg="%s already exists! Overwrite it?" % os.path.basename(path), |
| 192 | os.path.basename(path), | ||
| 193 | showCancel=True, | 180 | showCancel=True, |
| 194 | icon='Warning', | 181 | icon="Warning", |
| 195 | parent=window) | 182 | parent=window, |
| 183 | ) | ||
| 196 | if not ch: | 184 | if not ch: |
| 197 | # user clicked cancel | 185 | # user clicked cancel |
| 198 | return True | 186 | return True |
| @@ -225,10 +213,10 @@ class PresetManager(QtWidgets.QDialog): | |||
| 225 | return | 213 | return |
| 226 | comp, vers, name = self.presetRows[row] | 214 | comp, vers, name = self.presetRows[row] |
| 227 | ch = self.parent.showMessage( | 215 | ch = self.parent.showMessage( |
| 228 | msg='Really delete %s?' % name, | 216 | msg="Really delete %s?" % name, |
| 229 | showCancel=True, | 217 | showCancel=True, |
| 230 | icon='Warning', | 218 | icon="Warning", |
| 231 | parent=self | 219 | parent=self, |
| 232 | ) | 220 | ) |
| 233 | if not ch: | 221 | if not ch: |
| 234 | return | 222 | return |
| @@ -240,9 +228,9 @@ class PresetManager(QtWidgets.QDialog): | |||
| 240 | 228 | ||
| 241 | def warnMessage(self, window=None): | 229 | def warnMessage(self, window=None): |
| 242 | self.parent.showMessage( | 230 | self.parent.showMessage( |
| 243 | msg='Preset names must contain only letters, ' | 231 | msg="Preset names must contain only letters, " "numbers, and spaces.", |
| 244 | 'numbers, and spaces.', | 232 | parent=window if window else self, |
| 245 | parent=window if window else self) | 233 | ) |
| 246 | 234 | ||
| 247 | def getPresetRow(self): | 235 | def getPresetRow(self): |
| 248 | row = self.listWidget_presets.currentRow() | 236 | row = self.listWidget_presets.currentRow() |
| @@ -262,14 +250,14 @@ class PresetManager(QtWidgets.QDialog): | |||
| 262 | rowTuple = ( | 250 | rowTuple = ( |
| 263 | self.core.selectedComponents[compIndex].name, | 251 | self.core.selectedComponents[compIndex].name, |
| 264 | self.core.selectedComponents[compIndex].version, | 252 | self.core.selectedComponents[compIndex].version, |
| 265 | preset | 253 | preset, |
| 266 | ) | 254 | ) |
| 267 | for i, tup in enumerate(self.presetRows): | 255 | for i, tup in enumerate(self.presetRows): |
| 268 | if rowTuple == tup: | 256 | if rowTuple == tup: |
| 269 | index = i | 257 | index = i |
| 270 | break | 258 | break |
| 271 | else: | 259 | else: |
| 272 | return -1 | 260 | return -1 |
| 273 | return index | 261 | return index |
| 274 | 262 | ||
| 275 | def openRenamePresetDialog(self): | 263 | def openRenamePresetDialog(self): |
| @@ -281,10 +269,10 @@ class PresetManager(QtWidgets.QDialog): | |||
| 281 | while True: | 269 | while True: |
| 282 | newName, OK = QtWidgets.QInputDialog.getText( | 270 | newName, OK = QtWidgets.QInputDialog.getText( |
| 283 | self, | 271 | self, |
| 284 | 'Preset Manager', | 272 | "Preset Manager", |
| 285 | 'Rename Preset:', | 273 | "Rename Preset:", |
| 286 | QtWidgets.QLineEdit.Normal, | 274 | QtWidgets.QLineEdit.EchoMode.Normal, |
| 287 | self.presetRows[index][2] | 275 | self.presetRows[index][2], |
| 288 | ) | 276 | ) |
| 289 | if OK: | 277 | if OK: |
| 290 | if badName(newName): | 278 | if badName(newName): |
| @@ -292,8 +280,7 @@ class PresetManager(QtWidgets.QDialog): | |||
| 292 | continue | 280 | continue |
| 293 | if newName: | 281 | if newName: |
| 294 | comp, vers, oldName = self.presetRows[index] | 282 | comp, vers, oldName = self.presetRows[index] |
| 295 | path = os.path.join( | 283 | path = os.path.join(self.presetDir, comp, str(vers)) |
| 296 | self.presetDir, comp, str(vers)) | ||
| 297 | newPath = os.path.join(path, newName) | 284 | newPath = os.path.join(path, newName) |
| 298 | if self.presetExists(newPath): | 285 | if self.presetExists(newPath): |
| 299 | return | 286 | return |
| @@ -311,20 +298,21 @@ class PresetManager(QtWidgets.QDialog): | |||
| 311 | self.drawPresetList() | 298 | self.drawPresetList() |
| 312 | path = os.path.dirname(newPath) | 299 | path = os.path.dirname(newPath) |
| 313 | for i, comp in enumerate(self.core.selectedComponents): | 300 | for i, comp in enumerate(self.core.selectedComponents): |
| 314 | if self.core.getPresetDir(comp) == path \ | 301 | if self.core.getPresetDir(comp) == path and comp.currentPreset == oldName: |
| 315 | and comp.currentPreset == oldName: | ||
| 316 | self.core.openPreset(newPath, i, newName) | 302 | self.core.openPreset(newPath, i, newName) |
| 317 | self.parent.updateComponentTitle(i, False) | 303 | self.parent.updateComponentTitle(i, False) |
| 318 | self.parent.drawPreview() | 304 | self.parent.drawPreview() |
| 319 | 305 | ||
| 320 | def openImportDialog(self): | 306 | def openImportDialog(self): |
| 321 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( | 307 | filename, _ = QtWidgets.QFileDialog.getOpenFileName( |
| 322 | self, "Import Preset File", | 308 | self, |
| 309 | "Import Preset File", | ||
| 323 | self.settings.value("presetDir"), | 310 | self.settings.value("presetDir"), |
| 324 | "Preset Files (*.avl)") | 311 | "Preset Files (*.avl)", |
| 312 | ) | ||
| 325 | if filename: | 313 | if filename: |
| 326 | # get installed path & ask user to overwrite if needed | 314 | # get installed path & ask user to overwrite if needed |
| 327 | path = '' | 315 | path = "" |
| 328 | while True: | 316 | while True: |
| 329 | if path: | 317 | if path: |
| 330 | if self.presetExists(path): | 318 | if self.presetExists(path): |
| @@ -345,15 +333,16 @@ class PresetManager(QtWidgets.QDialog): | |||
| 345 | if index == -1: | 333 | if index == -1: |
| 346 | return | 334 | return |
| 347 | filename, _ = QtWidgets.QFileDialog.getSaveFileName( | 335 | filename, _ = QtWidgets.QFileDialog.getSaveFileName( |
| 348 | self, "Export Preset", | 336 | self, |
| 337 | "Export Preset", | ||
| 349 | self.settings.value("presetDir"), | 338 | self.settings.value("presetDir"), |
| 350 | "Preset Files (*.avl)") | 339 | "Preset Files (*.avl)", |
| 340 | ) | ||
| 351 | if filename: | 341 | if filename: |
| 352 | comp, vers, name = self.presetRows[index] | 342 | comp, vers, name = self.presetRows[index] |
| 353 | if not self.core.exportPreset(filename, comp, vers, name): | 343 | if not self.core.exportPreset(filename, comp, vers, name): |
| 354 | self.parent.showMessage( | 344 | self.parent.showMessage( |
| 355 | msg='Couldn\'t export %s.' % filename, | 345 | msg="Couldn't export %s." % filename, parent=self |
| 356 | parent=self | ||
| 357 | ) | 346 | ) |
| 358 | self.settings.setValue("presetDir", os.path.dirname(filename)) | 347 | self.settings.setValue("presetDir", os.path.dirname(filename)) |
| 359 | 348 | ||
diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index 3943a5c..1d78516 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py | |||
| @@ -1,9 +1,10 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Thread that runs to create QImages for MainWindow's preview label. | 2 | Thread that runs to create QImages for MainWindow's preview label. |
| 3 | Processes a queue of component lists. | 3 | Processes a queue of component lists. |
| 4 | ''' | 4 | """ |
| 5 | from PyQt5 import QtCore, QtGui, uic | 5 | |
| 6 | from PyQt5.QtCore import pyqtSignal, pyqtSlot | 6 | from PyQt6 import QtCore, QtGui, uic |
| 7 | from PyQt6.QtCore import pyqtSignal, pyqtSlot | ||
| 7 | from PIL import Image | 8 | from PIL import Image |
| 8 | from PIL.ImageQt import ImageQt | 9 | from PIL.ImageQt import ImageQt |
| 9 | from queue import Queue, Empty | 10 | from queue import Queue, Empty |
| @@ -26,8 +27,8 @@ class Worker(QtCore.QObject): | |||
| 26 | super().__init__() | 27 | super().__init__() |
| 27 | self.core = core | 28 | self.core = core |
| 28 | self.settings = settings | 29 | self.settings = settings |
| 29 | width = int(self.settings.value('outputWidth')) | 30 | width = int(self.settings.value("outputWidth")) |
| 30 | height = int(self.settings.value('outputHeight')) | 31 | height = int(self.settings.value("outputHeight")) |
| 31 | self.queue = queue | 32 | self.queue = queue |
| 32 | self.background = Checkerboard(width, height) | 33 | self.background = Checkerboard(width, height) |
| 33 | 34 | ||
| @@ -35,10 +36,10 @@ class Worker(QtCore.QObject): | |||
| 35 | @pyqtSlot(list) | 36 | @pyqtSlot(list) |
| 36 | def createPreviewImage(self, components): | 37 | def createPreviewImage(self, components): |
| 37 | dic = { | 38 | dic = { |
| 38 | "components": components, | 39 | "components": components, |
| 39 | } | 40 | } |
| 40 | self.queue.put(dic) | 41 | self.queue.put(dic) |
| 41 | log.debug('Preview thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) | 42 | log.debug("Preview thread id: {}".format(int(QtCore.QThread.currentThreadId()))) |
| 42 | 43 | ||
| 43 | @pyqtSlot() | 44 | @pyqtSlot() |
| 44 | def process(self): | 45 | def process(self): |
| @@ -49,31 +50,34 @@ class Worker(QtCore.QObject): | |||
| 49 | self.queue.get(block=False) | 50 | self.queue.get(block=False) |
| 50 | except Empty: | 51 | except Empty: |
| 51 | continue | 52 | continue |
| 52 | width = int(self.settings.value('outputWidth')) | 53 | width = int(self.settings.value("outputWidth")) |
| 53 | height = int(self.settings.value('outputHeight')) | 54 | height = int(self.settings.value("outputHeight")) |
| 54 | if self.background.width != width \ | 55 | if self.background.width != width or self.background.height != height: |
| 55 | or self.background.height != height: | ||
| 56 | self.background = Checkerboard(width, height) | 56 | self.background = Checkerboard(width, height) |
| 57 | 57 | ||
| 58 | frame = self.background.copy() | 58 | frame = self.background.copy() |
| 59 | log.info('Creating new preview frame') | 59 | log.info("Creating new preview frame") |
| 60 | components = nextPreviewInformation["components"] | 60 | components = nextPreviewInformation["components"] |
| 61 | for component in reversed(components): | 61 | for component in reversed(components): |
| 62 | try: | 62 | try: |
| 63 | component.lockSize(width, height) | 63 | component.lockSize(width, height) |
| 64 | newFrame = component.previewRender() | 64 | newFrame = component.previewRender() |
| 65 | component.unlockSize() | 65 | component.unlockSize() |
| 66 | frame = Image.alpha_composite( | 66 | frame = Image.alpha_composite(frame, newFrame) |
| 67 | frame, newFrame | ||
| 68 | ) | ||
| 69 | 67 | ||
| 70 | except ValueError as e: | 68 | except ValueError as e: |
| 71 | errMsg = "Bad frame returned by %s's preview renderer. " \ | 69 | errMsg = ( |
| 72 | "%s. New frame size was %s*%s; should be %s*%s." % ( | 70 | "Bad frame returned by %s's preview renderer. " |
| 73 | str(component), str(e).capitalize(), | 71 | "%s. New frame size was %s*%s; should be %s*%s." |
| 74 | newFrame.width, newFrame.height, | 72 | % ( |
| 75 | width, height | 73 | str(component), |
| 74 | str(e).capitalize(), | ||
| 75 | newFrame.width, | ||
| 76 | newFrame.height, | ||
| 77 | width, | ||
| 78 | height, | ||
| 76 | ) | 79 | ) |
| 80 | ) | ||
| 77 | log.critical(errMsg) | 81 | log.critical(errMsg) |
| 78 | self.error.emit(errMsg) | 82 | self.error.emit(errMsg) |
| 79 | break | 83 | break |
diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index d910456..f52f8a3 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py | |||
| @@ -1,18 +1,20 @@ | |||
| 1 | from PyQt5 import QtCore, QtGui, QtWidgets | 1 | from PyQt6 import QtCore, QtGui, QtWidgets |
| 2 | import logging | 2 | import logging |
| 3 | 3 | ||
| 4 | log = logging.getLogger('AVP.Gui.PreviewWindow') | 4 | log = logging.getLogger("AVP.Gui.PreviewWindow") |
| 5 | 5 | ||
| 6 | 6 | ||
| 7 | class PreviewWindow(QtWidgets.QLabel): | 7 | class PreviewWindow(QtWidgets.QLabel): |
| 8 | ''' | 8 | """ |
| 9 | Paints the preview QLabel in MainWindow and maintains the aspect ratio | 9 | Paints the preview QLabel in MainWindow and maintains the aspect ratio |
| 10 | when the window is resized. | 10 | when the window is resized. |
| 11 | ''' | 11 | """ |
| 12 | |||
| 12 | def __init__(self, parent, img): | 13 | def __init__(self, parent, img): |
| 13 | super().__init__() | 14 | super().__init__() |
| 14 | self.parent = parent | 15 | self.parent = parent |
| 15 | self.setFrameStyle(QtWidgets.QFrame.StyledPanel) | 16 | # FIXME |
| 17 | # self.setFrameStyle(QtWidgets.QFrame.StyledPanel) | ||
| 16 | self.pixmap = QtGui.QPixmap(img) | 18 | self.pixmap = QtGui.QPixmap(img) |
| 17 | 19 | ||
| 18 | def paintEvent(self, event): | 20 | def paintEvent(self, event): |
| @@ -21,12 +23,13 @@ class PreviewWindow(QtWidgets.QLabel): | |||
| 21 | point = QtCore.QPoint(0, 0) | 23 | point = QtCore.QPoint(0, 0) |
| 22 | scaledPix = self.pixmap.scaled( | 24 | scaledPix = self.pixmap.scaled( |
| 23 | size, | 25 | size, |
| 24 | QtCore.Qt.KeepAspectRatio, | 26 | QtCore.Qt.AspectRatioMode.KeepAspectRatio, |
| 25 | transformMode=QtCore.Qt.SmoothTransformation) | 27 | transformMode=QtCore.Qt.TransformationMode.SmoothTransformation, |
| 28 | ) | ||
| 26 | 29 | ||
| 27 | # start painting the label from left upper corner | 30 | # start painting the label from left upper corner |
| 28 | point.setX(int((size.width() - scaledPix.width())/2)) | 31 | point.setX(int((size.width() - scaledPix.width()) / 2)) |
| 29 | point.setY(int((size.height() - scaledPix.height())/2)) | 32 | point.setY(int((size.height() - scaledPix.height()) / 2)) |
| 30 | painter.drawPixmap(point, scaledPix) | 33 | painter.drawPixmap(point, scaledPix) |
| 31 | 34 | ||
| 32 | def changePixmap(self, img): | 35 | def changePixmap(self, img): |
| @@ -40,22 +43,16 @@ class PreviewWindow(QtWidgets.QLabel): | |||
| 40 | i = self.parent.listWidget_componentList.currentRow() | 43 | i = self.parent.listWidget_componentList.currentRow() |
| 41 | if i >= 0: | 44 | if i >= 0: |
| 42 | component = self.parent.core.selectedComponents[i] | 45 | component = self.parent.core.selectedComponents[i] |
| 43 | if not hasattr(component, 'previewClickEvent'): | 46 | if not hasattr(component, "previewClickEvent"): |
| 44 | return | 47 | return |
| 45 | pos = (event.x(), event.y()) | 48 | qpoint = event.position().toPoint() |
| 49 | pos = (qpoint.x(), qpoint.y()) | ||
| 46 | size = (self.width(), self.height()) | 50 | size = (self.width(), self.height()) |
| 47 | butt = event.button() | 51 | butt = event.button() |
| 48 | log.info('Click event for #%s: %s button %s' % ( | 52 | log.info("Click event for #%s: %s button %s" % (i, pos, butt)) |
| 49 | i, pos, butt)) | 53 | component.previewClickEvent(pos, size, butt) |
| 50 | component.previewClickEvent( | ||
| 51 | pos, size, butt | ||
| 52 | ) | ||
| 53 | 54 | ||
| 54 | @QtCore.pyqtSlot(str) | 55 | @QtCore.pyqtSlot(str) |
| 55 | def threadError(self, msg): | 56 | def threadError(self, msg): |
| 56 | self.parent.showMessage( | 57 | self.parent.showMessage(msg=msg, icon="Critical", parent=self) |
| 57 | msg=msg, | 58 | log.info("%", repr(self.parent)) |
| 58 | icon='Critical', | ||
| 59 | parent=self | ||
| 60 | ) | ||
| 61 | log.info('%', repr(self.parent)) | ||
diff --git a/src/tests/__init__.py b/src/tests/__init__.py index 345bd96..e2d83e7 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py | |||
| @@ -5,20 +5,22 @@ from ..core import Core | |||
| 5 | 5 | ||
| 6 | 6 | ||
| 7 | def getTestDataPath(filename): | 7 | def getTestDataPath(filename): |
| 8 | return os.path.join(Core.wd, 'tests', 'data', filename) | 8 | return os.path.join(Core.wd, "tests", "data", filename) |
| 9 | 9 | ||
| 10 | 10 | ||
| 11 | def run(logFile): | 11 | def run(logFile): |
| 12 | """Run Pytest, which then imports and runs all tests in this module.""" | 12 | """Run Pytest, which then imports and runs all tests in this module.""" |
| 13 | os.environ["PYTEST_QT_API"] = "pyqt5" | 13 | os.environ["PYTEST_QT_API"] = "PyQt6" |
| 14 | with open(logFile, "w") as f: | 14 | with open(logFile, "w") as f: |
| 15 | # temporarily redirect stdout to a text file so we capture pytest's output | 15 | # temporarily redirect stdout to a text file so we capture pytest's output |
| 16 | sys.stdout = f | 16 | sys.stdout = f |
| 17 | try: | 17 | try: |
| 18 | val = pytest.main([ | 18 | val = pytest.main( |
| 19 | os.path.dirname(__file__), | 19 | [ |
| 20 | "-s", # disable pytest's internal capturing of stdout etc. | 20 | os.path.dirname(__file__), |
| 21 | ]) | 21 | "-s", # disable pytest's internal capturing of stdout etc. |
| 22 | ] | ||
| 23 | ) | ||
| 22 | finally: | 24 | finally: |
| 23 | sys.stdout = sys.__stdout__ | 25 | sys.stdout = sys.__stdout__ |
| 24 | 26 | ||
diff --git a/src/tests/test_commandline_export.py b/src/tests/test_commandline_export.py index 7f3530f..6126da7 100644 --- a/src/tests/test_commandline_export.py +++ b/src/tests/test_commandline_export.py | |||
| @@ -7,11 +7,20 @@ from pytestqt import qtbot | |||
| 7 | 7 | ||
| 8 | 8 | ||
| 9 | def test_commandline_classic_export(qtbot): | 9 | def test_commandline_classic_export(qtbot): |
| 10 | '''Run Qt event loop and create a video in the system /tmp or /temp''' | 10 | """Run Qt event loop and create a video in the system /tmp or /temp""" |
| 11 | soundFile = getTestDataPath("test.ogg") | 11 | soundFile = getTestDataPath("test.ogg") |
| 12 | outputDir = tempfile.mkdtemp(prefix="avp-test-") | 12 | outputDir = tempfile.mkdtemp(prefix="avp-test-") |
| 13 | outputFilename = os.path.join(outputDir, "output.mp4") | 13 | outputFilename = os.path.join(outputDir, "output.mp4") |
| 14 | sys.argv = ['', '-c', '0', 'classic', '-i', soundFile, '-o', outputFilename] | 14 | sys.argv = [ |
| 15 | "", | ||
| 16 | "-c", | ||
| 17 | "0", | ||
| 18 | "classic", | ||
| 19 | "-i", | ||
| 20 | soundFile, | ||
| 21 | "-o", | ||
| 22 | outputFilename, | ||
| 23 | ] | ||
| 15 | 24 | ||
| 16 | command = Command() | 25 | command = Command() |
| 17 | command.quit = lambda _: None | 26 | command.quit = lambda _: None |
| @@ -19,10 +28,10 @@ def test_commandline_classic_export(qtbot): | |||
| 19 | # Command object now has a video_thread Worker which is exporting the video | 28 | # Command object now has a video_thread Worker which is exporting the video |
| 20 | 29 | ||
| 21 | with qtbot.waitSignal(command.worker.videoCreated, timeout=10000): | 30 | with qtbot.waitSignal(command.worker.videoCreated, timeout=10000): |
| 22 | ''' | 31 | """ |
| 23 | Wait until videoCreated is emitted by the video_thread Worker | 32 | Wait until videoCreated is emitted by the video_thread Worker |
| 24 | or until 10 second timeout has passed | 33 | or until 10 second timeout has passed |
| 25 | ''' | 34 | """ |
| 26 | print(f"Test Video created at {outputFilename}") | 35 | print(f"Test Video created at {outputFilename}") |
| 27 | 36 | ||
| 28 | assert os.path.exists(outputFilename) | 37 | assert os.path.exists(outputFilename) |
diff --git a/src/tests/test_commandline_parser.py b/src/tests/test_commandline_parser.py index 0813e00..5d1232b 100644 --- a/src/tests/test_commandline_parser.py +++ b/src/tests/test_commandline_parser.py | |||
| @@ -5,28 +5,28 @@ from ..command import Command | |||
| 5 | 5 | ||
| 6 | def test_commandline_help(): | 6 | def test_commandline_help(): |
| 7 | command = Command() | 7 | command = Command() |
| 8 | sys.argv = ['', '--help'] | 8 | sys.argv = ["", "--help"] |
| 9 | with pytest.raises(SystemExit): | 9 | with pytest.raises(SystemExit): |
| 10 | command.parseArgs() | 10 | command.parseArgs() |
| 11 | 11 | ||
| 12 | 12 | ||
| 13 | def test_commandline_help_if_bad_args(): | 13 | def test_commandline_help_if_bad_args(): |
| 14 | command = Command() | 14 | command = Command() |
| 15 | sys.argv = ['', '--junk'] | 15 | sys.argv = ["", "--junk"] |
| 16 | with pytest.raises(SystemExit): | 16 | with pytest.raises(SystemExit): |
| 17 | command.parseArgs() | 17 | command.parseArgs() |
| 18 | 18 | ||
| 19 | 19 | ||
| 20 | def test_commandline_launches_gui_if_debug(): | 20 | def test_commandline_launches_gui_if_debug(): |
| 21 | command = Command() | 21 | command = Command() |
| 22 | sys.argv = ['', '--debug'] | 22 | sys.argv = ["", "--debug"] |
| 23 | mode = command.parseArgs() | 23 | mode = command.parseArgs() |
| 24 | assert mode == "GUI" | 24 | assert mode == "GUI" |
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | def test_commandline_launches_gui_if_debug_with_project(): | 27 | def test_commandline_launches_gui_if_debug_with_project(): |
| 28 | command = Command() | 28 | command = Command() |
| 29 | sys.argv = ['', 'test', '--debug'] | 29 | sys.argv = ["", "test", "--debug"] |
| 30 | mode = command.parseArgs() | 30 | mode = command.parseArgs() |
| 31 | assert mode == "GUI" | 31 | assert mode == "GUI" |
| 32 | 32 | ||
| @@ -34,11 +34,12 @@ def test_commandline_launches_gui_if_debug_with_project(): | |||
| 34 | def test_commandline_tries_to_export(): | 34 | def test_commandline_tries_to_export(): |
| 35 | command = Command() | 35 | command = Command() |
| 36 | didCallFunction = False | 36 | didCallFunction = False |
| 37 | |||
| 37 | def captureFunction(*args): | 38 | def captureFunction(*args): |
| 38 | nonlocal didCallFunction | 39 | nonlocal didCallFunction |
| 39 | didCallFunction = True | 40 | didCallFunction = True |
| 40 | 41 | ||
| 41 | sys.argv = ['', '-c', '0', 'classic', '-i', '_', '-o', '_'] | 42 | sys.argv = ["", "-c", "0", "classic", "-i", "_", "-o", "_"] |
| 42 | command.createAudioVisualization = captureFunction | 43 | command.createAudioVisualization = captureFunction |
| 43 | command.parseArgs() | 44 | command.parseArgs() |
| 44 | assert didCallFunction | 45 | assert didCallFunction |
diff --git a/src/tests/test_core_init.py b/src/tests/test_core_init.py index 438f7fe..950dc13 100644 --- a/src/tests/test_core_init.py +++ b/src/tests/test_core_init.py | |||
| @@ -4,15 +4,15 @@ from ..core import Core | |||
| 4 | def test_component_names(): | 4 | def test_component_names(): |
| 5 | core = Core() | 5 | core = Core() |
| 6 | assert core.compNames == [ | 6 | assert core.compNames == [ |
| 7 | 'Classic Visualizer', | 7 | "Classic Visualizer", |
| 8 | 'Color', | 8 | "Color", |
| 9 | "Conway's Game of Life", | 9 | "Conway's Game of Life", |
| 10 | 'Image', | 10 | "Image", |
| 11 | 'Sound', | 11 | "Sound", |
| 12 | 'Spectrum', | 12 | "Spectrum", |
| 13 | 'Title Text', | 13 | "Title Text", |
| 14 | 'Video', | 14 | "Video", |
| 15 | 'Waveform', | 15 | "Waveform", |
| 16 | ] | 16 | ] |
| 17 | 17 | ||
| 18 | 18 | ||
diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 2e800eb..e35aba2 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py | |||
| @@ -1,7 +1,8 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Common functions | 2 | Common functions |
| 3 | ''' | 3 | """ |
| 4 | from PyQt5 import QtWidgets | 4 | |
| 5 | from PyQt6 import QtWidgets | ||
| 5 | import string | 6 | import string |
| 6 | import os | 7 | import os |
| 7 | import sys | 8 | import sys |
| @@ -11,43 +12,38 @@ from copy import copy | |||
| 11 | from collections import OrderedDict | 12 | from collections import OrderedDict |
| 12 | 13 | ||
| 13 | 14 | ||
| 14 | log = logging.getLogger('AVP.Toolkit.Common') | 15 | log = logging.getLogger("AVP.Toolkit.Common") |
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | class blockSignals: | 18 | class blockSignals: |
| 18 | ''' | 19 | """ |
| 19 | Context manager to temporarily block list of QtWidgets from updating, | 20 | Context manager to temporarily block list of QtWidgets from updating, |
| 20 | and guarantee restoring the previous state afterwards. | 21 | and guarantee restoring the previous state afterwards. |
| 21 | ''' | 22 | """ |
| 23 | |||
| 22 | def __init__(self, widgets): | 24 | def __init__(self, widgets): |
| 23 | if type(widgets) is dict: | 25 | if type(widgets) is dict: |
| 24 | self.widgets = concatDictVals(widgets) | 26 | self.widgets = concatDictVals(widgets) |
| 25 | else: | 27 | else: |
| 26 | self.widgets = ( | 28 | self.widgets = widgets if hasattr(widgets, "__iter__") else [widgets] |
| 27 | widgets if hasattr(widgets, '__iter__') | ||
| 28 | else [widgets] | ||
| 29 | ) | ||
| 30 | 29 | ||
| 31 | def __enter__(self): | 30 | def __enter__(self): |
| 32 | log.verbose( | 31 | log.verbose( |
| 33 | 'Blocking signals for %s', | 32 | "Blocking signals for %s", |
| 34 | ", ".join([ | 33 | ", ".join([str(w.__class__.__name__) for w in self.widgets]), |
| 35 | str(w.__class__.__name__) for w in self.widgets | ||
| 36 | ]) | ||
| 37 | ) | 34 | ) |
| 38 | self.oldStates = [w.signalsBlocked() for w in self.widgets] | 35 | self.oldStates = [w.signalsBlocked() for w in self.widgets] |
| 39 | for w in self.widgets: | 36 | for w in self.widgets: |
| 40 | w.blockSignals(True) | 37 | w.blockSignals(True) |
| 41 | 38 | ||
| 42 | def __exit__(self, *args): | 39 | def __exit__(self, *args): |
| 43 | log.verbose( | 40 | log.verbose("Resetting blockSignals to %s", str(bool(sum(self.oldStates)))) |
| 44 | 'Resetting blockSignals to %s', str(bool(sum(self.oldStates)))) | ||
| 45 | for w, state in zip(self.widgets, self.oldStates): | 41 | for w, state in zip(self.widgets, self.oldStates): |
| 46 | w.blockSignals(state) | 42 | w.blockSignals(state) |
| 47 | 43 | ||
| 48 | 44 | ||
| 49 | def concatDictVals(d): | 45 | def concatDictVals(d): |
| 50 | '''Concatenates all values in given dict into one list.''' | 46 | """Concatenates all values in given dict into one list.""" |
| 51 | key, value = d.popitem() | 47 | key, value = d.popitem() |
| 52 | d[key] = value | 48 | d[key] = value |
| 53 | final = copy(value) | 49 | final = copy(value) |
| @@ -60,22 +56,22 @@ def concatDictVals(d): | |||
| 60 | 56 | ||
| 61 | 57 | ||
| 62 | def badName(name): | 58 | def badName(name): |
| 63 | '''Returns whether a name contains non-alphanumeric chars''' | 59 | """Returns whether a name contains non-alphanumeric chars""" |
| 64 | return any([letter in string.punctuation for letter in name]) | 60 | return any([letter in string.punctuation for letter in name]) |
| 65 | 61 | ||
| 66 | 62 | ||
| 67 | def alphabetizeDict(dictionary): | 63 | def alphabetizeDict(dictionary): |
| 68 | '''Alphabetizes a dict into OrderedDict ''' | 64 | """Alphabetizes a dict into OrderedDict""" |
| 69 | return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) | 65 | return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) |
| 70 | 66 | ||
| 71 | 67 | ||
| 72 | def presetToString(dictionary): | 68 | def presetToString(dictionary): |
| 73 | '''Returns string repr of a preset''' | 69 | """Returns string repr of a preset""" |
| 74 | return repr(alphabetizeDict(dictionary)) | 70 | return repr(alphabetizeDict(dictionary)) |
| 75 | 71 | ||
| 76 | 72 | ||
| 77 | def presetFromString(string): | 73 | def presetFromString(string): |
| 78 | '''Turns a string repr of OrderedDict into a regular dict''' | 74 | """Turns a string repr of OrderedDict into a regular dict""" |
| 79 | return dict(eval(string)) | 75 | return dict(eval(string)) |
| 80 | 76 | ||
| 81 | 77 | ||
| @@ -86,19 +82,21 @@ def appendUppercase(lst): | |||
| 86 | 82 | ||
| 87 | 83 | ||
| 88 | def pipeWrapper(func): | 84 | def pipeWrapper(func): |
| 89 | '''A decorator to insert proper kwargs into Popen objects.''' | 85 | """A decorator to insert proper kwargs into Popen objects.""" |
| 86 | |||
| 90 | def pipeWrapper(commandList, **kwargs): | 87 | def pipeWrapper(commandList, **kwargs): |
| 91 | if sys.platform == 'win32': | 88 | if sys.platform == "win32": |
| 92 | # Stop CMD window from appearing on Windows | 89 | # Stop CMD window from appearing on Windows |
| 93 | startupinfo = subprocess.STARTUPINFO() | 90 | startupinfo = subprocess.STARTUPINFO() |
| 94 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW | 91 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW |
| 95 | kwargs['startupinfo'] = startupinfo | 92 | kwargs["startupinfo"] = startupinfo |
| 96 | 93 | ||
| 97 | if 'bufsize' not in kwargs: | 94 | if "bufsize" not in kwargs: |
| 98 | kwargs['bufsize'] = 10**8 | 95 | kwargs["bufsize"] = 10**8 |
| 99 | if 'stdin' not in kwargs: | 96 | if "stdin" not in kwargs: |
| 100 | kwargs['stdin'] = subprocess.DEVNULL | 97 | kwargs["stdin"] = subprocess.DEVNULL |
| 101 | return func(commandList, **kwargs) | 98 | return func(commandList, **kwargs) |
| 99 | |||
| 102 | return pipeWrapper | 100 | return pipeWrapper |
| 103 | 101 | ||
| 104 | 102 | ||
| @@ -113,6 +111,7 @@ def disableWhenEncoding(func): | |||
| 113 | return | 111 | return |
| 114 | else: | 112 | else: |
| 115 | return func(self, *args, **kwargs) | 113 | return func(self, *args, **kwargs) |
| 114 | |||
| 116 | return decorator | 115 | return decorator |
| 117 | 116 | ||
| 118 | 117 | ||
| @@ -122,13 +121,14 @@ def disableWhenOpeningProject(func): | |||
| 122 | return | 121 | return |
| 123 | else: | 122 | else: |
| 124 | return func(self, *args, **kwargs) | 123 | return func(self, *args, **kwargs) |
| 124 | |||
| 125 | return decorator | 125 | return decorator |
| 126 | 126 | ||
| 127 | 127 | ||
| 128 | def rgbFromString(string): | 128 | def rgbFromString(string): |
| 129 | '''Turns an RGB string like "255, 255, 255" into a tuple''' | 129 | """Turns an RGB string like "255, 255, 255" into a tuple""" |
| 130 | try: | 130 | try: |
| 131 | tup = tuple([int(i) for i in string.split(',')]) | 131 | tup = tuple([int(i) for i in string.split(",")]) |
| 132 | if len(tup) != 3: | 132 | if len(tup) != 3: |
| 133 | raise ValueError | 133 | raise ValueError |
| 134 | for i in tup: | 134 | for i in tup: |
| @@ -141,42 +141,42 @@ def rgbFromString(string): | |||
| 141 | 141 | ||
| 142 | def formatTraceback(tb=None): | 142 | def formatTraceback(tb=None): |
| 143 | import traceback | 143 | import traceback |
| 144 | |||
| 144 | if tb is None: | 145 | if tb is None: |
| 145 | import sys | 146 | import sys |
| 147 | |||
| 146 | tb = sys.exc_info()[2] | 148 | tb = sys.exc_info()[2] |
| 147 | return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb)) | 149 | return "Traceback:\n%s" % "\n".join(traceback.format_tb(tb)) |
| 148 | 150 | ||
| 149 | 151 | ||
| 150 | def connectWidget(widget, func): | 152 | def connectWidget(widget, func): |
| 151 | if type(widget) == QtWidgets.QLineEdit: | 153 | if type(widget) == QtWidgets.QLineEdit: |
| 152 | widget.textChanged.connect(func) | 154 | widget.textChanged.connect(func) |
| 153 | elif type(widget) == QtWidgets.QSpinBox \ | 155 | elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox: |
| 154 | or type(widget) == QtWidgets.QDoubleSpinBox: | ||
| 155 | widget.valueChanged.connect(func) | 156 | widget.valueChanged.connect(func) |
| 156 | elif type(widget) == QtWidgets.QCheckBox: | 157 | elif type(widget) == QtWidgets.QCheckBox: |
| 157 | widget.stateChanged.connect(func) | 158 | widget.stateChanged.connect(func) |
| 158 | elif type(widget) == QtWidgets.QComboBox: | 159 | elif type(widget) == QtWidgets.QComboBox: |
| 159 | widget.currentIndexChanged.connect(func) | 160 | widget.currentIndexChanged.connect(func) |
| 160 | else: | 161 | else: |
| 161 | log.warning('Failed to connect %s ', str(widget.__class__.__name__)) | 162 | log.warning("Failed to connect %s ", str(widget.__class__.__name__)) |
| 162 | return False | 163 | return False |
| 163 | return True | 164 | return True |
| 164 | 165 | ||
| 165 | 166 | ||
| 166 | def setWidgetValue(widget, val): | 167 | def setWidgetValue(widget, val): |
| 167 | '''Generic setValue method for use with any typical QtWidget''' | 168 | """Generic setValue method for use with any typical QtWidget""" |
| 168 | log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val)) | 169 | log.verbose("Setting %s to %s" % (str(widget.__class__.__name__), val)) |
| 169 | if type(widget) == QtWidgets.QLineEdit: | 170 | if type(widget) == QtWidgets.QLineEdit: |
| 170 | widget.setText(val) | 171 | widget.setText(val) |
| 171 | elif type(widget) == QtWidgets.QSpinBox \ | 172 | elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox: |
| 172 | or type(widget) == QtWidgets.QDoubleSpinBox: | ||
| 173 | widget.setValue(val) | 173 | widget.setValue(val) |
| 174 | elif type(widget) == QtWidgets.QCheckBox: | 174 | elif type(widget) == QtWidgets.QCheckBox: |
| 175 | widget.setChecked(val) | 175 | widget.setChecked(val) |
| 176 | elif type(widget) == QtWidgets.QComboBox: | 176 | elif type(widget) == QtWidgets.QComboBox: |
| 177 | widget.setCurrentIndex(val) | 177 | widget.setCurrentIndex(val) |
| 178 | else: | 178 | else: |
| 179 | log.warning('Failed to set %s ', str(widget.__class__.__name__)) | 179 | log.warning("Failed to set %s ", str(widget.__class__.__name__)) |
| 180 | return False | 180 | return False |
| 181 | return True | 181 | return True |
| 182 | 182 | ||
| @@ -184,8 +184,7 @@ def setWidgetValue(widget, val): | |||
| 184 | def getWidgetValue(widget): | 184 | def getWidgetValue(widget): |
| 185 | if type(widget) == QtWidgets.QLineEdit: | 185 | if type(widget) == QtWidgets.QLineEdit: |
| 186 | return widget.text() | 186 | return widget.text() |
| 187 | elif type(widget) == QtWidgets.QSpinBox \ | 187 | elif type(widget) == QtWidgets.QSpinBox or type(widget) == QtWidgets.QDoubleSpinBox: |
| 188 | or type(widget) == QtWidgets.QDoubleSpinBox: | ||
| 189 | return widget.value() | 188 | return widget.value() |
| 190 | elif type(widget) == QtWidgets.QCheckBox: | 189 | elif type(widget) == QtWidgets.QCheckBox: |
| 191 | return widget.isChecked() | 190 | return widget.isChecked() |
diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index ff06469..5aedff3 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Tools for using ffmpeg | 2 | Tools for using ffmpeg |
| 3 | ''' | 3 | """ |
| 4 | |||
| 4 | import numpy | 5 | import numpy |
| 5 | import sys | 6 | import sys |
| 6 | import os | 7 | import os |
| @@ -14,67 +15,74 @@ from .. import core | |||
| 14 | from .common import checkOutput, pipeWrapper | 15 | from .common import checkOutput, pipeWrapper |
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | log = logging.getLogger('AVP.Toolkit.Ffmpeg') | 18 | log = logging.getLogger("AVP.Toolkit.Ffmpeg") |
| 18 | 19 | ||
| 19 | 20 | ||
| 20 | class FfmpegVideo: | 21 | class FfmpegVideo: |
| 21 | '''Opens a pipe to ffmpeg and stores a buffer of raw video frames.''' | 22 | """Opens a pipe to ffmpeg and stores a buffer of raw video frames.""" |
| 22 | 23 | ||
| 23 | # error from the thread used to fill the buffer | 24 | # error from the thread used to fill the buffer |
| 24 | threadError = None | 25 | threadError = None |
| 25 | 26 | ||
| 26 | def __init__(self, **kwargs): | 27 | def __init__(self, **kwargs): |
| 27 | mandatoryArgs = [ | 28 | mandatoryArgs = [ |
| 28 | 'inputPath', | 29 | "inputPath", |
| 29 | 'filter_', | 30 | "filter_", |
| 30 | 'width', | 31 | "width", |
| 31 | 'height', | 32 | "height", |
| 32 | 'frameRate', # frames per second | 33 | "frameRate", # frames per second |
| 33 | 'chunkSize', # number of bytes in one frame | 34 | "chunkSize", # number of bytes in one frame |
| 34 | 'parent', # mainwindow object | 35 | "parent", # mainwindow object |
| 35 | 'component', # component object | 36 | "component", # component object |
| 36 | ] | 37 | ] |
| 37 | for arg in mandatoryArgs: | 38 | for arg in mandatoryArgs: |
| 38 | setattr(self, arg, kwargs[arg]) | 39 | setattr(self, arg, kwargs[arg]) |
| 39 | 40 | ||
| 40 | self.frameNo = -1 | 41 | self.frameNo = -1 |
| 41 | self.currentFrame = 'None' | 42 | self.currentFrame = "None" |
| 42 | self.map_ = None | 43 | self.map_ = None |
| 43 | 44 | ||
| 44 | if 'loopVideo' in kwargs and kwargs['loopVideo']: | 45 | if "loopVideo" in kwargs and kwargs["loopVideo"]: |
| 45 | self.loopValue = '-1' | 46 | self.loopValue = "-1" |
| 46 | else: | 47 | else: |
| 47 | self.loopValue = '0' | 48 | self.loopValue = "0" |
| 48 | if 'filter_' in kwargs: | 49 | if "filter_" in kwargs: |
| 49 | if kwargs['filter_'][0] != '-filter_complex': | 50 | if kwargs["filter_"][0] != "-filter_complex": |
| 50 | kwargs['filter_'].insert(0, '-filter_complex') | 51 | kwargs["filter_"].insert(0, "-filter_complex") |
| 51 | else: | 52 | else: |
| 52 | kwargs['filter_'] = None | 53 | kwargs["filter_"] = None |
| 53 | 54 | ||
| 54 | self.command = [ | 55 | self.command = [ |
| 55 | core.Core.FFMPEG_BIN, | 56 | core.Core.FFMPEG_BIN, |
| 56 | '-thread_queue_size', '512', | 57 | "-thread_queue_size", |
| 57 | '-r', str(self.frameRate), | 58 | "512", |
| 58 | '-stream_loop', str(self.loopValue), | 59 | "-r", |
| 59 | '-i', self.inputPath, | 60 | str(self.frameRate), |
| 60 | '-f', 'image2pipe', | 61 | "-stream_loop", |
| 61 | '-pix_fmt', 'rgba', | 62 | str(self.loopValue), |
| 63 | "-i", | ||
| 64 | self.inputPath, | ||
| 65 | "-f", | ||
| 66 | "image2pipe", | ||
| 67 | "-pix_fmt", | ||
| 68 | "rgba", | ||
| 62 | ] | 69 | ] |
| 63 | if type(kwargs['filter_']) is list: | 70 | if type(kwargs["filter_"]) is list: |
| 64 | self.command.extend( | 71 | self.command.extend(kwargs["filter_"]) |
| 65 | kwargs['filter_'] | 72 | self.command.extend( |
| 66 | ) | 73 | [ |
| 67 | self.command.extend([ | 74 | "-codec:v", |
| 68 | '-codec:v', 'rawvideo', '-', | 75 | "rawvideo", |
| 69 | ]) | 76 | "-", |
| 77 | ] | ||
| 78 | ) | ||
| 70 | 79 | ||
| 71 | self.frameBuffer = PriorityQueue() | 80 | self.frameBuffer = PriorityQueue() |
| 72 | self.frameBuffer.maxsize = self.frameRate | 81 | self.frameBuffer.maxsize = self.frameRate |
| 73 | self.finishedFrames = {} | 82 | self.finishedFrames = {} |
| 74 | 83 | ||
| 75 | self.thread = threading.Thread( | 84 | self.thread = threading.Thread( |
| 76 | target=self.fillBuffer, | 85 | target=self.fillBuffer, name="FFmpeg Frame-Fetcher" |
| 77 | name='FFmpeg Frame-Fetcher' | ||
| 78 | ) | 86 | ) |
| 79 | self.thread.daemon = True | 87 | self.thread.daemon = True |
| 80 | self.thread.start() | 88 | self.thread.start() |
| @@ -91,22 +99,29 @@ class FfmpegVideo: | |||
| 91 | 99 | ||
| 92 | def fillBuffer(self): | 100 | def fillBuffer(self): |
| 93 | from ..component import ComponentError | 101 | from ..component import ComponentError |
| 102 | |||
| 94 | if core.Core.logEnabled: | 103 | if core.Core.logEnabled: |
| 95 | logFilename = os.path.join( | 104 | logFilename = os.path.join( |
| 96 | core.Core.logDir, 'render_%s.log' % str(self.component.compPos) | 105 | core.Core.logDir, "render_%s.log" % str(self.component.compPos) |
| 97 | ) | 106 | ) |
| 98 | log.debug('Creating ffmpeg process (log at %s)', logFilename) | 107 | log.debug("Creating ffmpeg process (log at %s)", logFilename) |
| 99 | with open(logFilename, 'w') as logf: | 108 | with open(logFilename, "w") as logf: |
| 100 | logf.write(" ".join(self.command) + '\n\n') | 109 | logf.write(" ".join(self.command) + "\n\n") |
| 101 | with open(logFilename, 'a') as logf: | 110 | with open(logFilename, "a") as logf: |
| 102 | self.pipe = openPipe( | 111 | self.pipe = openPipe( |
| 103 | self.command, stdin=subprocess.DEVNULL, | 112 | self.command, |
| 104 | stdout=subprocess.PIPE, stderr=logf, bufsize=10**8 | 113 | stdin=subprocess.DEVNULL, |
| 114 | stdout=subprocess.PIPE, | ||
| 115 | stderr=logf, | ||
| 116 | bufsize=10**8, | ||
| 105 | ) | 117 | ) |
| 106 | else: | 118 | else: |
| 107 | self.pipe = openPipe( | 119 | self.pipe = openPipe( |
| 108 | self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, | 120 | self.command, |
| 109 | stderr=subprocess.DEVNULL, bufsize=10**8 | 121 | stdin=subprocess.DEVNULL, |
| 122 | stdout=subprocess.PIPE, | ||
| 123 | stderr=subprocess.DEVNULL, | ||
| 124 | bufsize=10**8, | ||
| 110 | ) | 125 | ) |
| 111 | 126 | ||
| 112 | while True: | 127 | while True: |
| @@ -117,12 +132,13 @@ class FfmpegVideo: | |||
| 117 | # If we run out of frames, use the last good frame and loop. | 132 | # If we run out of frames, use the last good frame and loop. |
| 118 | try: | 133 | try: |
| 119 | if len(self.currentFrame) == 0: | 134 | if len(self.currentFrame) == 0: |
| 120 | self.frameBuffer.put((self.frameNo-1, self.lastFrame)) | 135 | self.frameBuffer.put((self.frameNo - 1, self.lastFrame)) |
| 121 | continue | 136 | continue |
| 122 | except AttributeError: | 137 | except AttributeError: |
| 123 | FfmpegVideo.threadError = ComponentError( | 138 | FfmpegVideo.threadError = ComponentError( |
| 124 | self.component, 'video', | 139 | self.component, |
| 125 | "Video seemed playable but wasn't." | 140 | "video", |
| 141 | "Video seemed playable but wasn't.", | ||
| 126 | ) | 142 | ) |
| 127 | break | 143 | break |
| 128 | 144 | ||
| @@ -130,11 +146,12 @@ class FfmpegVideo: | |||
| 130 | self.currentFrame = self.pipe.stdout.read(self.chunkSize) | 146 | self.currentFrame = self.pipe.stdout.read(self.chunkSize) |
| 131 | except ValueError as e: | 147 | except ValueError as e: |
| 132 | if str(e) == "PyMemoryView_FromBuffer(): info->buf must not be NULL": | 148 | if str(e) == "PyMemoryView_FromBuffer(): info->buf must not be NULL": |
| 133 | log.debug("Ignored 'info->buf must not be NULL' error from FFmpeg pipe") | 149 | log.debug( |
| 150 | "Ignored 'info->buf must not be NULL' error from FFmpeg pipe" | ||
| 151 | ) | ||
| 134 | return | 152 | return |
| 135 | else: | 153 | else: |
| 136 | FfmpegVideo.threadError = ComponentError( | 154 | FfmpegVideo.threadError = ComponentError(self.component, "video") |
| 137 | self.component, 'video') | ||
| 138 | 155 | ||
| 139 | if len(self.currentFrame) != 0: | 156 | if len(self.currentFrame) != 0: |
| 140 | self.frameBuffer.put((self.frameNo, self.currentFrame)) | 157 | self.frameBuffer.put((self.frameNo, self.currentFrame)) |
| @@ -153,19 +170,17 @@ def closePipe(pipe): | |||
| 153 | 170 | ||
| 154 | def findFfmpeg(): | 171 | def findFfmpeg(): |
| 155 | if sys.platform == "win32": | 172 | if sys.platform == "win32": |
| 156 | bin = 'ffmpeg.exe' | 173 | bin = "ffmpeg.exe" |
| 157 | else: | 174 | else: |
| 158 | bin = 'ffmpeg' | 175 | bin = "ffmpeg" |
| 159 | 176 | ||
| 160 | if getattr(sys, 'frozen', False): | 177 | if getattr(sys, "frozen", False): |
| 161 | # The application is frozen | 178 | # The application is frozen |
| 162 | bin = os.path.join(core.Core.wd, bin) | 179 | bin = os.path.join(core.Core.wd, bin) |
| 163 | 180 | ||
| 164 | with open(os.devnull, "w") as f: | 181 | with open(os.devnull, "w") as f: |
| 165 | try: | 182 | try: |
| 166 | checkOutput( | 183 | checkOutput([bin, "-version"], stderr=f) |
| 167 | [bin, '-version'], stderr=f | ||
| 168 | ) | ||
| 169 | except (subprocess.CalledProcessError, FileNotFoundError): | 184 | except (subprocess.CalledProcessError, FileNotFoundError): |
| 170 | bin = "" | 185 | bin = "" |
| 171 | 186 | ||
| @@ -173,9 +188,9 @@ def findFfmpeg(): | |||
| 173 | 188 | ||
| 174 | 189 | ||
| 175 | def createFfmpegCommand(inputFile, outputFile, components, duration=-1): | 190 | def createFfmpegCommand(inputFile, outputFile, components, duration=-1): |
| 176 | ''' | 191 | """ |
| 177 | Constructs the major ffmpeg command used to export the video | 192 | Constructs the major ffmpeg command used to export the video |
| 178 | ''' | 193 | """ |
| 179 | if duration == -1: | 194 | if duration == -1: |
| 180 | duration = getAudioDuration(inputFile) | 195 | duration = getAudioDuration(inputFile) |
| 181 | safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters | 196 | safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters |
| @@ -183,31 +198,33 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): | |||
| 183 | Core = core.Core | 198 | Core = core.Core |
| 184 | 199 | ||
| 185 | # Test if user has libfdk_aac | 200 | # Test if user has libfdk_aac |
| 186 | encoders = checkOutput( | 201 | encoders = checkOutput("%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True) |
| 187 | "%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True | ||
| 188 | ) | ||
| 189 | encoders = encoders.decode("utf-8") | 202 | encoders = encoders.decode("utf-8") |
| 190 | 203 | ||
| 191 | acodec = Core.settings.value('outputAudioCodec') | 204 | acodec = Core.settings.value("outputAudioCodec") |
| 192 | 205 | ||
| 193 | options = Core.encoderOptions | 206 | options = Core.encoderOptions |
| 194 | containerName = Core.settings.value('outputContainer') | 207 | containerName = Core.settings.value("outputContainer") |
| 195 | vcodec = Core.settings.value('outputVideoCodec') | 208 | vcodec = Core.settings.value("outputVideoCodec") |
| 196 | vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k' | 209 | vbitrate = str(Core.settings.value("outputVideoBitrate")) + "k" |
| 197 | acodec = Core.settings.value('outputAudioCodec') | 210 | acodec = Core.settings.value("outputAudioCodec") |
| 198 | abitrate = str(Core.settings.value('outputAudioBitrate'))+'k' | 211 | abitrate = str(Core.settings.value("outputAudioBitrate")) + "k" |
| 199 | 212 | ||
| 200 | for cont in options['containers']: | 213 | for cont in options["containers"]: |
| 201 | if cont['name'] == containerName: | 214 | if cont["name"] == containerName: |
| 202 | container = cont['container'] | 215 | container = cont["container"] |
| 203 | break | 216 | break |
| 204 | 217 | ||
| 205 | vencoders = options['video-codecs'][vcodec] | 218 | vencoders = options["video-codecs"][vcodec] |
| 206 | aencoders = options['audio-codecs'][acodec] | 219 | aencoders = options["audio-codecs"][acodec] |
| 207 | 220 | ||
| 208 | def error(): | 221 | def error(): |
| 209 | nonlocal encoders, encoder | 222 | nonlocal encoders, encoder |
| 210 | log.critical("Selected encoder (%s) is not supported by Ffmpeg. The supported encoders are: %s", encoder, encoders) | 223 | log.critical( |
| 224 | "Selected encoder (%s) is not supported by Ffmpeg. The supported encoders are: %s", | ||
| 225 | encoder, | ||
| 226 | encoders, | ||
| 227 | ) | ||
| 211 | return [] | 228 | return [] |
| 212 | 229 | ||
| 213 | for encoder in vencoders: | 230 | for encoder in vencoders: |
| @@ -226,57 +243,75 @@ def createFfmpegCommand(inputFile, outputFile, components, duration=-1): | |||
| 226 | 243 | ||
| 227 | ffmpegCommand = [ | 244 | ffmpegCommand = [ |
| 228 | Core.FFMPEG_BIN, | 245 | Core.FFMPEG_BIN, |
| 229 | '-thread_queue_size', '512', | 246 | "-thread_queue_size", |
| 230 | '-y', # overwrite the output file if it already exists. | 247 | "512", |
| 231 | 248 | "-y", # overwrite the output file if it already exists. | |
| 232 | # INPUT VIDEO | 249 | # INPUT VIDEO |
| 233 | '-f', 'rawvideo', | 250 | "-f", |
| 234 | '-vcodec', 'rawvideo', | 251 | "rawvideo", |
| 235 | '-s', f'{Core.settings.value("outputWidth")}x{Core.settings.value("outputHeight")}', | 252 | "-vcodec", |
| 236 | '-pix_fmt', 'rgba', | 253 | "rawvideo", |
| 237 | '-r', str(Core.settings.value('outputFrameRate')), | 254 | "-s", |
| 238 | '-t', duration, | 255 | f'{Core.settings.value("outputWidth")}x{Core.settings.value("outputHeight")}', |
| 239 | '-an', # the video input has no sound | 256 | "-pix_fmt", |
| 240 | '-i', '-', # the video input comes from a pipe | 257 | "rgba", |
| 241 | 258 | "-r", | |
| 259 | str(Core.settings.value("outputFrameRate")), | ||
| 260 | "-t", | ||
| 261 | duration, | ||
| 262 | "-an", # the video input has no sound | ||
| 263 | "-i", | ||
| 264 | "-", # the video input comes from a pipe | ||
| 242 | # INPUT SOUND | 265 | # INPUT SOUND |
| 243 | '-t', duration, | 266 | "-t", |
| 244 | '-i', inputFile | 267 | duration, |
| 268 | "-i", | ||
| 269 | inputFile, | ||
| 245 | ] | 270 | ] |
| 246 | 271 | ||
| 247 | extraAudio = [ | 272 | extraAudio = [comp.audio for comp in components if "audio" in comp.properties()] |
| 248 | comp.audio for comp in components | ||
| 249 | if 'audio' in comp.properties() | ||
| 250 | ] | ||
| 251 | segment = createAudioFilterCommand(extraAudio, safeDuration) | 273 | segment = createAudioFilterCommand(extraAudio, safeDuration) |
| 252 | ffmpegCommand.extend(segment) | 274 | ffmpegCommand.extend(segment) |
| 253 | # Map audio from the filters or the single audio input, and map video from the pipe | 275 | # Map audio from the filters or the single audio input, and map video from the pipe |
| 254 | ffmpegCommand.extend([ | 276 | ffmpegCommand.extend( |
| 255 | '-map', '0:v', | 277 | [ |
| 256 | '-map', '[a]' if segment else '1:a', | 278 | "-map", |
| 257 | ]) | 279 | "0:v", |
| 258 | 280 | "-map", | |
| 259 | ffmpegCommand.extend([ | 281 | "[a]" if segment else "1:a", |
| 260 | # OUTPUT | 282 | ] |
| 261 | '-vcodec', vencoder, | 283 | ) |
| 262 | '-acodec', aencoder, | 284 | |
| 263 | '-b:v', vbitrate, | 285 | ffmpegCommand.extend( |
| 264 | '-b:a', abitrate, | 286 | [ |
| 265 | '-pix_fmt', Core.settings.value('outputVideoFormat'), | 287 | # OUTPUT |
| 266 | '-preset', Core.settings.value('outputPreset'), | 288 | "-vcodec", |
| 267 | '-f', container | 289 | vencoder, |
| 268 | ]) | 290 | "-acodec", |
| 269 | 291 | aencoder, | |
| 270 | if acodec == 'aac': | 292 | "-b:v", |
| 271 | ffmpegCommand.append('-strict') | 293 | vbitrate, |
| 272 | ffmpegCommand.append('-2') | 294 | "-b:a", |
| 295 | abitrate, | ||
| 296 | "-pix_fmt", | ||
| 297 | Core.settings.value("outputVideoFormat"), | ||
| 298 | "-preset", | ||
| 299 | Core.settings.value("outputPreset"), | ||
| 300 | "-f", | ||
| 301 | container, | ||
| 302 | ] | ||
| 303 | ) | ||
| 304 | |||
| 305 | if acodec == "aac": | ||
| 306 | ffmpegCommand.append("-strict") | ||
| 307 | ffmpegCommand.append("-2") | ||
| 273 | 308 | ||
| 274 | ffmpegCommand.append(outputFile) | 309 | ffmpegCommand.append(outputFile) |
| 275 | return ffmpegCommand | 310 | return ffmpegCommand |
| 276 | 311 | ||
| 277 | 312 | ||
| 278 | def createAudioFilterCommand(extraAudio, duration): | 313 | def createAudioFilterCommand(extraAudio, duration): |
| 279 | '''Add extra inputs and any needed filters to the main ffmpeg command.''' | 314 | """Add extra inputs and any needed filters to the main ffmpeg command.""" |
| 280 | # NOTE: Global filters are currently hard-coded here for debugging use | 315 | # NOTE: Global filters are currently hard-coded here for debugging use |
| 281 | globalFilters = 0 # increase to add global filters | 316 | globalFilters = 0 # increase to add global filters |
| 282 | 317 | ||
| @@ -288,21 +323,23 @@ def createAudioFilterCommand(extraAudio, duration): | |||
| 288 | extraFilters = {} | 323 | extraFilters = {} |
| 289 | for streamNo, params in enumerate(reversed(extraAudio)): | 324 | for streamNo, params in enumerate(reversed(extraAudio)): |
| 290 | extraInputFile, params = params | 325 | extraInputFile, params = params |
| 291 | ffmpegCommand.extend([ | 326 | ffmpegCommand.extend( |
| 292 | '-t', duration, | 327 | [ |
| 293 | # Tell ffmpeg about shorter clips (seemingly not needed) | 328 | "-t", |
| 294 | # streamDuration = getAudioDuration(extraInputFile) | 329 | duration, |
| 295 | # if streamDuration and streamDuration > float(safeDuration) | 330 | # Tell ffmpeg about shorter clips (seemingly not needed) |
| 296 | # else "{0:.3f}".format(streamDuration), | 331 | # streamDuration = getAudioDuration(extraInputFile) |
| 297 | '-i', extraInputFile | 332 | # if streamDuration and streamDuration > float(safeDuration) |
| 298 | ]) | 333 | # else "{0:.3f}".format(streamDuration), |
| 334 | "-i", | ||
| 335 | extraInputFile, | ||
| 336 | ] | ||
| 337 | ) | ||
| 299 | # Construct dataset of extra filters we'll need to add later | 338 | # Construct dataset of extra filters we'll need to add later |
| 300 | for ffmpegFilter in params: | 339 | for ffmpegFilter in params: |
| 301 | if streamNo + 2 not in extraFilters: | 340 | if streamNo + 2 not in extraFilters: |
| 302 | extraFilters[streamNo + 2] = [] | 341 | extraFilters[streamNo + 2] = [] |
| 303 | extraFilters[streamNo + 2].append(( | 342 | extraFilters[streamNo + 2].append((ffmpegFilter, params[ffmpegFilter])) |
| 304 | ffmpegFilter, params[ffmpegFilter] | ||
| 305 | )) | ||
| 306 | 343 | ||
| 307 | # Start creating avfilters! Popen-style, so don't use semicolons; | 344 | # Start creating avfilters! Popen-style, so don't use semicolons; |
| 308 | extraFilterCommand = [] | 345 | extraFilterCommand = [] |
| @@ -318,63 +355,73 @@ def createAudioFilterCommand(extraAudio, duration): | |||
| 318 | extraFilters[streamNo + 1] = [] | 355 | extraFilters[streamNo + 1] = [] |
| 319 | # Also filter the primary audio track | 356 | # Also filter the primary audio track |
| 320 | extraFilters[1] = [] | 357 | extraFilters[1] = [] |
| 321 | tmpInputs = { | 358 | tmpInputs = {streamNo: globalFilters - 1 for streamNo in extraFilters} |
| 322 | streamNo: globalFilters - 1 | ||
| 323 | for streamNo in extraFilters | ||
| 324 | } | ||
| 325 | 359 | ||
| 326 | # Add the global filters! | 360 | # Add the global filters! |
| 327 | # NOTE: list length must = globalFilters, currently hardcoded | 361 | # NOTE: list length must = globalFilters, currently hardcoded |
| 328 | if tmpInputs: | 362 | if tmpInputs: |
| 329 | extraFilterCommand.extend([ | 363 | extraFilterCommand.extend( |
| 330 | '[%s:a] ashowinfo [%stmp0]' % ( | 364 | [ |
| 331 | str(streamNo), | 365 | "[%s:a] ashowinfo [%stmp0]" % (str(streamNo), str(streamNo)) |
| 332 | str(streamNo) | 366 | for streamNo in tmpInputs |
| 333 | ) | 367 | ] |
| 334 | for streamNo in tmpInputs | 368 | ) |
| 335 | ]) | ||
| 336 | 369 | ||
| 337 | # Now add the per-stream filters! | 370 | # Now add the per-stream filters! |
| 338 | for streamNo, paramList in extraFilters.items(): | 371 | for streamNo, paramList in extraFilters.items(): |
| 339 | for param in paramList: | 372 | for param in paramList: |
| 340 | source = '[%s:a]' % str(streamNo) \ | 373 | source = ( |
| 341 | if tmpInputs[streamNo] == -1 else \ | 374 | "[%s:a]" % str(streamNo) |
| 342 | '[%stmp%s]' % ( | 375 | if tmpInputs[streamNo] == -1 |
| 343 | str(streamNo), str(tmpInputs[streamNo]) | 376 | else "[%stmp%s]" % (str(streamNo), str(tmpInputs[streamNo])) |
| 344 | ) | 377 | ) |
| 345 | tmpInputs[streamNo] = tmpInputs[streamNo] + 1 | 378 | tmpInputs[streamNo] = tmpInputs[streamNo] + 1 |
| 346 | extraFilterCommand.append( | 379 | extraFilterCommand.append( |
| 347 | '%s %s%s [%stmp%s]' % ( | 380 | "%s %s%s [%stmp%s]" |
| 348 | source, param[0], param[1], str(streamNo), | 381 | % ( |
| 349 | str(tmpInputs[streamNo]) | 382 | source, |
| 383 | param[0], | ||
| 384 | param[1], | ||
| 385 | str(streamNo), | ||
| 386 | str(tmpInputs[streamNo]), | ||
| 350 | ) | 387 | ) |
| 351 | ) | 388 | ) |
| 352 | 389 | ||
| 353 | # Join all the filters together and combine into 1 stream | 390 | # Join all the filters together and combine into 1 stream |
| 354 | extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \ | 391 | extraFilterCommand = "; ".join(extraFilterCommand) + "; " if tmpInputs else "" |
| 355 | if tmpInputs else '' | 392 | ffmpegCommand.extend( |
| 356 | ffmpegCommand.extend([ | 393 | [ |
| 357 | '-filter_complex', | 394 | "-filter_complex", |
| 358 | extraFilterCommand + | 395 | extraFilterCommand |
| 359 | '%s amix=inputs=%s:duration=first [a]' | 396 | + "%s amix=inputs=%s:duration=first [a]" |
| 360 | % ( | 397 | % ( |
| 361 | "".join([ | 398 | "".join( |
| 362 | '[%stmp%s]' % (str(i), tmpInputs[i]) | 399 | [ |
| 363 | if i in extraFilters else '[%s:a]' % str(i) | 400 | ( |
| 364 | for i in range(1, len(extraAudio) + 2) | 401 | "[%stmp%s]" % (str(i), tmpInputs[i]) |
| 365 | ]), | 402 | if i in extraFilters |
| 366 | str(len(extraAudio) + 1) | 403 | else "[%s:a]" % str(i) |
| 367 | ), | 404 | ) |
| 368 | ]) | 405 | for i in range(1, len(extraAudio) + 2) |
| 406 | ] | ||
| 407 | ), | ||
| 408 | str(len(extraAudio) + 1), | ||
| 409 | ), | ||
| 410 | ] | ||
| 411 | ) | ||
| 369 | return ffmpegCommand | 412 | return ffmpegCommand |
| 370 | 413 | ||
| 371 | 414 | ||
| 372 | def testAudioStream(filename): | 415 | def testAudioStream(filename): |
| 373 | '''Test if an audio stream definitely exists''' | 416 | """Test if an audio stream definitely exists""" |
| 374 | audioTestCommand = [ | 417 | audioTestCommand = [ |
| 375 | core.Core.FFMPEG_BIN, | 418 | core.Core.FFMPEG_BIN, |
| 376 | '-i', filename, | 419 | "-i", |
| 377 | '-vn', '-f', 'null', '-' | 420 | filename, |
| 421 | "-vn", | ||
| 422 | "-f", | ||
| 423 | "null", | ||
| 424 | "-", | ||
| 378 | ] | 425 | ] |
| 379 | try: | 426 | try: |
| 380 | checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) | 427 | checkOutput(audioTestCommand, stderr=subprocess.DEVNULL) |
| @@ -385,8 +432,8 @@ def testAudioStream(filename): | |||
| 385 | 432 | ||
| 386 | 433 | ||
| 387 | def getAudioDuration(filename): | 434 | def getAudioDuration(filename): |
| 388 | '''Try to get duration of audio file as float, or False if not possible''' | 435 | """Try to get duration of audio file as float, or False if not possible""" |
| 389 | command = [core.Core.FFMPEG_BIN, '-i', filename] | 436 | command = [core.Core.FFMPEG_BIN, "-i", filename] |
| 390 | 437 | ||
| 391 | try: | 438 | try: |
| 392 | fileInfo = checkOutput(command, stderr=subprocess.STDOUT) | 439 | fileInfo = checkOutput(command, stderr=subprocess.STDOUT) |
| @@ -397,17 +444,17 @@ def getAudioDuration(filename): | |||
| 397 | return False | 444 | return False |
| 398 | 445 | ||
| 399 | try: | 446 | try: |
| 400 | info = fileInfo.decode("utf-8").split('\n') | 447 | info = fileInfo.decode("utf-8").split("\n") |
| 401 | except UnicodeDecodeError as e: | 448 | except UnicodeDecodeError as e: |
| 402 | log.error('Unicode error:', str(e)) | 449 | log.error("Unicode error:", str(e)) |
| 403 | return False | 450 | return False |
| 404 | 451 | ||
| 405 | for line in info: | 452 | for line in info: |
| 406 | if 'Duration' in line: | 453 | if "Duration" in line: |
| 407 | d = line.split(',')[0] | 454 | d = line.split(",")[0] |
| 408 | d = d.split(' ')[3] | 455 | d = d.split(" ")[3] |
| 409 | d = d.split(':') | 456 | d = d.split(":") |
| 410 | duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) | 457 | duration = float(d[0]) * 3600 + float(d[1]) * 60 + float(d[2]) |
| 411 | break | 458 | break |
| 412 | else: | 459 | else: |
| 413 | # String not found in output | 460 | # String not found in output |
| @@ -416,10 +463,10 @@ def getAudioDuration(filename): | |||
| 416 | 463 | ||
| 417 | 464 | ||
| 418 | def readAudioFile(filename, videoWorker): | 465 | def readAudioFile(filename, videoWorker): |
| 419 | ''' | 466 | """ |
| 420 | Creates the completeAudioArray given to components | 467 | Creates the completeAudioArray given to components |
| 421 | and used to draw the classic visualizer. | 468 | and used to draw the classic visualizer. |
| 422 | ''' | 469 | """ |
| 423 | duration = getAudioDuration(filename) | 470 | duration = getAudioDuration(filename) |
| 424 | if not duration: | 471 | if not duration: |
| 425 | log.error(f"Audio file {filename} doesn't exist or unreadable.") | 472 | log.error(f"Audio file {filename} doesn't exist or unreadable.") |
| @@ -427,15 +474,23 @@ def readAudioFile(filename, videoWorker): | |||
| 427 | 474 | ||
| 428 | command = [ | 475 | command = [ |
| 429 | core.Core.FFMPEG_BIN, | 476 | core.Core.FFMPEG_BIN, |
| 430 | '-i', filename, | 477 | "-i", |
| 431 | '-f', 's16le', | 478 | filename, |
| 432 | '-acodec', 'pcm_s16le', | 479 | "-f", |
| 433 | '-ar', '44100', # ouput will have 44100 Hz | 480 | "s16le", |
| 434 | '-ac', '1', # mono (set to '2' for stereo) | 481 | "-acodec", |
| 435 | '-'] | 482 | "pcm_s16le", |
| 483 | "-ar", | ||
| 484 | "44100", # ouput will have 44100 Hz | ||
| 485 | "-ac", | ||
| 486 | "1", # mono (set to '2' for stereo) | ||
| 487 | "-", | ||
| 488 | ] | ||
| 436 | in_pipe = openPipe( | 489 | in_pipe = openPipe( |
| 437 | command, | 490 | command, |
| 438 | stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8 | 491 | stdout=subprocess.PIPE, |
| 492 | stderr=subprocess.DEVNULL, | ||
| 493 | bufsize=10**8, | ||
| 439 | ) | 494 | ) |
| 440 | 495 | ||
| 441 | completeAudioArray = numpy.empty(0, dtype="int16") | 496 | completeAudioArray = numpy.empty(0, dtype="int16") |
| @@ -447,18 +502,18 @@ def readAudioFile(filename, videoWorker): | |||
| 447 | return | 502 | return |
| 448 | # read 2 seconds of audio | 503 | # read 2 seconds of audio |
| 449 | progress += 4 | 504 | progress += 4 |
| 450 | raw_audio = in_pipe.stdout.read(88200*4) | 505 | raw_audio = in_pipe.stdout.read(88200 * 4) |
| 451 | if len(raw_audio) == 0: | 506 | if len(raw_audio) == 0: |
| 452 | break | 507 | break |
| 453 | audio_array = numpy.fromstring(raw_audio, dtype="int16") | 508 | audio_array = numpy.frombuffer(raw_audio, dtype="int16") |
| 454 | completeAudioArray = numpy.append(completeAudioArray, audio_array) | 509 | completeAudioArray = numpy.append(completeAudioArray, audio_array) |
| 455 | 510 | ||
| 456 | percent = int(100*(progress/duration)) | 511 | percent = int(100 * (progress / duration)) |
| 457 | if percent >= 100: | 512 | if percent >= 100: |
| 458 | percent = 100 | 513 | percent = 100 |
| 459 | 514 | ||
| 460 | if lastPercent != percent: | 515 | if lastPercent != percent: |
| 461 | string = 'Loading audio file: '+str(percent)+'%' | 516 | string = "Loading audio file: " + str(percent) + "%" |
| 462 | videoWorker.progressBarSetText.emit(string) | 517 | videoWorker.progressBarSetText.emit(string) |
| 463 | videoWorker.progressBarUpdate.emit(percent) | 518 | videoWorker.progressBarUpdate.emit(percent) |
| 464 | 519 | ||
| @@ -468,25 +523,23 @@ def readAudioFile(filename, videoWorker): | |||
| 468 | in_pipe.wait() | 523 | in_pipe.wait() |
| 469 | 524 | ||
| 470 | # add 0s the end | 525 | # add 0s the end |
| 471 | completeAudioArrayCopy = numpy.zeros( | 526 | completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16") |
| 472 | len(completeAudioArray) + 44100, dtype="int16") | 527 | completeAudioArrayCopy[: len(completeAudioArray)] = completeAudioArray |
| 473 | completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray | ||
| 474 | completeAudioArray = completeAudioArrayCopy | 528 | completeAudioArray = completeAudioArrayCopy |
| 475 | 529 | ||
| 476 | return (completeAudioArray, duration) | 530 | return (completeAudioArray, duration) |
| 477 | 531 | ||
| 478 | 532 | ||
| 479 | def exampleSound( | 533 | def exampleSound(style="white", extra="apulsator=offset_l=0.35:offset_r=0.67"): |
| 480 | style='white', extra='apulsator=offset_l=0.35:offset_r=0.67'): | 534 | """Help generate an example sound for use in creating a preview""" |
| 481 | '''Help generate an example sound for use in creating a preview''' | ||
| 482 | 535 | ||
| 483 | if style == 'white': | 536 | if style == "white": |
| 484 | src = '-2+random(0)' | 537 | src = "-2+random(0)" |
| 485 | elif style == 'freq': | 538 | elif style == "freq": |
| 486 | src = 'sin(1000*t*PI*t)' | 539 | src = "sin(1000*t*PI*t)" |
| 487 | elif style == 'wave': | 540 | elif style == "wave": |
| 488 | src = 'sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)' | 541 | src = "sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)" |
| 489 | elif style == 'stereo': | 542 | elif style == "stereo": |
| 490 | src = '0.1*sin(2*PI*(360-2.5/2)*t) | 0.1*sin(2*PI*(360+2.5/2)*t)' | 543 | src = "0.1*sin(2*PI*(360-2.5/2)*t) | 0.1*sin(2*PI*(360+2.5/2)*t)" |
| 491 | 544 | ||
| 492 | return "aevalsrc='%s', %s%s" % (src, extra, ', ' if extra else '') | 545 | return "aevalsrc='%s', %s%s" % (src, extra, ", " if extra else "") |
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 520bd43..94537a6 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py | |||
| @@ -1,25 +1,27 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Common tools for drawing compatible frames in a Component's frameRender() | 2 | Common tools for drawing compatible frames in a Component's frameRender() |
| 3 | ''' | 3 | """ |
| 4 | from PyQt5 import QtGui | 4 | |
| 5 | from PyQt6 import QtGui | ||
| 5 | from PIL import Image | 6 | from PIL import Image |
| 6 | from PIL.ImageQt import ImageQt | 7 | from PIL.ImageQt import ImageQt |
| 8 | from PyQt6 import QtCore | ||
| 7 | import sys | 9 | import sys |
| 8 | import os | 10 | import os |
| 9 | import math | 11 | import math |
| 10 | import logging | 12 | import logging |
| 11 | |||
| 12 | from .. import core | 13 | from .. import core |
| 13 | 14 | ||
| 14 | 15 | ||
| 15 | log = logging.getLogger('AVP.Toolkit.Frame') | 16 | log = logging.getLogger("AVP.Toolkit.Frame") |
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | class FramePainter(QtGui.QPainter): | 19 | class FramePainter(QtGui.QPainter): |
| 19 | ''' | 20 | """ |
| 20 | A QPainter for a blank frame, which can be converted into a | 21 | A QPainter for a blank frame, which can be converted into a |
| 21 | Pillow image with finalize() | 22 | Pillow image with finalize() |
| 22 | ''' | 23 | """ |
| 24 | |||
| 23 | def __init__(self, width, height): | 25 | def __init__(self, width, height): |
| 24 | image = BlankFrame(width, height) | 26 | image = BlankFrame(width, height) |
| 25 | log.debug("Creating QImage from PIL image object") | 27 | log.debug("Creating QImage from PIL image object") |
| @@ -34,21 +36,33 @@ class FramePainter(QtGui.QPainter): | |||
| 34 | 36 | ||
| 35 | def finalize(self): | 37 | def finalize(self): |
| 36 | log.verbose("Finalizing FramePainter") | 38 | log.verbose("Finalizing FramePainter") |
| 39 | buffer = QtCore.QBuffer() | ||
| 40 | buffer.open(QtCore.QBuffer.OpenModeFlag.ReadWrite) | ||
| 41 | self.image.save(buffer, "PNG") | ||
| 42 | import io | ||
| 43 | |||
| 44 | frame = Image.open(io.BytesIO(buffer.data())) | ||
| 45 | buffer.close() | ||
| 46 | self.end() | ||
| 47 | return frame | ||
| 37 | imBytes = self.image.bits().asstring(self.image.byteCount()) | 48 | imBytes = self.image.bits().asstring(self.image.byteCount()) |
| 38 | frame = Image.frombytes( | 49 | frame = Image.frombytes( |
| 39 | 'RGBA', (self.image.width(), self.image.height()), imBytes | 50 | "RGBA", (self.image.width(), self.image.height()), imBytes |
| 40 | ) | 51 | ) |
| 41 | self.end() | 52 | self.end() |
| 42 | return frame | 53 | return frame |
| 43 | 54 | ||
| 44 | 55 | ||
| 45 | class PaintColor(QtGui.QColor): | 56 | class PaintColor(QtGui.QColor): |
| 46 | '''Reverse the painter colour if the hardware stores RGB values backward''' | 57 | """ |
| 58 | Subclass of QtGui.QColor with an added scale() method | ||
| 59 | Previously this class reversed the painter colour to solve | ||
| 60 | hardware issues related to endianness, | ||
| 61 | but Qt appears to deal with this itself nowadays | ||
| 62 | """ | ||
| 63 | |||
| 47 | def __init__(self, r, g, b, a=255): | 64 | def __init__(self, r, g, b, a=255): |
| 48 | if sys.byteorder == 'big': | 65 | super().__init__(r, g, b, a) |
| 49 | super().__init__(r, g, b, a) | ||
| 50 | else: | ||
| 51 | super().__init__(b, g, r, a) | ||
| 52 | 66 | ||
| 53 | 67 | ||
| 54 | def scale(scalePercent, width, height, returntype=None): | 68 | def scale(scalePercent, width, height, returntype=None): |
| @@ -63,7 +77,8 @@ def scale(scalePercent, width, height, returntype=None): | |||
| 63 | 77 | ||
| 64 | 78 | ||
| 65 | def defaultSize(framefunc): | 79 | def defaultSize(framefunc): |
| 66 | '''Makes width/height arguments optional''' | 80 | """Makes width/height arguments optional""" |
| 81 | |||
| 67 | def decorator(*args): | 82 | def decorator(*args): |
| 68 | if len(args) < 2: | 83 | if len(args) < 2: |
| 69 | newArgs = list(args) | 84 | newArgs = list(args) |
| @@ -75,6 +90,7 @@ def defaultSize(framefunc): | |||
| 75 | newArgs.insert(0, width) | 90 | newArgs.insert(0, width) |
| 76 | args = tuple(newArgs) | 91 | args = tuple(newArgs) |
| 77 | return framefunc(*args) | 92 | return framefunc(*args) |
| 93 | |||
| 78 | return decorator | 94 | return decorator |
| 79 | 95 | ||
| 80 | 96 | ||
| @@ -84,21 +100,18 @@ def FloodFrame(width, height, RgbaTuple): | |||
| 84 | 100 | ||
| 85 | @defaultSize | 101 | @defaultSize |
| 86 | def BlankFrame(width, height): | 102 | def BlankFrame(width, height): |
| 87 | '''The base frame used by each component to start drawing.''' | 103 | """The base frame used by each component to start drawing.""" |
| 88 | return FloodFrame(width, height, (0, 0, 0, 0)) | 104 | return FloodFrame(width, height, (0, 0, 0, 0)) |
| 89 | 105 | ||
| 90 | 106 | ||
| 91 | @defaultSize | 107 | @defaultSize |
| 92 | def Checkerboard(width, height): | 108 | def Checkerboard(width, height): |
| 93 | ''' | 109 | """ |
| 94 | A checkerboard to represent transparency to the user. | 110 | A checkerboard to represent transparency to the user. |
| 95 | TODO: Would be cool to generate this image with numpy instead. | 111 | """ |
| 96 | ''' | 112 | # TODO: Would be cool to generate this image with numpy instead. |
| 97 | log.debug('Creating new %s*%s checkerboard' % (width, height)) | 113 | log.debug("Creating new %s*%s checkerboard" % (width, height)) |
| 98 | image = FloodFrame(1920, 1080, (0, 0, 0, 0)) | 114 | image = FloodFrame(1920, 1080, (0, 0, 0, 0)) |
| 99 | image.paste(Image.open( | 115 | image.paste(Image.open(os.path.join(core.Core.wd, "gui", "background.png")), (0, 0)) |
| 100 | os.path.join(core.Core.wd, 'gui', "background.png")), | ||
| 101 | (0, 0) | ||
| 102 | ) | ||
| 103 | image = image.resize((width, height)) | 116 | image = image.resize((width, height)) |
| 104 | return image | 117 | return image |
diff --git a/src/video_thread.py b/src/video_thread.py index 70e4d5d..5d72409 100644 --- a/src/video_thread.py +++ b/src/video_thread.py | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | ''' | 1 | """ |
| 2 | Worker thread created to export a video. It has a slot to begin export using | 2 | Worker thread created to export a video. It has a slot to begin export using |
| 3 | an input file, output path, and component list. | 3 | an input file, output path, and component list. |
| 4 | 4 | ||
| @@ -6,9 +6,10 @@ Signals are emitted to update MainWindow's progress bar, detail text, and previe | |||
| 6 | A Command object takes the place of MainWindow while in commandline mode. | 6 | A Command object takes the place of MainWindow while in commandline mode. |
| 7 | 7 | ||
| 8 | Export can be cancelled with cancel() | 8 | Export can be cancelled with cancel() |
| 9 | ''' | 9 | """ |
| 10 | from PyQt5 import QtCore, QtGui | 10 | |
| 11 | from PyQt5.QtCore import pyqtSignal, pyqtSlot | 11 | from PyQt6 import QtCore, QtGui |
| 12 | from PyQt6.QtCore import pyqtSignal, pyqtSlot | ||
| 12 | from PIL import Image | 13 | from PIL import Image |
| 13 | from PIL.ImageQt import ImageQt | 14 | from PIL.ImageQt import ImageQt |
| 14 | import numpy | 15 | import numpy |
| @@ -22,8 +23,10 @@ import logging | |||
| 22 | from .component import ComponentError | 23 | from .component import ComponentError |
| 23 | from .toolkit.frame import Checkerboard | 24 | from .toolkit.frame import Checkerboard |
| 24 | from .toolkit.ffmpeg import ( | 25 | from .toolkit.ffmpeg import ( |
| 25 | openPipe, readAudioFile, | 26 | openPipe, |
| 26 | getAudioDuration, createFfmpegCommand | 27 | readAudioFile, |
| 28 | getAudioDuration, | ||
| 29 | createFfmpegCommand, | ||
| 27 | ) | 30 | ) |
| 28 | 31 | ||
| 29 | 32 | ||
| @@ -32,7 +35,7 @@ log = logging.getLogger("AVP.VideoThread") | |||
| 32 | 35 | ||
| 33 | class Worker(QtCore.QObject): | 36 | class Worker(QtCore.QObject): |
| 34 | 37 | ||
| 35 | imageCreated = pyqtSignal('QImage') | 38 | imageCreated = pyqtSignal("QImage") |
| 36 | videoCreated = pyqtSignal() | 39 | videoCreated = pyqtSignal() |
| 37 | progressBarUpdate = pyqtSignal(int) | 40 | progressBarUpdate = pyqtSignal(int) |
| 38 | progressBarSetText = pyqtSignal(str) | 41 | progressBarSetText = pyqtSignal(str) |
| @@ -61,31 +64,34 @@ class Worker(QtCore.QObject): | |||
| 61 | self.inputFile, self.outputFile, self.components, duration | 64 | self.inputFile, self.outputFile, self.components, duration |
| 62 | ) | 65 | ) |
| 63 | except sp.CalledProcessError as e: | 66 | except sp.CalledProcessError as e: |
| 64 | #FIXME video_thread should own this error signal, not components | 67 | # FIXME video_thread should own this error signal, not components |
| 65 | self.components[0]._error.emit("Ffmpeg could not be found. Is it installed?", str(e)) | 68 | self.components[0]._error.emit( |
| 69 | "Ffmpeg could not be found. Is it installed?", str(e) | ||
| 70 | ) | ||
| 66 | self.error = True | 71 | self.error = True |
| 67 | return | 72 | return |
| 68 | 73 | ||
| 69 | if not ffmpegCommand: | 74 | if not ffmpegCommand: |
| 70 | #FIXME video_thread should own this error signal, not components | 75 | # FIXME video_thread should own this error signal, not components |
| 71 | self.components[0]._error.emit("The FFmpeg command could not be generated.", "") | 76 | self.components[0]._error.emit( |
| 72 | log.critical("Cancelling render process due to failure while generating the ffmpeg command.") | 77 | "The FFmpeg command could not be generated.", "" |
| 78 | ) | ||
| 79 | log.critical( | ||
| 80 | "Cancelling render process due to failure while generating the ffmpeg command." | ||
| 81 | ) | ||
| 73 | self.failExport() | 82 | self.failExport() |
| 74 | return | 83 | return |
| 75 | return ffmpegCommand | 84 | return ffmpegCommand |
| 76 | 85 | ||
| 77 | def determineAudioLength(self): | 86 | def determineAudioLength(self): |
| 78 | ''' | 87 | """ |
| 79 | Returns audio length which determines length of final video, or False if failure occurs | 88 | Returns audio length which determines length of final video, or False if failure occurs |
| 80 | ''' | 89 | """ |
| 81 | if any([ | 90 | if any( |
| 82 | True if 'pcm' in comp.properties() else False | 91 | [True if "pcm" in comp.properties() else False for comp in self.components] |
| 83 | for comp in self.components | 92 | ): |
| 84 | ]): | ||
| 85 | self.progressBarSetText.emit("Loading audio file...") | 93 | self.progressBarSetText.emit("Loading audio file...") |
| 86 | audioFileTraits = readAudioFile( | 94 | audioFileTraits = readAudioFile(self.inputFile, self) |
| 87 | self.inputFile, self | ||
| 88 | ) | ||
| 89 | if audioFileTraits is None: | 95 | if audioFileTraits is None: |
| 90 | self.cancelExport() | 96 | self.cancelExport() |
| 91 | return False | 97 | return False |
| @@ -95,25 +101,27 @@ class Worker(QtCore.QObject): | |||
| 95 | duration = getAudioDuration(self.inputFile) | 101 | duration = getAudioDuration(self.inputFile) |
| 96 | self.completeAudioArray = [] | 102 | self.completeAudioArray = [] |
| 97 | self.audioArrayLen = int( | 103 | self.audioArrayLen = int( |
| 98 | ((duration * self.hertz) + | 104 | ((duration * self.hertz) + self.hertz) - self.sampleSize |
| 99 | self.hertz) - self.sampleSize) | 105 | ) |
| 100 | return duration | 106 | return duration |
| 101 | 107 | ||
| 102 | def preFrameRender(self): | 108 | def preFrameRender(self): |
| 103 | ''' | 109 | """ |
| 104 | Initializes components that need to pre-compute stuff. | 110 | Initializes components that need to pre-compute stuff. |
| 105 | Also prerenders "static" components like text and merges them if possible | 111 | Also prerenders "static" components like text and merges them if possible |
| 106 | ''' | 112 | """ |
| 107 | self.staticComponents = {} | 113 | self.staticComponents = {} |
| 108 | 114 | ||
| 109 | # Call preFrameRender on each component | 115 | # Call preFrameRender on each component |
| 110 | canceledByComponent = False | 116 | canceledByComponent = False |
| 111 | initText = ", ".join([ | 117 | initText = ", ".join( |
| 112 | "%s) %s" % (num, str(component)) | 118 | [ |
| 113 | for num, component in enumerate(reversed(self.components)) | 119 | "%s) %s" % (num, str(component)) |
| 114 | ]) | 120 | for num, component in enumerate(reversed(self.components)) |
| 115 | print('Loaded Components:', initText) | 121 | ] |
| 116 | log.info('Calling preFrameRender for %s', initText) | 122 | ) |
| 123 | print("Loaded Components:", initText) | ||
| 124 | log.info("Calling preFrameRender for %s", initText) | ||
| 117 | for compNo, comp in enumerate(reversed(self.components)): | 125 | for compNo, comp in enumerate(reversed(self.components)): |
| 118 | try: | 126 | try: |
| 119 | comp.preFrameRender( | 127 | comp.preFrameRender( |
| @@ -122,80 +130,85 @@ class Worker(QtCore.QObject): | |||
| 122 | audioArrayLen=self.audioArrayLen, | 130 | audioArrayLen=self.audioArrayLen, |
| 123 | sampleSize=self.sampleSize, | 131 | sampleSize=self.sampleSize, |
| 124 | progressBarUpdate=self.progressBarUpdate, | 132 | progressBarUpdate=self.progressBarUpdate, |
| 125 | progressBarSetText=self.progressBarSetText | 133 | progressBarSetText=self.progressBarSetText, |
| 126 | ) | 134 | ) |
| 127 | except ComponentError: | 135 | except ComponentError: |
| 128 | log.warning( | 136 | log.warning( |
| 129 | '#%s %s encountered an error in its preFrameRender method', | 137 | "#%s %s encountered an error in its preFrameRender method", |
| 130 | compNo, | 138 | compNo, |
| 131 | comp | 139 | comp, |
| 132 | ) | 140 | ) |
| 133 | 141 | ||
| 134 | compProps = comp.properties() | 142 | compProps = comp.properties() |
| 135 | if 'error' in compProps or comp._lockedError is not None: | 143 | if "error" in compProps or comp._lockedError is not None: |
| 136 | self.cancel() | 144 | self.cancel() |
| 137 | self.canceled = True | 145 | self.canceled = True |
| 138 | canceledByComponent = True | 146 | canceledByComponent = True |
| 139 | compError = comp.error() \ | 147 | compError = ( |
| 140 | if type(comp.error()) is tuple else (comp.error(), '') | 148 | comp.error() if type(comp.error()) is tuple else (comp.error(), "") |
| 149 | ) | ||
| 141 | errMsg = ( | 150 | errMsg = ( |
| 142 | "Component #%s (%s) encountered an error!" % ( | 151 | "Component #%s (%s) encountered an error!" |
| 143 | str(compNo), comp.name | 152 | % (str(compNo), comp.name) |
| 144 | ) | 153 | if comp.error() is None |
| 145 | if comp.error() is None else | 154 | else "Export cancelled by component #%s (%s): %s" |
| 146 | 'Export cancelled by component #%s (%s): %s' % ( | 155 | % (str(compNo), comp.name, compError[0]) |
| 147 | str(compNo), | ||
| 148 | comp.name, | ||
| 149 | compError[0] | ||
| 150 | ) | ||
| 151 | ) | 156 | ) |
| 152 | log.error(errMsg) | 157 | log.error(errMsg) |
| 153 | comp._error.emit(errMsg, compError[1]) | 158 | comp._error.emit(errMsg, compError[1]) |
| 154 | break | 159 | break |
| 155 | if 'static' in compProps: | 160 | if "static" in compProps: |
| 156 | log.info('Saving static frame from #%s %s', compNo, comp) | 161 | log.info("Saving static frame from #%s %s", compNo, comp) |
| 157 | self.staticComponents[compNo] = \ | 162 | self.staticComponents[compNo] = comp.frameRender(0).copy() |
| 158 | comp.frameRender(0).copy() | ||
| 159 | 163 | ||
| 160 | # Check if any errors occured | 164 | # Check if any errors occured |
| 161 | log.debug("Checking if a component wishes to cancel the export...") | 165 | log.debug("Checking if a component wishes to cancel the export...") |
| 162 | if self.canceled: | 166 | if self.canceled: |
| 163 | if canceledByComponent: | 167 | if canceledByComponent: |
| 164 | log.error( | 168 | log.error( |
| 165 | 'Export cancelled by component #%s (%s): %s', | 169 | "Export cancelled by component #%s (%s): %s", |
| 166 | compNo, | 170 | compNo, |
| 167 | comp.name, | 171 | comp.name, |
| 168 | 'No message.' if comp.error() is None else ( | 172 | ( |
| 169 | comp.error() if type(comp.error()) is str | 173 | "No message." |
| 170 | else comp.error()[0] | 174 | if comp.error() is None |
| 171 | ) | 175 | else ( |
| 176 | comp.error() | ||
| 177 | if type(comp.error()) is str | ||
| 178 | else comp.error()[0] | ||
| 179 | ) | ||
| 180 | ), | ||
| 172 | ) | 181 | ) |
| 173 | self.cancelExport() | 182 | self.cancelExport() |
| 174 | 183 | ||
| 175 | # Merge static frames that can be merged to reduce workload | 184 | # Merge static frames that can be merged to reduce workload |
| 176 | def mergeConsecutiveStaticComponentFrames(self): | 185 | def mergeConsecutiveStaticComponentFrames(self): |
| 177 | log.info("Merging consecutive static component frames") | 186 | log.info("Merging consecutive static component frames") |
| 178 | for compNo in range(len(self.components)): | 187 | for compNo in range(len(self.components)): |
| 179 | if compNo not in self.staticComponents \ | 188 | if ( |
| 180 | or compNo + 1 not in self.staticComponents: | 189 | compNo not in self.staticComponents |
| 190 | or compNo + 1 not in self.staticComponents | ||
| 191 | ): | ||
| 181 | continue | 192 | continue |
| 182 | self.staticComponents[compNo + 1] = Image.alpha_composite( | 193 | self.staticComponents[compNo + 1] = Image.alpha_composite( |
| 183 | self.staticComponents.pop(compNo), | 194 | self.staticComponents.pop(compNo), |
| 184 | self.staticComponents[compNo + 1] | 195 | self.staticComponents[compNo + 1], |
| 185 | ) | 196 | ) |
| 186 | self.staticComponents[compNo] = None | 197 | self.staticComponents[compNo] = None |
| 198 | |||
| 187 | mergeConsecutiveStaticComponentFrames(self) | 199 | mergeConsecutiveStaticComponentFrames(self) |
| 188 | 200 | ||
| 189 | def frameRender(self, audioI): | 201 | def frameRender(self, audioI): |
| 190 | ''' | 202 | """ |
| 191 | Renders a frame composited together from the frames returned by each component | 203 | Renders a frame composited together from the frames returned by each component |
| 192 | audioI is a multiple of self.sampleSize, which can be divided to determine frameNo | 204 | audioI is a multiple of self.sampleSize, which can be divided to determine frameNo |
| 193 | ''' | 205 | """ |
| 206 | |||
| 194 | def err(): | 207 | def err(): |
| 195 | self.closePipe() | 208 | self.closePipe() |
| 196 | self.cancelExport() | 209 | self.cancelExport() |
| 197 | self.error = True | 210 | self.error = True |
| 198 | msg = 'A call to renderFrame in the video thread failed critically.' | 211 | msg = "A call to renderFrame in the video thread failed critically." |
| 199 | log.critical(msg) | 212 | log.critical(msg) |
| 200 | comp._error.emit(msg, str(e)) | 213 | comp._error.emit(msg, str(e)) |
| 201 | 214 | ||
| @@ -222,18 +235,16 @@ class Worker(QtCore.QObject): | |||
| 222 | if frame is None: # bottom-most layer | 235 | if frame is None: # bottom-most layer |
| 223 | frame = comp.frameRender(bgI) | 236 | frame = comp.frameRender(bgI) |
| 224 | else: | 237 | else: |
| 225 | frame = Image.alpha_composite( | 238 | frame = Image.alpha_composite(frame, comp.frameRender(bgI)) |
| 226 | frame, comp.frameRender(bgI) | ||
| 227 | ) | ||
| 228 | except Exception as e: | 239 | except Exception as e: |
| 229 | err() | 240 | err() |
| 230 | return frame | 241 | return frame |
| 231 | 242 | ||
| 232 | def showPreview(self, frame): | 243 | def showPreview(self, frame): |
| 233 | ''' | 244 | """ |
| 234 | Receives a final frame that will be piped to FFmpeg, | 245 | Receives a final frame that will be piped to FFmpeg, |
| 235 | adds it to the MainWindow for the live preview | 246 | adds it to the MainWindow for the live preview |
| 236 | ''' | 247 | """ |
| 237 | # We must store a reference to this QImage | 248 | # We must store a reference to this QImage |
| 238 | # or else Qt will garbage-collect it on the C++ side | 249 | # or else Qt will garbage-collect it on the C++ side |
| 239 | self.latestPreview = ImageQt(frame) | 250 | self.latestPreview = ImageQt(frame) |
| @@ -241,7 +252,7 @@ class Worker(QtCore.QObject): | |||
| 241 | 252 | ||
| 242 | @pyqtSlot() | 253 | @pyqtSlot() |
| 243 | def createVideo(self): | 254 | def createVideo(self): |
| 244 | ''' | 255 | """ |
| 245 | 1. Numpy is set to ignore division errors during this method | 256 | 1. Numpy is set to ignore division errors during this method |
| 246 | 2. Determine length of final video | 257 | 2. Determine length of final video |
| 247 | 3. Call preFrameRender on each component | 258 | 3. Call preFrameRender on each component |
| @@ -250,15 +261,14 @@ class Worker(QtCore.QObject): | |||
| 250 | 6. Iterate over the audio data array and call frameRender on the components to get frames | 261 | 6. Iterate over the audio data array and call frameRender on the components to get frames |
| 251 | 7. Close the out_pipe | 262 | 7. Close the out_pipe |
| 252 | 8. Call postFrameRender on each component | 263 | 8. Call postFrameRender on each component |
| 253 | ''' | 264 | """ |
| 254 | log.debug("Video worker received signal to createVideo") | 265 | log.debug("Video worker received signal to createVideo") |
| 255 | log.debug( | 266 | log.debug("Video thread id: {}".format(int(QtCore.QThread.currentThreadId()))) |
| 256 | 'Video thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) | 267 | numpy.seterr(divide="ignore") |
| 257 | numpy.seterr(divide='ignore') | ||
| 258 | self.encoding.emit(True) | 268 | self.encoding.emit(True) |
| 259 | self.extraAudio = [] | 269 | self.extraAudio = [] |
| 260 | self.width = int(self.settings.value('outputWidth')) | 270 | self.width = int(self.settings.value("outputWidth")) |
| 261 | self.height = int(self.settings.value('outputHeight')) | 271 | self.height = int(self.settings.value("outputHeight")) |
| 262 | 272 | ||
| 263 | # Set core.Core.canceled to False and call .reset() on each component | 273 | # Set core.Core.canceled to False and call .reset() on each component |
| 264 | self.reset() | 274 | self.reset() |
| @@ -284,16 +294,18 @@ class Worker(QtCore.QObject): | |||
| 284 | if not ffmpegCommand: | 294 | if not ffmpegCommand: |
| 285 | return | 295 | return |
| 286 | cmd = " ".join(ffmpegCommand) | 296 | cmd = " ".join(ffmpegCommand) |
| 287 | print('###### FFMPEG COMMAND ######\n%s' % cmd) | 297 | print("###### FFMPEG COMMAND ######\n%s" % cmd) |
| 288 | print('############################') | 298 | print("############################") |
| 289 | log.info(cmd) | 299 | log.info(cmd) |
| 290 | 300 | ||
| 291 | # Open pipe to FFmpeg | 301 | # Open pipe to FFmpeg |
| 292 | log.info('Opening pipe to FFmpeg') | 302 | log.info("Opening pipe to FFmpeg") |
| 293 | try: | 303 | try: |
| 294 | self.out_pipe = openPipe( | 304 | self.out_pipe = openPipe( |
| 295 | ffmpegCommand, | 305 | ffmpegCommand, |
| 296 | stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout | 306 | stdin=sp.PIPE, |
| 307 | stdout=sys.stdout, | ||
| 308 | stderr=sys.stdout, | ||
| 297 | ) | 309 | ) |
| 298 | except sp.CalledProcessError: | 310 | except sp.CalledProcessError: |
| 299 | log.critical("Out_Pipe to FFmpeg couldn't be created!", exc_info=True) | 311 | log.critical("Out_Pipe to FFmpeg couldn't be created!", exc_info=True) |
| @@ -334,7 +346,7 @@ class Worker(QtCore.QObject): | |||
| 334 | # Finished creating the video! | 346 | # Finished creating the video! |
| 335 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ | 347 | # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ |
| 336 | 348 | ||
| 337 | numpy.seterr(all='print') | 349 | numpy.seterr(all="print") |
| 338 | 350 | ||
| 339 | self.closePipe() | 351 | self.closePipe() |
| 340 | 352 | ||
| @@ -348,14 +360,14 @@ class Worker(QtCore.QObject): | |||
| 348 | except Exception: | 360 | except Exception: |
| 349 | pass | 361 | pass |
| 350 | self.progressBarUpdate.emit(0) | 362 | self.progressBarUpdate.emit(0) |
| 351 | self.progressBarSetText.emit('Export Canceled') | 363 | self.progressBarSetText.emit("Export Canceled") |
| 352 | else: | 364 | else: |
| 353 | if self.error: | 365 | if self.error: |
| 354 | self.failExport() | 366 | self.failExport() |
| 355 | else: | 367 | else: |
| 356 | print("Export Complete") | 368 | print("Export Complete") |
| 357 | self.progressBarUpdate.emit(100) | 369 | self.progressBarUpdate.emit(100) |
| 358 | self.progressBarSetText.emit('Export Complete') | 370 | self.progressBarSetText.emit("Export Complete") |
| 359 | 371 | ||
| 360 | self.error = False | 372 | self.error = False |
| 361 | self.canceled = False | 373 | self.canceled = False |
| @@ -366,21 +378,21 @@ class Worker(QtCore.QObject): | |||
| 366 | try: | 378 | try: |
| 367 | self.out_pipe.stdin.close() | 379 | self.out_pipe.stdin.close() |
| 368 | except (BrokenPipeError, OSError): | 380 | except (BrokenPipeError, OSError): |
| 369 | log.debug('Broken pipe to FFmpeg!') | 381 | log.debug("Broken pipe to FFmpeg!") |
| 370 | if self.out_pipe.stderr is not None: | 382 | if self.out_pipe.stderr is not None: |
| 371 | log.error(self.out_pipe.stderr.read()) | 383 | log.error(self.out_pipe.stderr.read()) |
| 372 | self.out_pipe.stderr.close() | 384 | self.out_pipe.stderr.close() |
| 373 | self.error = True | 385 | self.error = True |
| 374 | self.out_pipe.wait() | 386 | self.out_pipe.wait() |
| 375 | 387 | ||
| 376 | def cancelExport(self, message='Export Canceled'): | 388 | def cancelExport(self, message="Export Canceled"): |
| 377 | self.progressBarUpdate.emit(0) | 389 | self.progressBarUpdate.emit(0) |
| 378 | self.progressBarSetText.emit(message) | 390 | self.progressBarSetText.emit(message) |
| 379 | self.encoding.emit(False) | 391 | self.encoding.emit(False) |
| 380 | self.videoCreated.emit() | 392 | self.videoCreated.emit() |
| 381 | 393 | ||
| 382 | def failExport(self): | 394 | def failExport(self): |
| 383 | self.cancelExport('Export Failed') | 395 | self.cancelExport("Export Failed") |
| 384 | 396 | ||
| 385 | def updateProgress(self, pStr, pVal): | 397 | def updateProgress(self, pStr, pVal): |
| 386 | self.progressBarValue.emit(pVal) | 398 | self.progressBarValue.emit(pVal) |
