diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5f1ff3790..80de32e0e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,85 +1,171 @@
-name: Build with pyinstaller
+name: Build TiLiA
on:
workflow_dispatch:
push:
tags:
- - 'v*'
+ - "v*"
jobs:
build:
- name: Build with pyinstaller
+ name: Build with Nuitka
+ continue-on-error: true
runs-on: ${{ matrix.os }}
+ permissions:
+ contents: read
+ outputs:
+ linux-build: ${{ steps.set-path.outputs.path }}
strategy:
matrix:
- include:
- - os: windows-latest
- CMD_BUILD: pyinstaller tilia.spec
- OUT_FILE_OS: win
- OUT_FILE_EXTENSION: .exe
- ASSET_MIME: application/vnd.microsoft.portable-executable
-
- - os: macos-13
- CMD_BUILD: >
- pyinstaller tilia.spec &&
- cd dist &&
- mv tilia-$APP_VERSION-mac.app tilia-$APP_VERSION-mac-intel.app &&
- zip -r9 tilia-$APP_VERSION-mac-intel.zip tilia-$APP_VERSION-mac-intel.app
- OUT_FILE_OS: mac-intel
- OUT_FILE_EXTENSION: .zip
- ASSET_MIME: application/zip
-
- - os: macos-14
- CMD_BUILD: >
- pyinstaller tilia.spec &&
- cd dist &&
- mv tilia-$APP_VERSION-mac.app tilia-$APP_VERSION-mac-silicon.app &&
- zip -r9 tilia-$APP_VERSION-mac-silicon.zip tilia-$APP_VERSION-mac-silicon.app
- OUT_FILE_OS: mac-silicon
- OUT_FILE_EXTENSION: .zip
- ASSET_MIME: application/zip
-
- - os: ubuntu-latest
- CMD_BUILD: >
- pyinstaller tilia.spec
- OUT_FILE_OS: linux
- OUT_FILE_EXTENSION:
- ASSET_MIME: application/octet-stream
+ os: [
+ macos-latest,
+ macos-15-intel,
+ ubuntu-22.04,
+ windows-latest
+ ]
+ env:
+ QT_DEBUG_PLUGINS: 1
+ QT_QPA_PLATFORM: offscreen
steps:
- - uses: actions/checkout@v4
-
- - name: Extract package version
- id: extract_version
- shell: pwsh
- run: |
- $version = (Select-String '^version =' setup.cfg).Line -split ' = ' | Select-Object -Last 1
- echo "APP_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
-
- - name: Set output file name
- shell: pwsh
- run: |
- $out_file_name = "tilia-${{env.APP_VERSION}}-${{matrix.OUT_FILE_OS}}${{matrix.OUT_FILE_EXTENSION}}"
- echo "OUT_FILE_NAME=$out_file_name" | Out-File -FilePath $env:GITHUB_ENV -Append
+ - uses: actions/checkout@v6
- name: Set up Python 3.11
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: 3.11
- cache: 'pip'
+ cache: "pip"
+
+ - name: Setup additional Linux dependencies
+ if: runner.os == 'Linux'
+ run: |
+ chmod +x ./scripts/linux-setup.sh
+ ./scripts/linux-setup.sh
+
+ - name: Remove problematic brew libs (see Nuitka/Nuitka#2853)
+ if: runner.os == 'macOS' && runner.arch != 'ARM64'
+ run: |
+ brew remove --force --ignore-dependencies openssl@3
+ brew cleanup openssl@3
- name: Install dependencies
run: |
- python -m pip install --upgrade pip setuptools wheel
- pip install -e .
- pip install pyinstaller
+ python -m pip install --upgrade pip
+ pip install -e . --group build
- name: Build executable
+ id: build-exe
+ shell: bash
+ run: |
+ echo "python scripts/deploy.py ${{github.ref_name}} ${{matrix.os}}" | bash
+
+ - name: Test executable [mac]
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ brew install coreutils
+ echo "Checking dependencies"
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is less than 10s.
+ # then zip everything up and delete the original non-zipped file.
+ echo "Starting GUI..."
+ start=$(date +%s)
+ echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
+ echo "\n\nStarting CLI..."
+ echo "gtimeout 10 ${{ steps.build-exe.outputs.out-filepath }}/Contents/MacOS/${{ steps.build-exe.outputs.out-filename }} --user-interface=cli" | bash || true
+
+ - name: Test executable [Windows]
+ if: runner.os == 'Windows'
shell: bash
- run: ${{matrix.CMD_BUILD}}
+ run: |
+ echo "Checking dependencies"
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ echo "Starting GUI..."
+ start=$(date +%s)
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
+ echo "\n\nStarting CLI..."
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true
+
+ - name: Test executable [Linux]
+ id: set-path
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ echo "path=${{ steps.build-exe.outputs.out-filename }}" >> "$GITHUB_OUTPUT"
+ echo "Checking dependencies"
+ # test build starts up - should see the output of qt_debug_plugins. the command will kill itself in 10s if everything works. something went wrong if time taken for this command is not 10s.
+ echo "Starting GUI..."
+ start=$(date +%s)
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }}" | bash || true
+ if (($(date +%s) - $start < 10)); then
+ echo "::error::Process terminated early! Potential errors in build."
+ exit 1
+ fi
+ echo "\n\nStarting CLI..."
+ echo "timeout 10 ${{ steps.build-exe.outputs.out-filepath }} --user-interface=cli" | bash || true
- name: Upload executable
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
+ with:
+ name: ${{ steps.build-exe.outputs.out-filename }}
+ path: build/*/exe
+ retention-days: 1
+
+ deploy:
+ name: Create release
+ continue-on-error: true
+ needs: build
+ runs-on: "ubuntu-latest"
+ permissions:
+ contents: write
+ env:
+ QT_DEBUG_PLUGINS: 1
+ QT_QPA_PLATFORM: offscreen
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Download executable
+ uses: actions/download-artifact@v8
+ with:
+ path: build/
+ pattern: 'TiLiA-v*'
+ merge-multiple: true
+
+ - name: Test Linux still works in a clean environment
+ continue-on-error: true
+ id: test-linux
+ run: |
+ if [ -e build/ubuntu/exe/${{ needs.build.outputs.linux-build }} ];
+ then
+ chmod ugo+x build/ubuntu/exe/${{ needs.build.outputs.linux-build }}
+ echo "Starting GUI..."
+ start=$(date +%s)
+ echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }}" | bash || true
+ if (($(date +%s) - $start < 10));
+ then
+ echo "Linux started in a clean environment!";
+ else
+ echo "Linux didn't work... Check libraries with previous step?";
+ fi;
+ echo "Starting CLI..."
+ echo "timeout 10 build/ubuntu/exe/${{ needs.build.outputs.linux-build }} --user-interface=cli" | bash || true
+ else
+ echo "file not found!";
+ fi
+ ls build -ltraR |egrep -v '\.$|\.\.|\.:|\.\/|total|^d' | sed '/^$/d' || true
+
+ - name: Upload
+ uses: "softprops/action-gh-release@v2"
with:
- name: ${{env.OUT_FILE_NAME}}
- path: dist/${{env.OUT_FILE_NAME}}
+ tag_name: ${{github.ref_name}}
+ make_latest: ${{github.event_name == 'push'}}
+ generate_release_notes: true
+ files: |
+ build/*/exe/TiLiA-v*
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index e69a3fc6e..1c95dcb4e 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -24,8 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- # TODO: test for 3.13
- python-version: ['3.10', '3.11', '3.12']
+ python-version: ['3.10', '3.11', '3.12', '3.13']
os: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
@@ -34,27 +33,31 @@ jobs:
path: ~/Library/Caches/pip
- os: windows-latest
path: ~\AppData\Local\pip\Cache
+ exclude:
+ - os: windows-latest
+ python-version: '3.13' # no pyside support
timeout-minutes: 30
env:
- DISPLAY: ":99.0"
- QT_SELECT: "qt6"
+ QT_QPA_PLATFORM: offscreen
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- - name: Setup xvfb (Linux)
+ - name: Setup additional Linux dependencies
if: runner.os == 'Linux'
run: |
- sudo apt-get update
- sudo apt-get install -y xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xinput0 libxcb-xfixes0 libxcb-shape0 libglib2.0-0 libgl1-mesa-dev libpulse-dev
- sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev
- sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
+ chmod +x ./scripts/linux-setup.sh
+ ./scripts/linux-setup.sh
- - uses: actions/cache@v4
+ - name: Install timeout
+ if: runner.os == 'macOS'
+ run: brew install coreutils
+
+ - uses: actions/cache@v5
with:
path: ${{ matrix.path }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
@@ -64,15 +67,15 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip wheel
- pip install -e .[testing,ci-tests]
+ pip install -e . --group testing --group ci-tests
- name: List installed packages
run: |
pip freeze
- - name: Lint with flake8
+ - name: Lint with ruff
run: |
- flake8
+ ruff check
continue-on-error: true
- name: Test with pytest
@@ -82,3 +85,12 @@ jobs:
- name: Generate Coverage Report
run: |
coverage report -m
+
+ - name: Check Program
+ shell: bash
+ run: |
+ export QT_DEBUG_PLUGINS=1
+ echo "Starting GUI..."
+ timeout 10 tilia || true
+ echo "Starting CLI..."
+ timeout 10 tilia -i=cli || true
diff --git a/.gitignore b/.gitignore
index 29a3b0892..57b553bff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,7 @@
/.pytest_cache/
/build/
/dist/
-/tilia/installers/win/tilia*
/tilia.egg-info/
/venv/
__pycache__/
-pytest.ini
tilia/dev/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6237874a0..c70117428 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,20 +1,21 @@
repos:
-- repo: https://github.com/psf/black
+ - repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- - id: black
+ - id: black
-- repo: https://github.com/pre-commit/pre-commit-hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- args: ['--maxkb=1000']
+ args: ["--maxkb=1000"]
- id: debug-statements
- id: check-yaml
-- repo: https://github.com/pycqa/flake8
- rev: 7.1.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.15.0
hooks:
- - id: flake8
+ - id: ruff-check
+ args: [ --fix ]
diff --git a/.tilia.env b/.tilia.env
new file mode 100644
index 000000000..ffbaed9d8
--- /dev/null
+++ b/.tilia.env
@@ -0,0 +1,3 @@
+LOG_REQUESTS = 1
+EXCLUDE_FROM_LOG = TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_STATE_RECORD
+ENVIRONMENT='dev'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0a0335265..f38a04887 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,4 +28,11 @@ If you find a bug, [check](https://github.com/TimeLineAnnotator/desktop/issues)
# Suggesting features or enhancements
We are very much looking for ideas of new features for TiLiA. If you would like to suggest one, [check](https://github.com/TimeLineAnnotator/desktop/issues) to see if your feature has already been requested. If not, [open a new one](https://github.com/TimeLineAnnotator/desktop/issues/new) detailing your suggestion.
-You need Python 3.11 to build and test TiLiA. You will also need to have ffmpeg installed to use the export and convert audio features.
+You need Python >=3.10 to build and test TiLiA. You will also need to have ffmpeg installed to use the export and convert audio features.
+
+# Developing for TiLiA
+For a better development experience, we recommend the installation of a few more packages:
+```
+pip install --group dev --group testing
+pre-commit install
+```
diff --git a/README.md b/README.md
index f19f9dbcd..105121ea5 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,15 @@
-TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured but easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PyQt library for its GUI.
+TiLiA (TimeLine Annotator) is a GUI for producing and displaying complex annotations with video and audio files. It is a full-featured, easy-to-use set of tools for researchers and enthusiasts to better analyze their media of interest without needing to rely on textual representations (like music scores). It is written in Python, using the PySide library for its GUI.
-TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are six types of timelines, but many more are planned.
+TiLiA allows users to annotate media files primarily through timelines of various types. Each one provides different tools and enables specific annotations and visualizations. Currently, there are seven types of timelines, but many more are planned.
-Here are some examples TiLiA visualizations:
+Here are some examples of TiLiA visualizations:
- Formal analysis of the Piano Sonata in D Major, K.284:
- [First movement](https://tilia-app.com/viewer/135/)
@@ -20,12 +20,12 @@ Here are some examples TiLiA visualizations:
## Current features
-- 6 kinds of timelines
+- 7 kinds of timelines
- AudioWave: visualize audio files through bars that represent changes in amplitude
- - Beat: beat and measure markers with support to numbering
+ - Beat: beat and measure markers with support for numbering
- Harmony: Roman numeral and chord symbol labels using a specialized font, including proper display of inversion numerals, quality symbols and applied chords
- - Hierarchy: nested and leveled units organized in arbitrally complex hierarchical structures
- - Marker: simple, labeled markers to indicate discrete events
+ - Hierarchy: nested and levelled units organized in arbitrarily complex hierarchical structures
+ - Marker: simple, labelled markers to indicate discrete events
- PDF: visualize PDF files synced to playback
- Score: visualize music scores in custom, to-scale notation or conventional engraving
- Controlling playback by clicking on timeline units
@@ -41,73 +41,78 @@ Here are some examples TiLiA visualizations:
- Command-line interface (see the CLI [help page](docs/cli-tutorial.md) for more information)
## How to install and use
-TiLiA has releases for Windows, macOS and Linux. Instructions to download and run are [at the website](https://tilia-app.com/help/introduction/).
+TiLiA has releases for Windows, macOS and Linux. Instructions to download and run are [on our website](https://tilia-app.com/help/introduction/).
-Tutorials on how to use TiLiA can be found at the [website](https://tilia-app.com/help/) or in video [here](https://www.youtube.com/@tilia-app).
+Tutorials on how to use TiLiA can be found on our [website](https://tilia-app.com/help/) or in these [videos](https://www.youtube.com/@tilia-app).
## Build or run from source
-TiLiA can be also run and build from source.
+TiLiA can also be run and built from source.
### Prerequisites
Before you start, you will need:
-- Python 3.11 or later. You can get it at the [Python website](https://www.python.org/downloads/).
-- `pip` to install dependencies.
+- Python 3.10 - 3.12. Download from the [Python website](https://www.python.org/downloads/). (Support for Python 3.13 is variable due to the dependencies used.)
+-
+ Other pre-requisites
-### Note for Linux users
+ | Package | Installation | Notes |
+ | --- | --- | --- |
+ | `pip` | `python -m ensurepip --upgrade` | `pip` should come with your Python installation |
+ | `git` | Download [here](https://git-scm.com/install) | Not necessary for a direct download from this repository |
+
+
+### Note to Linux users
Users have reported dependency issues when running TiLiA on Linux (see [#370](https://github.com/TimeLineAnnotator/desktop/issues/370) and [#371](https://github.com/TimeLineAnnotator/desktop/issues/371)).
-That is probably due to `PyInstaller` not being completely compatible with `PyQt`.
-We are working to fix that with with a new build process using `pyside6-deploy` instead.
+Due to system variations between Linux distributions, some additional system dependencies may be required. Visit our [help page](https://tilia-app.com/help/installation#troubleshooting-linux) for more information.
### Running from source
-Clone TiLiA with:
+- Clone TiLiA with:
```
git clone https://github.com/TimeLineAnnotator/desktop.git tilia-desktop
```
-Change directory to the cloned repository:
+- Change directory to the cloned repository:
```
cd tilia-desktop
```
-Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
-Failure to do so is likely to cause issues with dependencies.
+> Note: We recommend using a clean [virtual environment](https://docs.python.org/3/library/venv.html) for the next steps.
+Failure to do so may cause dependency issues.
-Install python dependencies with:
+- Install TiLiA and its dependencies with:
```
pip install -e .
```
-On Linux, some additional Qt dependencies are required:
-```
-sudo apt install libnss3 libasound libxkbfile1 libpulse0
-```
-To run TiLiA from source, use:
+- To run TiLiA from source, run:
```
-python -m tilia.main
+tilia
```
-TiLiA also offers a CLI mode, which can be run with:
+- TiLiA also offers a CLI mode, which can be run with:
```
-python -m tilia.main --user-interface cli
+tilia --user-interface cli
```
+> Note: The CLI is currently only available when run from source, and not in the compiled executable.
### Building from source
-TiLiA uses [PyInstaller](https://pyinstaller.org/en/stable/) to build binaries.
-Note that the binaries will be for the platform you are building on, as `PyInstaller` supports no cross-compilation.
+TiLiA uses [Nuitka](https://nuitka.net/) to build binaries.
+Note that the binaries will be for the platform you are building on, as `Nuitka` supports no cross-compilation.
-After cloning tilia and installing the dependencies (see above), install `PyInstaller` with:
+- After cloning TiLiA, install TiLiA's run and build dependencies with:
```
-pip install pyinstaller
+pip install -e . --group build
```
-To build a stand-alone executable, run PyInstaller with the settings in `tilia.spec`:
+- To build a stand-alone executable, run the script:
```
-pyinstaller tilia.spec
+python scripts/deploy.py [ref_name] [os_type]
```
-The executable will be created in `dist` folder inside the project directory.
+> Note: `ref_name` and `os_type` are arbitrary strings that do not affect the build outcome.
+
+The executable will be found in the `build/[os_type]/exe` folder in the project directory.
## Planned features
diff --git a/TESTING.md b/TESTING.md
index 8593df908..b371ef7f5 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -1,4 +1,6 @@
The test suite is written in pytest. Below are some things to keep in my mind when writing tests. For examples of good and thorough tests, see `tests\ui\timelines\test_marker_timeline_ui.py`. Older modules should be refactored at some point to follow the guidelines below.
+## Pre-requisites
+`pip install --group testing`
## How to simulate interaction with the UI?
- The `user_actions` fixture can be used to trigger actions on the UI. This is equivalent to pressing buttons on the UI. We should also check that the actions are available in the UI where we expect them.
- The `tilia_state` fixture can be used to make certain changes to state simulating user input (e.g. `tilia_state.current_time = 10`).
diff --git a/build.py b/build.py
deleted file mode 100644
index d87a35cb2..000000000
--- a/build.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import subprocess
-
-import tilia.constants
-
-from pathlib import Path
-
-PATH_TO_INNO = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
-PATH_TO_INSTALL_SCRIPT_TEMPLATE = Path("tilia", "installers", "win", "template.iss")
-PATH_TO_INSTALL_SCRIPT = Path(
- "tilia", "installers", "win", f"tilia_{tilia.constants.VERSION}.iss"
-)
-
-
-def confirm_version_number_update():
- answer = input(
- f"Did you remember to update the version number (current version number is {tilia.constants.VERSION})? y/n "
- )
-
- if answer.lower() == "y":
- return True
- else:
- return False
-
-
-def build():
- p = subprocess.Popen("pyinstaller tilia.spec -y")
- p.wait()
-
-
-def make_installer():
- create_install_script()
-
- p = subprocess.Popen(f"{PATH_TO_INNO} {PATH_TO_INSTALL_SCRIPT.resolve()}")
- p.wait()
-
-
-def create_install_script() -> None:
- with open(PATH_TO_INSTALL_SCRIPT_TEMPLATE, "r") as f:
- template = f.read()
-
- install_script = template.replace("$VERSION$", tilia.constants.VERSION)
- install_script = install_script.replace(
- "$SOURCE_PATH$", Path("").resolve().__str__()
- )
-
- with open(PATH_TO_INSTALL_SCRIPT, "w") as f:
- f.write(install_script)
-
-
-def main() -> None:
- if not confirm_version_number_update():
- return
-
- # build()
- make_installer()
-
-
-if __name__ == "__main__":
- main()
diff --git a/pyproject.toml b/pyproject.toml
index 255236759..07a377ff1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,124 @@
[build-system]
-requires = ["setuptools>=78.1.1", "wheel"]
+requires = [
+ "Nuitka",
+ "setuptools",
+]
build-backend = "setuptools.build_meta"
-[tool.pytest.ini_options]
-env = ['QT_QPA_PLATFORM=offscreen', 'ENVIRONMENT=test']
+[project]
+name = "TiLiA"
+version = "0.5.15.0"
+description = "A GUI for creating and visualizing annotations over audio and video files."
+readme = "README.md"
+requires-python = ">=3.10,<3.14"
+dependencies = [
+ "colorama~=0.4.6",
+ "httpx~=0.28.1",
+ "isodate~=0.7.2",
+ "lxml>=5.3.0",
+ "music21>=9.1",
+ "numpy~=1.26.4",
+ "platformdirs>=4.0.0",
+ "prettytable>=3.7.0",
+ "python-dotenv>=1.0.1",
+ "pypdf>=6.0.0",
+ "PySide6>=6.8.0",
+ "PySide6_Addons>=6.8.0",
+ "PySide6_Essentials>=6.8.0",
+ "sentry-sdk>=2.21.0",
+ "soundfile~=0.13.1",
+ "TiLiA",
+ "tomli; python_version < '3.11'"
+]
+
+[[project.authors]]
+name = "Felipe Defensor et al."
+email = "tilia@tilia-app.com"
+
+[project.scripts]
+tilia = "tilia.__main__:main"
+
+[project.urls]
+Homepage = "https://tilia-app.com"
+Repository = "https://github.com/TimeLineAnnotator/desktop"
+
+[dependency-groups]
+build = [
+ "build",
+ "ImageIO; sys_platform == 'darwin'",
+ "Nuitka >= 4.0",
+ "ordered-set==4.1.0",
+ "pyyaml",
+ "setuptools<82.0.0; python_version >= '3.12'", # setuptools>82.0.0 no longer provides pkg_resources
+ "wheel==0.38.4",
+ "zstandard==0.20.0"
+]
+ci-tests = [
+ "coverage>=7.0.0",
+ "coverage-badge>=1.0.0",
+ "ruff>=0.15.0",
+ "pytest-cov>=6.0.0",
+]
+dev = [
+ "icecream>=2.1.0",
+ "pre-commit",
+]
+testing = [
+ "pytest>=8.0.0",
+ "pytest-env>=1.1.5",
+]
[tool.coverage.run]
source = ["tilia"]
omit = ["tilia/dev/*"]
+
+[tool.nuitka]
+assume-yes-for-downloads = true
+enable-plugins = ["pyside6"]
+include-qt-plugins = ["multimedia"]
+nofollow-import-to = ["*.tests", "*.test"]
+noinclude-qt-translations = true
+onefile-tempdir-spec = "{CACHE_DIR}/{PRODUCT}/v{VERSION}"
+remove-output = true
+report = "compilation-report.xml"
+user-package-configuration-file = "tilia.nuitka-package.config.yml"
+deployment = true
+mode = "app"
+
+[tool.pytest.ini_options]
+env = [
+ "ENVIRONMENT=test",
+ "QT_QPA_PLATFORM=offscreen",
+]
+
+[tool.ruff]
+exclude = ["tilia/dev"]
+line-length = 88
+lint.ignore = ["E203","E501"]
+lint.per-file-ignores = {"__init__.py"=["F401"]}
+target-version = "py310"
+
+[tool.setuptools.packages.find]
+where = ["."]
+exclude = [
+ "build", "build.*",
+ "dist",
+ "docs", "docs.*",
+ "paper", "paper.*",
+ "scripts",
+ "tests", "tests.*",
+ "venv.*", "venv"
+]
+
+[tool.setuptools.package-data]
+tilia = [
+ "../.tilia.env",
+ "../tilia.nuitka-package.config.yml", # because we build from sdist
+ "../LICENSE",
+ "ui/img/*",
+ "ui/fonts/*",
+ "media/player/youtube.html",
+ "media/player/youtube.css",
+ "parsers/score/svg_maker.html",
+ "parsers/score/timewise_to_partwise.xsl",
+]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 6b0d12491..000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-platformdirs>=4.0.0
-prettytable>=3.7.0
-lxml>=5.3.0
-music21>=9.1
-numpy~=1.26.4
-PyQt6==6.6.0
-PyQt6-Qt6==6.6.0
-PyQt6-sip==13.6.0
-PyQt6-WebEngine==6.6.0
-PyQt6-WebEngine-Qt6==6.6.0
-python-dotenv>=1.0.1
-pypdf>=6.0.0
-httpx~=0.28.1
-isodate~=0.7.2
-soundfile~=0.13.1
-colorama~=0.4.6
-
-pytest>=8.0.0
-pytest-env>=1.1.5
-icecream~=2.1.3
-colorama~=0.4.6
-sentry-sdk>=2.21.0
diff --git a/scripts/deploy.py b/scripts/deploy.py
new file mode 100644
index 000000000..10b0461d3
--- /dev/null
+++ b/scripts/deploy.py
@@ -0,0 +1,279 @@
+"""
+Script to build TiLiA with Nuitka.
+(a more flexible alternative to building with only pyside-deploy or Nuitka)
+pyside-deploy has very limited Nuitka-specific options and Nuitka requires a specific file structure to build.
+Hence this pyside-deploy-inspired script.
+
+- Run `python scripts/deploy.py [ref_name] [os_type]`
+- Creates sdist to create metadata file from information in pyproject and filter out unnecessary files
+- Then builds executable, which will be found in:
+ build/exe/[os_type]/TiLiA-[tilia version in pyproject](-[ref_name, if not the same as tilia version])-[os_type]
+"""
+
+from colorama import Fore
+import dotenv
+from enum import Enum
+from lxml import etree
+from nuitka.distutils.DistutilsCommands import build as n_build
+import os
+from pathlib import Path
+from subprocess import check_call
+import sys
+import tarfile
+import traceback
+
+
+ref_name = ""
+build_os = ""
+buildlib = Path(__file__).parents[1] / "build"
+toml_file = Path(__file__).parents[1] / "pyproject.toml"
+pkg_cfg = "tilia.nuitka-package.config.yml"
+outdir = Path()
+out_filename = ""
+
+if not toml_file.exists():
+ options = {}
+else:
+ if sys.version_info >= (3, 11):
+ from tomllib import load
+ else:
+ from tomli import load
+
+ with open(toml_file, "rb") as f:
+ options = load(f)
+
+
+class P(Enum):
+ CMD = Fore.BLUE
+ ERROR = Fore.RED
+ OK = Fore.GREEN
+
+
+def _print(text: list[str | list[str]], p_type: P | None = None):
+ if not text:
+ return
+ formatted_text = "\n".join([t.__str__() for t in text])
+ if p_type:
+ formatted_text = p_type.value + formatted_text + Fore.RESET
+ sys.stdout.write(formatted_text + "\n")
+
+
+def _handle_inputs():
+ assert len(sys.argv) == 3, "Incorrect number of inputs"
+ global ref_name, build_os, outdir
+ ref_name = sys.argv[1]
+ # in a git action runner, sys.argv[2] could look like someOS-latest, someOS-22.02, etc, where someOS is probably macos, ubuntu or windows.
+ # we save build_os as the runner os stripped of "latest" and any digits: just someOS.
+ if "macos" in sys.argv[2] and "intel" not in sys.argv[2]:
+ build_os = "macos-silicon"
+ # to identify the difference between macos-silicon and macos-intel. currently uses the images macos-latest and macos-15-intel (which are silicon and intel respectively.)
+ else:
+ build_os = "-".join(
+ [
+ x
+ for x in sys.argv[2].split("-")
+ if not x.replace(".", "", 1).isdigit() and x != "latest"
+ ]
+ )
+ outdir = buildlib / build_os
+
+
+def _get_nuitka_toml() -> list[str]:
+ toml_cmds = []
+ for option, value in options.get("tool", {}).get("nuitka", {}).items():
+ toml_cmds.extend(n_build._parseOptionsEntry(option, value))
+ return toml_cmds
+
+
+def _set_out_filename(name: str, version: str):
+ def _clean_version(v: str) -> list[str]:
+ return v.strip("v ").split(".")
+
+ global out_filename
+ if _clean_version(version) == _clean_version(ref_name):
+ out_filename = f"{name}-v{version}-{build_os}"
+ else:
+ out_filename = f"{name}-v{version}-{ref_name}-{build_os}"
+
+
+def _get_exe_cmd() -> list[str]:
+ name = options.get("project", {}).get("name", "TiLiA")
+ version = options.get("project", {}).get("version", "0")
+ _set_out_filename(name, version)
+ icon_path = Path(__file__).parents[1] / "tilia" / "ui" / "img" / "main_icon.ico"
+ exe_args = [
+ sys.executable,
+ "-m",
+ "nuitka",
+ f"--output-dir={outdir}/exe",
+ f"--product-name={name}",
+ f"--file-version={version}",
+ f"--output-filename={out_filename}",
+ f"--macos-app-icon={icon_path}",
+ "--macos-app-mode=gui",
+ f"--macos-app-version={version}",
+ "--windows-console-mode=attach",
+ f"--windows-icon-from-ico={icon_path}",
+ f"--linux-icon={icon_path}",
+ ]
+
+ return exe_args
+
+
+def _get_sdist() -> Path:
+ for f in outdir.iterdir():
+ if "".join(f.suffixes[-2:]) == ".tar.gz":
+ return f
+ raise Exception(f"Could not find sdist in {outdir}:", [*outdir.iterdir()])
+
+
+def _get_main_file() -> Path:
+ main = _create_lib()
+ _update_yml()
+ return main
+
+
+def _create_lib() -> Path:
+ sdist = _get_sdist()
+ base = ".".join(sdist.name.split(".")[:-2])
+ tilia = f"{base}/tilia"
+ lib = outdir / "Lib"
+
+ ext_data = [
+ f"{base}/{e[3:]}"
+ for e in options.get("tool", {})
+ .get("setuptools", {})
+ .get("package-data", {})
+ .get("tilia", [])
+ if e.split("/")[0] == ".."
+ ]
+
+ with tarfile.open(sdist) as f:
+ main = f"{tilia}/__main__.py"
+ assert main in f.getnames(), f"Could not locate {main}"
+ f.extractall(
+ lib,
+ filter=lambda x, _: (
+ x
+ if x.name.startswith(tilia)
+ or x.name.startswith(f"{base}/TiLiA.egg-info")
+ or x.name in ext_data
+ else None
+ ),
+ )
+
+ os.chdir(lib / base)
+ return lib / tilia
+
+
+def _update_yml():
+ if not Path(pkg_cfg).exists():
+ return
+
+ import yaml
+
+ with open(pkg_cfg) as f:
+ yml = yaml.safe_load(f)
+
+ yml.append(
+ {
+ "module-name": "tilia",
+ "data-files": [
+ {
+ "patterns": [
+ e
+ for e in options.get("tool", {})
+ .get("setuptools", {})
+ .get("package-data", {})
+ .get("tilia", [])
+ if pkg_cfg not in e
+ ]
+ },
+ {"include-metadata": ["TiLiA"]},
+ ],
+ }
+ )
+
+ with open(pkg_cfg, "w") as f:
+ yaml.dump(yml, f)
+
+
+def _build_sdist():
+ sdist_cmd = [
+ sys.executable,
+ "-m",
+ "build",
+ "--no-isolation",
+ "--verbose",
+ "--sdist",
+ f"--outdir={outdir.as_posix()}",
+ ]
+
+ _print(["Building sdist with command:", sdist_cmd], P.CMD)
+ check_call(sdist_cmd)
+
+
+def _build_exe():
+ main_file = _get_main_file()
+ exe_cmd = _get_exe_cmd()
+ exe_cmd.extend(_get_nuitka_toml())
+ exe_cmd.append(main_file.as_posix())
+
+ _print(["Building exe with command:", exe_cmd], P.CMD)
+ check_call(exe_cmd)
+ _print(["Build complete!"], P.OK)
+ _print(["Compilation report:"])
+ _print(
+ [
+ etree.tostring(
+ etree.parse(main_file.parent / "compilation-report.xml"),
+ pretty_print=True,
+ encoding=str,
+ )
+ ]
+ )
+
+
+def build():
+ _handle_inputs()
+ old_env_var = dotenv.dotenv_values(".tilia.env").get("ENVIRONMENT", "")
+ dotenv.set_key(".tilia.env", "ENVIRONMENT", "prod")
+ if buildlib.exists():
+ _print(["Cleaning build folder..."], P.ERROR)
+ for root, dirs, files in os.walk(buildlib, False):
+ r = Path(root)
+ _print([f"\t~{r}"])
+ for f in files:
+ os.unlink(r / f)
+ for d in dirs:
+ os.rmdir(r / d)
+ os.rmdir(buildlib)
+
+ old_dir = os.getcwd()
+ try:
+ _build_sdist()
+ _build_exe()
+ if os.environ.get("GITHUB_OUTPUT"):
+ if "mac" in build_os:
+ os.rename(
+ outdir / "exe" / "tilia.app",
+ outdir / "exe" / (out_filename + ".app"),
+ )
+ out_filepath = outdir / "exe" / (out_filename + ".app")
+ else:
+ out_filepath = outdir / "exe" / out_filename
+ with open(os.environ["GITHUB_OUTPUT"], "a") as f:
+ f.write(f"out-filepath={out_filepath.as_posix()}\n")
+ f.write(f"out-filename={out_filename}\n")
+ os.chdir(old_dir)
+ dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var)
+ except Exception as e:
+ _print(["Build failed!", e.__str__()], P.ERROR)
+ _print([traceback.format_exc()])
+ os.chdir(old_dir)
+ dotenv.set_key(".tilia.env", "ENVIRONMENT", old_env_var)
+ raise SystemExit(1) from e
+
+
+if __name__ == "__main__":
+ build()
diff --git a/scripts/linux-setup.sh b/scripts/linux-setup.sh
new file mode 100644
index 000000000..4effae2be
--- /dev/null
+++ b/scripts/linux-setup.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+
+echo "Setup Linux build environment"
+trap 'echo Setup failed; exit 1' ERR
+
+df -h .
+
+##########################################################################
+# GET DEPENDENCIES
+##########################################################################
+
+apt_packages=(
+ coreutils
+ curl
+ gawk
+ git
+ lcov
+ libasound2-dev
+ libcups2-dev
+ libfontconfig1-dev
+ libfreetype6-dev
+ libgcrypt20-dev
+ libgl1-mesa-dev
+ libglib2.0-dev
+ libjack-dev
+ libnss3-dev
+ libportmidi-dev
+ libpulse-dev
+ librsvg2-dev
+ libsndfile1-dev
+ libssl-dev
+ libtool
+ make
+ p7zip-full
+ sed
+ unzip
+ wget
+ )
+
+apt_packages_runtime=(
+ # Alphabetical order please!
+ libdbus-1-3
+ libegl1-mesa-dev
+ libgles2-mesa-dev
+ libodbc2
+ libpq-dev
+ libssl-dev
+ libxcomposite-dev
+ libxcursor-dev
+ libxi-dev
+ libxkbcommon-x11-0
+ libxrandr2
+ libxtst-dev
+ libdrm-dev
+ libxcb-cursor-dev
+ libxcb-icccm4
+ libxcb-image0
+ libxcb-keysyms1
+ libxcb-randr0
+ libxcb-render-util0
+ libxcb-xinerama0
+ libxcb-xkb-dev
+ libxkbcommon-dev
+ libopengl-dev
+ libvulkan-dev
+ )
+
+apt_packages_ffmpeg=(
+ ffmpeg
+ libavcodec-dev
+ libavformat-dev
+ libswscale-dev
+ )
+
+apt_packages_pw_deps=(
+ libdbus-1-dev
+ libudev-dev
+ )
+
+sudo apt-get update
+sudo apt-get install -y --no-install-recommends \
+ "${apt_packages[@]}" \
+ "${apt_packages_runtime[@]}" \
+ "${apt_packages_ffmpeg[@]}" \
+ "${apt_packages_pw_deps[@]}"
+
+##########################################################################
+# PIPEWIRE
+##########################################################################
+
+sudo apt-get install pipewire-media-session- wireplumber
+
+systemctl --user --now enable wireplumber.service
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index dc55445e6..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,46 +0,0 @@
-[metadata]
-name = TiLiA
-version = 0.5.15
-description = A GUI for creating and visualizing annotations over audio and video files.
-author = Felipe Defensor et al.
-
-[options]
-packages = tilia
-install_requires =
- lxml>=5.3.0
- platformdirs>=4.0.0
- prettytable>=3.7.0
- music21>=9.1
- numpy~=1.26.4
- PyQt6==6.6.0
- PyQt6-Qt6==6.6.0
- PyQt6-sip==13.6.0
- PyQt6-WebEngine==6.6.0
- PyQt6-WebEngine-Qt6==6.6.0
- python-dotenv>=1.0.1
- pypdf~=4.2.0
- sentry-sdk>=2.21.0
- soundfile~=0.13.1
- httpx~=0.28.1
- isodate~=0.7.2
- colorama~=0.4.6
-python_requires = >=3.10
-zip_safe = no
-
-[options.extras_require]
-testing =
- pytest>=8.0.0
- pytest-env>=1.1.5
- colorama>=0.4.6
- icecream>=2.1.0
-ci-tests =
- coverage>=7.0.0
- coverage-badge>=1.0.0
- flake8>=7.0.0
- pytest-cov>=6.0.0
-
-[flake8]
-max-line-length = 88
-per-file-ignores = __init__.py: F401
-extend-ignore = E203,E501
-exclude = tilia/dev
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 7f1a1763c..000000000
--- a/setup.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from setuptools import setup
-
-if __name__ == "__main__":
- setup()
diff --git a/tests/conftest.py b/tests/conftest.py
index 24ab48ce5..b145a8503 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,16 +3,16 @@
from pathlib import Path
from typing import Literal
-import dotenv
import pytest
-from PyQt6.QtCore import QSettings
-from PyQt6.QtWidgets import QApplication
+from PySide6.QtCore import QSettings
+from PySide6.QtWidgets import QApplication
from colorama import Fore, Style
+import tilia.utils # noqa: F401
+
import tilia.constants as constants_module
import tilia.log as logging_module
import tilia.settings as settings_module
-from tilia.dirs import PROJECT_ROOT
from tilia.media.player.base import MediaTimeChangeReason
from tilia.app import App
from tilia.boot import setup_logic
@@ -51,9 +51,6 @@
"tests.timelines.score.fixtures",
]
-dotenv_path = PROJECT_ROOT / ".env"
-success = dotenv.load_dotenv(dotenv_path)
-
class TiliaErrors:
def __init__(self):
@@ -199,7 +196,7 @@ def resources() -> Path:
@pytest.fixture(scope="module")
def use_test_settings(qapplication):
settings_module.settings._settings = QSettings(
- constants_module.APP_NAME, "DesktopTests"
+ constants_module.APP_NAME, "DesktopTests", None
)
settings_module.settings._check_all_default_settings_present()
settings_module.settings.set("general", "prioritise_performance", True)
diff --git a/tests/mock.py b/tests/mock.py
index af1589495..a884c1cba 100644
--- a/tests/mock.py
+++ b/tests/mock.py
@@ -2,7 +2,7 @@
from typing import Any, Sequence
from unittest.mock import patch, Mock
-from PyQt6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
+from PySide6.QtWidgets import QFileDialog, QMessageBox, QInputDialog
from tilia.requests import Get, Post, serve, server, stop_serving
from tilia.requests import post as post_original
@@ -51,8 +51,8 @@ def __exit__(self, exc_type, exc_val, exc_tb):
def _callback(self, *_, **__):
try:
return_value = self.return_values[self.return_count]
- except IndexError:
- raise IndexError("Not enough return values to serve")
+ except IndexError as e:
+ raise IndexError("Not enough return values to serve") from e
self.return_count += 1
return return_value
@@ -142,6 +142,6 @@ def patch_ask_for_string_dialog(success: bool | list[bool], string: str | list[s
else:
raise ValueError("input must be a string if success is a bool.")
- return_values = list(zip(string, success))
- with (patch.object(QInputDialog, "getText", side_effect=return_values),):
+ return_values = list(zip(string, success, strict=True))
+ with patch.object(QInputDialog, "getText", side_effect=return_values):
yield
diff --git a/tests/parsers/csv/test_harmony_from_csv.py b/tests/parsers/csv/test_harmony_from_csv.py
index efe814ddc..e090779e8 100644
--- a/tests/parsers/csv/test_harmony_from_csv.py
+++ b/tests/parsers/csv/test_harmony_from_csv.py
@@ -82,7 +82,7 @@ def test_fails_without_a_required_column(self, required_attr, harmony_tl):
success, errors = call_patched_import_by_time_func(harmony_tl, data)
assert_in_errors(required_attr, errors)
- def test_returns_error_for_invalid_rows_and_processess_valid_rows(self, harmony_tl):
+ def test_returns_error_for_invalid_rows_and_processes_valid_rows(self, harmony_tl):
data = "\n".join(
[
"time,harmony_or_key,symbol",
diff --git a/tests/parsers/csv/test_markers_from_csv.py b/tests/parsers/csv/test_markers_from_csv.py
index 722d8a4a9..9a12d13d3 100644
--- a/tests/parsers/csv/test_markers_from_csv.py
+++ b/tests/parsers/csv/test_markers_from_csv.py
@@ -3,7 +3,7 @@
from typing import Literal
from unittest.mock import patch, mock_open
-from PyQt6.QtWidgets import QFileDialog
+from PySide6.QtWidgets import QFileDialog
from tests.utils import undoable
from tilia.requests import Post, post
diff --git a/tests/parsers/score/test_score_from_musicxml.py b/tests/parsers/score/test_score_from_musicxml.py
index b63fc4ed6..f4cae4fdf 100644
--- a/tests/parsers/score/test_score_from_musicxml.py
+++ b/tests/parsers/score/test_score_from_musicxml.py
@@ -1,6 +1,6 @@
from unittest.mock import patch
-from PyQt6.QtWidgets import QMessageBox
+from PySide6.QtWidgets import QMessageBox
from tilia.parsers.score.musicxml import notes_from_musicXML
from tilia.timelines.component_kinds import ComponentKind
diff --git a/tests/player/test_player.py b/tests/player/test_player.py
index 1e16c61fd..5c2e39e13 100644
--- a/tests/player/test_player.py
+++ b/tests/player/test_player.py
@@ -11,7 +11,7 @@ def conservative_player_stop(tilia):
"""
Increases player.SLEEP_AFTER_STOP to 5 seconds if on CI.
Avoids freezes when setting URL after stop. Workaround for running tests on CI.
- Proper hadling of player status changes would be a more robust solution.
+ Proper handling of player status changes would be a more robust solution.
"""
original_sleep_after_stop = tilia.player.SLEEP_AFTER_STOP
diff --git a/tests/test_app.py b/tests/test_app.py
index b8dab9b21..a1be29f61 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -28,7 +28,7 @@
class TestLogger:
def test_sentry_not_logging(self):
# TODO: make this test run first in batch testing.
- # enabling sentry during tests will fill inbox up unneccesarily
+ # enabling sentry during tests will fill inbox up unnecessarily
assert "tilia.log" in tilia.log.sentry_sdk.integrations.logging._IGNORED_LOGGERS
@@ -493,7 +493,7 @@ def test_open_without_saving_changes(self, tilia, tls, marker_tlui, tmp_path):
assert len(tls) == 2 # assert load was successful
assert len(list(contents["timelines"][str(prev_tl_id)]["components"])) == 0
- def test_open_canceling_should_save_changes_dialog(
+ def test_open_cancelling_should_save_changes_dialog(
self, tilia, tls, marker_tlui, tmp_path
):
previous_path = tmp_path / "previous.tla"
@@ -584,7 +584,7 @@ def test_all_windows_are_closed(self, tilia, qtui):
with Serve(Get.FROM_USER_SHOULD_SAVE_CHANGES, (True, False)):
commands.execute("file.new")
- # this doesn't actaully check if windows are closed
+ # this doesn't actually check if windows are closed
# it checks if app._windows[kind] is None.
# Those should be equivalent, if everything is working as it should
assert not any(qtui.is_window_open(k) for k in WindowKind)
@@ -645,7 +645,7 @@ def test_moving_files(self, tla, media, tilia, qtui, tmp_path):
new_media = old_media.rename(new_folder / media)
# open file at new folder
- with (patch_file_dialog(True, [str(new_tla)])):
+ with patch_file_dialog(True, [str(new_tla)]):
commands.execute("file.open")
assert tilia.player.media_path == str(new_media)
diff --git a/tests/test_constants.py b/tests/test_constants.py
new file mode 100644
index 000000000..ccee02720
--- /dev/null
+++ b/tests/test_constants.py
@@ -0,0 +1,24 @@
+import importlib.metadata
+import sys
+from unittest.mock import patch
+
+
+def test_tilia_metadata_not_found():
+ # If tilia.constants was already imported, we must remove it from sys.modules
+ # to ensure the module-level code runs again with our mocks.
+ if "tilia.constants" in sys.modules:
+ del sys.modules["tilia.constants"]
+
+ # 1. Mock Path.exists to return False so it skips the pyproject.toml logic
+ with patch("pathlib.Path.exists", return_value=False), patch(
+ "importlib.metadata.metadata",
+ side_effect=importlib.metadata.PackageNotFoundError,
+ ):
+
+ import tilia.constants as constants
+
+ assert constants.APP_NAME == ""
+ assert constants.VERSION == "0.0.0"
+ assert constants.YEAR == "2022-2026"
+ assert constants.AUTHOR == ""
+ assert constants.EMAIL == ""
diff --git a/tests/timelines/base/test_validators.py b/tests/timelines/base/test_validators.py
index f38af83a1..3a41e574c 100644
--- a/tests/timelines/base/test_validators.py
+++ b/tests/timelines/base/test_validators.py
@@ -1,4 +1,4 @@
-from PyQt6.QtGui import QColor
+from PySide6.QtGui import QColor
from tilia.timelines.base.validators import validate_color
diff --git a/tests/ui/cli/test_load_media.py b/tests/ui/cli/test_load_media.py
index ca1cb9b3d..949cb8912 100644
--- a/tests/ui/cli/test_load_media.py
+++ b/tests/ui/cli/test_load_media.py
@@ -92,7 +92,9 @@ def test_with_timelines_scale_not_provided_answer_yes_but_dont_confirm_crop(
assert marker_tl[0].get_data("time") == 50 * EXAMPLE_MEDIA_SCALE_FACTOR
-def test_with_timelines_scale_not_provied_answer_crop(cli, tilia_state, marker_tl):
+def test_with_timelines_scale_not_provided_answer_crop(
+ cli, tilia_state, marker_tl
+):
for time in [5, 50]:
marker_tl.create_marker(time)
diff --git a/tests/ui/dialogs/test_crash.py b/tests/ui/dialogs/test_crash.py
index e75c680a5..f1261247d 100644
--- a/tests/ui/dialogs/test_crash.py
+++ b/tests/ui/dialogs/test_crash.py
@@ -40,7 +40,7 @@ def test_crash_support_empty_fields(self):
set_user.assert_not_called()
- def test_crash_support_remmeber(self):
+ def test_crash_support_remember(self):
crash_support_dialog = CrashSupportDialog(None)
crash_support_dialog.email_field.setText("an invalid email")
crash_support_dialog.name_field.setText("John Doe")
@@ -51,7 +51,7 @@ def test_crash_support_remmeber(self):
set_user.assert_called_once_with("", "John Doe")
- def test_crash_support_not_remmeber(self):
+ def test_crash_support_not_remember(self):
crash_support_dialog = CrashSupportDialog(None)
crash_support_dialog.email_field.setText("an invalid email")
crash_support_dialog.name_field.setText("John Doe")
diff --git a/tests/ui/timelines/harmony/test_harmony_ui.py b/tests/ui/timelines/harmony/test_harmony_ui.py
index bb371e70b..a1861db81 100644
--- a/tests/ui/timelines/harmony/test_harmony_ui.py
+++ b/tests/ui/timelines/harmony/test_harmony_ui.py
@@ -79,7 +79,7 @@ def test_paste_single_into_element(self, tlui):
click_harmony_ui(tlui[1])
commands.execute("timeline.component.paste")
assert len(tlui) == 2
- for attr, value in attributes_to_copy.items():
+ for attr in attributes_to_copy.keys():
assert tlui[1].get_data(attr) == attributes_to_copy[attr]
def test_paste_multiple_into_element(self, tlui):
@@ -117,5 +117,5 @@ def test_paste_multiple_into_element(self, tlui):
click_harmony_ui(target_hui)
commands.execute("timeline.component.paste")
assert len(tlui) == 6
- for attr, value in attributes_to_copy.items():
+ for attr in attributes_to_copy.keys():
assert target_hui.get_data(attr) == attributes_to_copy[attr]
diff --git a/tests/ui/timelines/harmony/test_mode_ui.py b/tests/ui/timelines/harmony/test_mode_ui.py
index 0fbadcd51..651898711 100644
--- a/tests/ui/timelines/harmony/test_mode_ui.py
+++ b/tests/ui/timelines/harmony/test_mode_ui.py
@@ -71,7 +71,7 @@ def test_paste_single_into_element(self, tlui):
click_mode_ui(tlui[1])
commands.execute("timeline.component.paste")
assert len(tlui) == 2
- for attr, value in attributes_to_copy.items():
+ for attr in attributes_to_copy.keys():
assert tlui[1].get_data(attr) == attributes_to_copy[attr]
def test_paste_multiple_into_element(self, tlui):
@@ -101,5 +101,5 @@ def test_paste_multiple_into_element(self, tlui):
click_mode_ui(target_mui)
commands.execute("timeline.component.paste")
assert len(tlui) == 6
- for attr, value in attributes_to_copy.items():
+ for attr in attributes_to_copy.keys():
assert target_mui.get_data(attr) == attributes_to_copy[attr]
diff --git a/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py b/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py
index ca11860e8..2c48a54b8 100644
--- a/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py
+++ b/tests/ui/timelines/hierarchy/test_hierarchy_timeline_ui.py
@@ -1,5 +1,5 @@
import pytest
-from PyQt6.QtGui import QColor
+from PySide6.QtGui import QColor
from tests.mock import Serve, patch_yes_or_no_dialog
from tilia.requests import Post, Get, post
diff --git a/tests/ui/timelines/hierarchy/test_hierarchy_ui.py b/tests/ui/timelines/hierarchy/test_hierarchy_ui.py
index 2c3f29e71..57c16e2ab 100644
--- a/tests/ui/timelines/hierarchy/test_hierarchy_ui.py
+++ b/tests/ui/timelines/hierarchy/test_hierarchy_ui.py
@@ -30,7 +30,7 @@ def test_full_name_no_label(self, tlui):
assert (
tlui[0].full_name
- == "tl" + HierarchyUI.FULL_NAME_SEPARATOR + HierarchyUI.NAME_WHEN_UNLABELED
+ == "tl" + HierarchyUI.FULL_NAME_SEPARATOR + HierarchyUI.NAME_WHEN_UNLABELLED
)
def test_full_name_with_parent(self, tlui):
diff --git a/tests/ui/timelines/interact.py b/tests/ui/timelines/interact.py
index 09ed9cbe3..7e7ee6724 100644
--- a/tests/ui/timelines/interact.py
+++ b/tests/ui/timelines/interact.py
@@ -1,8 +1,8 @@
from typing import Literal
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QGraphicsView, QGraphicsItem, QApplication
-from PyQt6.QtTest import QTest
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QGraphicsView, QGraphicsItem, QApplication
+from PySide6.QtTest import QTest
from tilia.requests import Post, post, get, Get
from tilia.ui.coords import time_x_converter
@@ -92,8 +92,8 @@ def press_key(key: str, modifier: Qt.KeyboardModifier | None = None):
if len(key) > 1:
try:
key = getattr(Qt.Key, f"Key_{key}")
- except AttributeError:
- raise ValueError(f"Unknown key: {key}")
+ except AttributeError as e:
+ raise ValueError(f"Unknown key: {key}") from e
QTest.keyClick(
get_focused_widget(), key, modifier=modifier or Qt.KeyboardModifier.NoModifier
diff --git a/tests/ui/timelines/marker/test_marker_timeline_ui.py b/tests/ui/timelines/marker/test_marker_timeline_ui.py
index 17faa1b15..b8160fc31 100644
--- a/tests/ui/timelines/marker/test_marker_timeline_ui.py
+++ b/tests/ui/timelines/marker/test_marker_timeline_ui.py
@@ -1,8 +1,8 @@
from unittest.mock import patch
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QColor
-from PyQt6.QtWidgets import QColorDialog, QInputDialog
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import QColorDialog, QInputDialog
from tests.mock import Serve, patch_ask_for_string_dialog, patch_yes_or_no_dialog
from tests.ui.test_qtui import get_toolbars_of_class
diff --git a/tests/ui/timelines/marker/test_marker_ui.py b/tests/ui/timelines/marker/test_marker_ui.py
index ea649e284..8cbe1eeed 100644
--- a/tests/ui/timelines/marker/test_marker_ui.py
+++ b/tests/ui/timelines/marker/test_marker_ui.py
@@ -1,7 +1,7 @@
from unittest.mock import patch, Mock
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QColor
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor
from tests.mock import PatchPost
from tilia.requests import Post
diff --git a/tests/ui/timelines/score/test_musicxml.py b/tests/ui/timelines/score/test_musicxml.py
index 1fefb500f..39cddace4 100644
--- a/tests/ui/timelines/score/test_musicxml.py
+++ b/tests/ui/timelines/score/test_musicxml.py
@@ -13,7 +13,7 @@ def setup_valid_beats(self, beat_tl):
beat_tl.recalculate_measures()
- def test_user_accpets(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state):
+ def test_user_accepts(self, qtui, score_tlui, beat_tl, tmp_path, tilia_state):
self.setup_valid_beats(beat_tl)
with patch_yes_or_no_dialog(True):
diff --git a/tests/ui/timelines/score/test_score_timeline_ui.py b/tests/ui/timelines/score/test_score_timeline_ui.py
index 9351c7517..e3b477c2f 100644
--- a/tests/ui/timelines/score/test_score_timeline_ui.py
+++ b/tests/ui/timelines/score/test_score_timeline_ui.py
@@ -151,7 +151,7 @@ def test_missing_staff_deletes_timeline(qtui, tls, tilia_errors, tmp_path):
"staff_index": 0,
"color": None,
"comments": "",
- "" "display_accidental": False,
+ "display_accidental": False,
"kind": "NOTE",
"hash": "",
},
@@ -235,7 +235,7 @@ def test_symbol_staff_collision(qtui, tmp_path):
json.dumps(file_data_with_symbols), encoding="utf-8"
)
- with (patch_file_dialog(True, [tmp_file_with_symbols])):
+ with patch_file_dialog(True, [tmp_file_with_symbols]):
commands.execute("file.open")
score = get(Get.TIMELINE_UI_BY_ATTR, "TIMELINE_KIND", TimelineKind.SCORE_TIMELINE)
diff --git a/tests/ui/timelines/test_timelineui_collection.py b/tests/ui/timelines/test_timelineui_collection.py
index 9896ed1f2..8edb37889 100644
--- a/tests/ui/timelines/test_timelineui_collection.py
+++ b/tests/ui/timelines/test_timelineui_collection.py
@@ -222,7 +222,7 @@ def test_by_page_is_triggered(self, tluis):
move_to_x_mock.assert_called()
center_on_time_mock.assert_not_called()
- def test_by_page_is_not_triggered_when_not_over_treshold(self, tluis):
+ def test_by_page_is_not_triggered_when_not_over_threshold(self, tluis):
self._set_auto_scroll(ScrollType.BY_PAGE)
with patch.object(tluis.view, "move_to_x") as move_to_x_mock:
post(Post.PLAYER_CURRENT_TIME_CHANGED, 10, MediaTimeChangeReason.PLAYBACK)
diff --git a/tests/ui/windows/test_manage_timelines.py b/tests/ui/windows/test_manage_timelines.py
index 2ee34c3a8..3c246718c 100644
--- a/tests/ui/windows/test_manage_timelines.py
+++ b/tests/ui/windows/test_manage_timelines.py
@@ -1,8 +1,9 @@
+from contextlib import contextmanager
from typing import Literal
import pytest
-from PyQt6.QtCore import Qt
-from PyQt6.QtTest import QTest
+from PySide6.QtCore import Qt
+from PySide6.QtTest import QTest
from tests.mock import Serve
from tilia.requests import Get, get
@@ -14,23 +15,33 @@
def assert_order_is_correct(tls: Timelines, expected: list[Timeline]):
# assert timeline order
- for tl, expected in zip(sorted(tls), expected):
- assert tl == expected
+ for tl, e in zip(sorted(tls), expected, strict=True):
+ assert tl == e
# assert list widget order
for i, tl in enumerate(expected):
tlui = get(Get.TIMELINE_UI, tl.id)
- assert ManageTimelines().list_widget.item(i).timeline_ui == tlui
+ with manage_timelines() as mt:
+ assert mt.list_widget.item(i).timeline_ui == tlui
+
+
+@contextmanager
+def manage_timelines():
+ """Context manager for the ManageTimelines window."""
+ mt = ManageTimelines()
+ try:
+ yield mt
+ finally:
+ mt.close()
class TestChangeTimelineVisibility:
@staticmethod
def toggle_timeline_is_visible(row: int = 0):
"""Toggles timeline visibility using the Manage Timelines window."""
- mt = ManageTimelines()
- mt.list_widget.setCurrentRow(row)
- QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton)
- mt.close()
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(row)
+ QTest.mouseClick(mt.checkbox, Qt.MouseButton.LeftButton)
def test_hide(self, marker_tlui):
commands.execute("timeline.set_is_visible", marker_tlui, True)
@@ -64,17 +75,16 @@ def setup_timelines(self, tluis, tls):
@staticmethod
def click_set_ordinal_button(button: Literal["up", "down"], row: int):
"""Toggles timeline visibility using the ManageTimelines window."""
- mt = ManageTimelines()
- mt.list_widget.setCurrentRow(row)
- if button == "up":
- button = mt.up_button
- elif button == "down":
- button = mt.down_button
- else:
- assert False, "Invalid button value."
-
- QTest.mouseClick(button, Qt.MouseButton.LeftButton)
- mt.close()
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(row)
+ if button == "up":
+ button = mt.up_button
+ elif button == "down":
+ button = mt.down_button
+ else:
+ assert False, "Invalid button value."
+
+ QTest.mouseClick(button, Qt.MouseButton.LeftButton)
def test_increase_ordinal(self, tls, setup_timelines):
tl0, tl1, tl2 = setup_timelines
@@ -141,3 +151,13 @@ def test_decrease_ordinal_with_last_selected_does_nothing(
self.click_set_ordinal_button("down", 2)
assert_order_is_correct(tls, [tl0, tl1, tl2])
+
+
+class TesttimelinesChangeWhileOpen:
+ def test_timeline_is_deleted(self, tluis):
+ commands.execute("timelines.add.marker", name="")
+ with manage_timelines() as mt:
+ mt.list_widget.setCurrentRow(0)
+ commands.execute("timeline.delete", tluis[0], confirm=False)
+
+ # Much more could be tested here.
diff --git a/tests/utils.py b/tests/utils.py
index e5c29edff..9ff041d82 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -5,7 +5,7 @@
from pprint import pformat
from typing import Callable
-from PyQt6.QtWidgets import QMenu
+from PySide6.QtWidgets import QMenu
from tilia.requests import get, Get, Post, post
from tests.mock import patch_file_dialog
diff --git a/tilia.nuitka-package.config.yml b/tilia.nuitka-package.config.yml
new file mode 100644
index 000000000..6c5353d8c
--- /dev/null
+++ b/tilia.nuitka-package.config.yml
@@ -0,0 +1,59 @@
+# see https://nuitka.net/user-documentation/nuitka-package-config.html for help on this file.
+# yamllint disable rule:line-length
+# yamllint disable rule:indentation
+# yamllint disable rule:comments-indentation
+# yamllint disable rule:comments
+# too many spelling things, spell-checker: disable
+---
+- module-name: "music21" # mostly regex matching entire refrences to tests and deleting.
+ anti-bloat:
+ - description: "remove tests"
+ replacements_plain:
+ "'mainTest',": ""
+ "'test',": ""
+ "from music21.test.testRunner import mainTest": ""
+ "from music21 import test": ""
+ - description: "remove module tests"
+ global_replacements_re:
+ '(?m)class Test([^(]*)\(unittest.TestCase\):[\d\D]*?(?=^if|^# -|^class|^_[A-Z]|\Z)': ""
+ "if __name__ == '__main__':([\\d|\\D]*)": ""
+ "from music21.(.*) import test(.*)": ""
+ global_replacements_plain:
+ "import unittest": ""
+
+- module-name: "executing._pytest_utils"
+ anti-bloat:
+ - description: "remove pytest reference"
+ change_function:
+ "is_pytest_compatible": "'(lambda : False)'"
+ when: "not use_pytest"
+
+- module-name: "sentry_sdk.integrations"
+ implicit-imports:
+ - depends:
+ - "sentry_sdk.integrations.argv"
+ - "sentry_sdk.integrations.atexit"
+ - "sentry_sdk.integrations.dedupe"
+ - "sentry_sdk.integrations.excepthook"
+ - "sentry_sdk.integrations.logging"
+ - "sentry_sdk.integrations.modules"
+ - "sentry_sdk.integrations.stdlib"
+ - "sentry_sdk.integrations.threading"
+ anti-bloat:
+ - description: "fix import of unused integrations"
+ replacements_plain:
+ "auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS,": "auto_enabling_integrations=[],"
+
+- module-name: "tilia.utils"
+ anti-bloat:
+ - description: "slightly hacky way of forcing prod if env file is not found"
+ replacements_plain:
+ 'os.environ["ENVIRONMENT"] = "dev"': 'os.environ["ENVIRONMENT"] = "prod"'
+
+- module-name: "tilia.boot"
+ anti-bloat:
+ - description: "CLI doesn't currently work in the executable"
+ replacements_re:
+ 'elif interface == "cli":(\n|.)*?return CLI\(\)': ''
+ replacements_plain:
+ 'choices=["qt", "cli"]': 'choices=["qt"], help="CLI option is currently not available in the exe. Run from source instead."'
diff --git a/tilia.spec b/tilia.spec
deleted file mode 100644
index 41456aa98..000000000
--- a/tilia.spec
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- mode: python ; coding: utf-8 -*-
-import argparse
-import platform
-import dotenv
-
-from pathlib import Path
-
-from tilia.constants import APP_NAME, VERSION
-
-# Parse build options
-parser = argparse.ArgumentParser()
-parser.add_argument("--debug", action="store_true")
-options = parser.parse_args()
-
-options = parser.parse_args()
-
-# Set platform suffix
-if platform.system() == 'Windows':
- platform_suffix = 'win'
-elif platform.system() == 'Darwin':
- platform_suffix = 'mac'
-elif platform.system() == 'Linux':
- platform_suffix = 'linux'
-else:
- platform_suffix = platform.system()
-
-# Set enviroment to production
-dotenv.set_key(".env", "ENVIRONMENT", "prod")
-
-# Build executable
-a = Analysis(
- ["./tilia/main.py"],
- pathex=[],
- binaries=None,
- datas=[
- ("./README.md", "."),
- ("./LICENSE", "."),
- ("./.env", "."),
- ("./setup.cfg", "."),
- ("./tilia/ui/img", "./tilia/ui/img/"),
- ("./tilia/ui/fonts", "./tilia/ui/fonts/"),
- ("./tilia/media/player/youtube.html", "./tilia/media/player/"),
- ("./tilia/media/player/youtube.css", "./tilia/media/player/"),
- ("./tilia/parsers/score/svg_maker.html", "./tilia/parsers/score/"),
- ("./tilia/parsers/score/timewise_to_partwise.xsl", "./tilia/parsers/score/"),
- ],
- hiddenimports=[],
- hookspath=None,
- runtime_hooks=None,
- excludes=None,
-)
-
-pyz = PYZ(a.pure)
-
-icon_path = Path("tilia", "ui", "img", "main_icon.ico").resolve().__str__()
-executable_basename = f"{APP_NAME.lower()}-{VERSION}-{platform_suffix}"
-
-if options.debug:
- exe = EXE(
- pyz,
- a.scripts,
- name=executable_basename,
- console=True,
- embed_manifest=True,
- exclude_binaries=True,
- icon=icon_path,
- )
-
- coll = COLLECT(exe, a.datas, a.binaries, name="TiLiA")
-else:
- exe = EXE(
- pyz,
- a.scripts,
- a.datas,
- a.binaries,
- name=executable_basename,
- console=False,
- embed_manifest=True,
- icon=icon_path,
- )
- app = BUNDLE(
- exe,
- name=f"{executable_basename}.app",
- icon=icon_path,
- version=VERSION,
- info_plist={
- 'NSPrincipalClass': 'NSApplication',
- 'NSAppleScriptEnabled': False,
- 'CFBundleDocumentTypes': [
- {
- 'CFBundleTypeName': 'My File Format',
- 'CFBundleTypeIconFile': 'MyFileIcon.icns',
- 'LSItemContentTypes': ['com.example.myformat'],
- 'LSHandlerRank': 'Owner'
- }
- ]
- },
- )
-
-# Reset enviroment
-dotenv.set_key(".env", "ENVIRONMENT", "dev")
diff --git a/tilia/__main__.py b/tilia/__main__.py
new file mode 100644
index 000000000..e769ab26c
--- /dev/null
+++ b/tilia/__main__.py
@@ -0,0 +1,72 @@
+import sys
+from pathlib import Path
+import platform
+import subprocess
+import traceback
+import webbrowser
+
+sys.path[0] = Path(__file__).parents[1].__str__()
+
+
+def deps_debug(exc: ImportError):
+ from tilia.constants import EMAIL, GITHUB_URL, WEBSITE_URL # noqa: E402
+
+ def _raise_deps_error(exc: ImportError, message: list[str]):
+ raise RuntimeError("\n".join(message)) from exc
+
+ distro = platform.freedesktop_os_release().get("ID_LIKE", "").split()[0]
+ link = f"{WEBSITE_URL}/help/installation?distro={distro}#troubleshooting-linux"
+ root_path = Path(
+ [*traceback.walk_tb(exc.__traceback__)][0][0].f_code.co_filename
+ ).parent
+ lib_path = root_path / "PySide6/qt-plugins/platforms/libqxcb.so"
+
+ if not lib_path.exists():
+ if "__compiled__" not in globals():
+ msg = [
+ "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.",
+ "Did you forget to install python dependencies?",
+ ]
+ else:
+ msg = [
+ "Could not locate the necessary libraries to run TiLiA. libqxcb.so file not found.",
+ f"Open an issue on our repo at <{GITHUB_URL}> or contact us at <{EMAIL}> for help.\n"
+ "Dumping all files in tree for debug...",
+ subprocess.getoutput(f"ls {root_path.as_posix()} -ltraR"),
+ ]
+ _raise_deps_error(exc, msg)
+
+ deps = subprocess.getoutput(f"ldd {lib_path.as_posix()}")
+ if "=> not found" in deps:
+ missing_deps = []
+ for line in deps.splitlines():
+ if "=> not found" in line:
+ dep = line.strip().rstrip(" => not found")
+ missing_deps.append(dep)
+
+ if missing_deps:
+ deps = f"Missing libraries:\n{missing_deps}"
+
+ msg = [
+ "TiLiA could not start due to missing system dependencies.",
+ f"Visit <{link}> for help on installation.",
+ "Install the necessary dependencies then restart.\n",
+ deps,
+ ]
+ webbrowser.open(link)
+ _raise_deps_error(exc, msg)
+
+
+def main():
+ try:
+ from tilia.boot import boot # noqa: E402
+
+ boot()
+ except ImportError as exc:
+ if sys.platform != "linux":
+ raise exc
+ deps_debug(exc)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tilia/app.py b/tilia/app.py
index 8467aa803..2792234e4 100644
--- a/tilia/app.py
+++ b/tilia/app.py
@@ -169,7 +169,7 @@ def on_open(self, path: Path | str | None = None) -> None:
def update_recent_files(self):
try:
geometry, window_state = get(Get.WINDOW_GEOMETRY), get(Get.WINDOW_STATE)
- except tilia.exceptions.NoReplyToRequest:
+ except NoReplyToRequest:
geometry, window_state = None, None
settings.update_recent_files(
@@ -257,7 +257,7 @@ def on_restore_state(self, state: dict) -> None:
def recover_to_state(self, state: dict) -> None:
"""
- Clears the app and attemps to restore the given state.
+ Clears the app and attempts to restore the given state.
Unlike `on_restore_state` this will crash if an error occurs during
the restoration.
This is meant to be used after an exception occurred, so if the
diff --git a/tilia/boot.py b/tilia/boot.py
index 5fbf2afe0..9c70ae093 100644
--- a/tilia/boot.py
+++ b/tilia/boot.py
@@ -3,25 +3,23 @@
import sys
import traceback
-import dotenv
-from PyQt6.QtWidgets import QApplication
+from PySide6.QtWidgets import QApplication
+import tilia.utils # noqa: F401
from tilia.app import App
from tilia.clipboard import Clipboard
-from tilia.dirs import PROJECT_ROOT, setup_dirs
+from tilia.dirs import setup_dirs
from tilia.file.file_manager import FileManager
from tilia.file.autosave import AutoSaver
from tilia.log import logger
from tilia.media.player import QtAudioPlayer
-from tilia.ui.cli.ui import CLI
-from tilia.ui.qtui import QtUI, TiliaMainWindow
from tilia.undo_manager import UndoManager
app = None
ui = None
-def handle_expection(type, value, tb):
+def handle_exception(type, value, tb):
exc_message = "".join(traceback.format_exception(type, value, tb))
if ui:
ui.show_crash_dialog(exc_message)
@@ -34,11 +32,8 @@ def handle_expection(type, value, tb):
def boot():
- sys.excepthook = handle_expection
- dotenv_path = PROJECT_ROOT / ".env"
- success = dotenv.load_dotenv(dotenv_path)
- if not success:
- print(f"Could not load environment variables from {dotenv_path}")
+ sys.excepthook = handle_exception
+
args = setup_parser()
setup_dirs()
logger.setup()
@@ -94,9 +89,14 @@ def setup_logic(autosaver=True):
def setup_ui(q_application: QApplication, interface: str):
if interface == "qt":
+ from tilia.ui.qtui import QtUI, TiliaMainWindow
+
mw = TiliaMainWindow()
return QtUI(q_application, mw)
+
elif interface == "cli":
+ from tilia.ui.cli.ui import CLI
+
return CLI()
diff --git a/tilia/constants.py b/tilia/constants.py
index 37b00702e..550b09ab4 100644
--- a/tilia/constants.py
+++ b/tilia/constants.py
@@ -1,30 +1,56 @@
-import configparser
+from importlib import metadata
from pathlib import Path
+import re
-setupcfg = configparser.ConfigParser()
-setupcfg.read(Path(__file__).parent.parent / "setup.cfg")
+if (toml := Path(__file__).parent.parent / "pyproject.toml").exists():
+ import sys
-if setupcfg.has_section("metadata"):
- APP_NAME = setupcfg["metadata"]["name"]
- AUTHOR = setupcfg["metadata"]["author"]
- VERSION = setupcfg["metadata"]["version"]
+ if sys.version_info >= (3, 11):
+ from tomllib import load
+ else:
+ from tomli import load
+
+ with open(toml, "rb") as f:
+ setupcfg = load(f).get("project", {})
+ AUTHOR = setupcfg.get("authors", [{"name": ""}])[0]["name"]
+ EMAIL = setupcfg.get("authors", [{"email": ""}])[0]["email"]
else:
- APP_NAME = "TiLiA"
- AUTHOR = ""
- VERSION = "beta"
+ try:
+ setupcfg = metadata.metadata("TiLiA").json.copy()
+
+ AUTHOR = re.search(r'"(.*?)"', setupcfg.get("author_email", "")).group(1)
+ EMAIL = re.search(r"<(.*?)>", setupcfg.get("author_email", "")).group(1)
+ if "urls" not in setupcfg:
+ setupcfg["urls"] = {}
+ for url in setupcfg.get("project_url", {}):
+ k, _, v = url.partition(", ")
+ setupcfg["urls"][k] = v
+ setupcfg["description"] = setupcfg.get("summary", "")
+ except metadata.PackageNotFoundError:
+ setupcfg = {}
+ AUTHOR = ""
+ EMAIL = ""
+
+APP_NAME = setupcfg.get("name", "")
+VERSION = setupcfg.get("version", "0.0.0")
-YEAR = "2022-2025"
+YEAR = "2022-2026"
FILE_EXTENSION = "tla"
-EMAIL_URL = "mailto:tilia@tilia-app.com"
-GITHUB_URL = "https://github.com/TimeLineAnnotator/desktop"
-WEBSITE_URL = "https://tilia-app.com"
+EMAIL_URL = "mailto:" + EMAIL
+
+GITHUB_URL = setupcfg.get("urls", {}).get("Repository", "")
+WEBSITE_URL = setupcfg.get("urls", {}).get("Homepage", "")
YOUTUBE_URL_REGEX = r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$"
NOTICE = f"""
-{APP_NAME}, {setupcfg["metadata"]["description"] if AUTHOR else ""}
+{APP_NAME}, {setupcfg.get("description", "") if AUTHOR else ""}
Copyright © {YEAR} {AUTHOR}
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
"""
+
+with open(Path(__file__).parents[1] / "LICENSE", encoding="utf-8") as f:
+ LICENSE_TEXT = f.read()
+LICENSE = re.split("How to Apply These Terms to Your New Programs", LICENSE_TEXT)[0]
diff --git a/tilia/dirs.py b/tilia/dirs.py
index f2f2650fe..762002b93 100644
--- a/tilia/dirs.py
+++ b/tilia/dirs.py
@@ -12,10 +12,7 @@
_USER_DATA_DIR = Path(
platformdirs.user_data_dir(tilia.constants.APP_NAME, roaming=True)
)
-data_path = _SITE_DATA_DIR
-PROJECT_ROOT = Path(tilia.__file__).parents[1]
-TILIA_DIR = Path(tilia.__file__).parent
-IMG_DIR = Path(TILIA_DIR, "ui", "img")
+IMG_DIR = Path(__file__).parent / "ui" / "img"
def setup_data_dir() -> Path:
@@ -40,7 +37,9 @@ def setup_logs_path(data_dir):
def setup_dirs() -> None:
- os.chdir(os.path.dirname(__file__))
+ # if not in prod, set directory to root of tilia
+ if os.environ.get("ENVIRONMENT") != "prod":
+ os.chdir(os.path.dirname(__file__))
data_dir = setup_data_dir()
diff --git a/tilia/errors.py b/tilia/errors.py
index 0373df743..3d04c7dd6 100644
--- a/tilia/errors.py
+++ b/tilia/errors.py
@@ -24,7 +24,7 @@ class Error(NamedTuple):
CSV_IMPORT_FAILED = Error("CSV import failed", "Import failed:\n{}")
CSV_IMPORT_SUCCESS_ERRORS = Error(
"CSV import",
- "Import was successful, but some components may not have been imported.\nThe following errors occured:\n{}",
+ "Import was successful, but some components may not have been imported.\nThe following errors occurred:\n{}",
)
CREATE_TIMELINE_WITHOUT_MEDIA = Error(
"Create timeline error", "Cannot create timeline with no media loaded."
@@ -75,7 +75,7 @@ class Error(NamedTuple):
COMPONENTS_COPY_ERROR = Error("Copy components error", "{}")
COMPONENTS_LOAD_ERROR = Error(
"Load components error",
- "Some components were not loaded. The following errors occured:\n{}",
+ "Some components were not loaded. The following errors occurred:\n{}",
)
COMPONENTS_PASTE_ERROR = Error("Paste components error", "{}")
FILE_NOT_FOUND = Error(
diff --git a/tilia/file/file_manager.py b/tilia/file/file_manager.py
index 3a0132202..1d72d8fef 100644
--- a/tilia/file/file_manager.py
+++ b/tilia/file/file_manager.py
@@ -2,8 +2,11 @@
from pathlib import Path
import json
-from tilia.exceptions import MediaMetadataFieldNotFound, MediaMetadataFieldAlreadyExists
-import tilia.exceptions
+from tilia.exceptions import (
+ MediaMetadataFieldAlreadyExists,
+ MediaMetadataFieldNotFound,
+ NoReplyToRequest,
+)
from tilia.file.common import are_tilia_data_equal, write_tilia_file_to_disk
from tilia.requests import listen, Post, Get, serve, get, post
from tilia.file.tilia_file import TiliaFile, validate_tla_data
@@ -219,7 +222,7 @@ def save(self, data: dict, path: Path | str):
try:
geometry, window_state = get(Get.WINDOW_GEOMETRY), get(Get.WINDOW_STATE)
- except tilia.exceptions.NoReplyToRequest:
+ except NoReplyToRequest:
geometry, window_state = None, None
settings.update_recent_files(path, geometry, window_state)
diff --git a/tilia/log.py b/tilia/log.py
index 198371585..b02c78589 100644
--- a/tilia/log.py
+++ b/tilia/log.py
@@ -41,7 +41,7 @@ def __init__(self):
self._dump_count = count()
def setup(self):
- match (env := os.environ.get("ENVIRONMENT", "prod")):
+ match env := os.environ.get("ENVIRONMENT"):
case "test":
self.disabled = True
self.setup_sentry(env)
diff --git a/tilia/main.py b/tilia/main.py
deleted file mode 100644
index 61175e8aa..000000000
--- a/tilia/main.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from tilia.boot import boot
-
-
-def main() -> None:
- boot()
-
-
-if __name__ == "__main__":
- main()
diff --git a/tilia/media/player/base.py b/tilia/media/player/base.py
index b3bf688b0..c4ee053a9 100644
--- a/tilia/media/player/base.py
+++ b/tilia/media/player/base.py
@@ -5,7 +5,7 @@
from enum import Enum, auto
from pathlib import Path
-from PyQt6.QtCore import QTimer
+from PySide6.QtCore import QTimer
import tilia.errors
from tilia.media import exporter
diff --git a/tilia/media/player/qtplayer.py b/tilia/media/player/qtplayer.py
index 2d986628c..c158af543 100644
--- a/tilia/media/player/qtplayer.py
+++ b/tilia/media/player/qtplayer.py
@@ -1,9 +1,7 @@
from __future__ import annotations
-import time
-
-from PyQt6.QtCore import QUrl
-from PyQt6.QtMultimedia import (
+from PySide6.QtCore import QUrl, QEventLoop, SignalInstance, QTimer
+from PySide6.QtMultimedia import (
QMediaPlayer,
QAudioOutput,
QAudio,
@@ -17,6 +15,42 @@
from tilia.ui.player import PlayerStatus
+def wait_for_signal(signal: SignalInstance, value):
+ """
+ Many Qt functions run on threads, and this wrapper makes sure that after starting a process, the right signal is emitted before continuing the TiLiA process.
+ See _engine_stop of QtPlayer for an example implementation.
+
+ :param signal: The signal to watch.
+ :type signal: SignalInstance
+ :param value: The "right" output value that signal should emit before continuing. (eg. on stopping player, playbackStateChanged emits StoppedState when player has been successfully stopped. Only then can we continue the rest of the update process.)
+ """
+
+ def signal_wrapper(func):
+ timer = QTimer(singleShot=True, interval=100)
+ loop = QEventLoop()
+ success = False
+
+ def value_checker(signal_value):
+ if signal_value == value:
+ nonlocal success
+ success = True
+ loop.quit()
+
+ def check_signal(*args, **kwargs):
+ nonlocal success
+ if not func(*args, **kwargs):
+ return False
+ signal.connect(value_checker)
+ timer.timeout.connect(loop.quit)
+ timer.start()
+ loop.exec()
+ return timer.isActive() and success
+
+ return check_signal
+
+ return signal_wrapper
+
+
class QtPlayer(Player):
MEDIA_TYPE = ""
@@ -48,9 +82,15 @@ def on_audio_outputs_changed(self) -> None:
self.audio_output.setDevice(QAudioDevice())
def _engine_load_media(self, media_path: str) -> bool:
- self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze.
- self.player.setSource(QUrl.fromLocalFile(media_path))
- return True
+ @wait_for_signal(
+ self.player.mediaStatusChanged, QMediaPlayer.MediaStatus.LoadedMedia
+ )
+ def load_media(media_path):
+ self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze.
+ self.player.setSource(QUrl.fromLocalFile(media_path))
+ return True
+
+ return load_media(media_path)
def _engine_get_current_time(self):
return self.player.position() / 1000
@@ -68,12 +108,14 @@ def _engine_unpause(self) -> None:
self.player.play()
def _engine_stop(self):
- self.player.stop()
- # Sleeping avoids freeze if about to change player URL. Not sure why the freeze happens.
- # Waiting for self.player.mediaStatus() == MediaPlayer.MediaStatus.Stopped also does not work.
- # I have tested different sleep times and 0.01 seems to prevent freezes reliably
- # while still having no perceptible impact on test performance.
- time.sleep(0.01)
+ @wait_for_signal(
+ self.player.playbackStateChanged, QMediaPlayer.PlaybackState.StoppedState
+ )
+ def stop():
+ self.player.stop()
+ return True
+
+ return stop()
def _engine_unload_media(self):
self._engine_stop() # Must be _engine_stop() instead of player.stop() to avoid freeze.
diff --git a/tilia/media/player/qtvideo.py b/tilia/media/player/qtvideo.py
index 08017396a..3c3c252ea 100644
--- a/tilia/media/player/qtvideo.py
+++ b/tilia/media/player/qtvideo.py
@@ -1,7 +1,7 @@
from __future__ import annotations
-from PyQt6.QtMultimediaWidgets import QVideoWidget
-from PyQt6.QtWidgets import QSizePolicy
+from PySide6.QtMultimediaWidgets import QVideoWidget
+from PySide6.QtWidgets import QSizePolicy
from .qtplayer import QtPlayer
from tilia.ui.windows.view_window import ViewWindow
diff --git a/tilia/media/player/youtube.py b/tilia/media/player/youtube.py
index a467cf1d4..0cd2bd20b 100644
--- a/tilia/media/player/youtube.py
+++ b/tilia/media/player/youtube.py
@@ -2,16 +2,16 @@
from enum import Enum
from pathlib import Path
-from PyQt6.QtCore import QUrl, pyqtSlot, QObject, QTimer, QByteArray
-from PyQt6.QtWebChannel import QWebChannel
+from PySide6.QtCore import QUrl, Slot, QObject, QTimer, QByteArray
+from PySide6.QtWebChannel import QWebChannel
import tilia.constants
import tilia.errors
from tilia.media.player import Player
-from PyQt6.QtWebEngineWidgets import QWebEngineView
-from PyQt6.QtWebEngineCore import QWebEngineSettings, QWebEngineUrlRequestInterceptor
+from PySide6.QtWebEngineWidgets import QWebEngineView
+from PySide6.QtWebEngineCore import QWebEngineSettings, QWebEngineUrlRequestInterceptor
from tilia.media.player.base import MediaTimeChangeReason
from tilia.requests import Post, post
@@ -38,11 +38,11 @@ def __init__(
self.player_toolbar_enabled = False
self.display_error = display_error
- @pyqtSlot("float")
+ @Slot("float")
def on_new_time(self, time):
self.set_current_time(time)
- @pyqtSlot("int")
+ @Slot("int")
def on_player_state_change(self, state):
if state == self.State.UNSTARTED.value:
post(Post.PLAYER_UPDATE_CONTROLS, PlayerStatus.WAITING_FOR_YOUTUBE)
@@ -56,11 +56,11 @@ def on_player_state_change(self, state):
else:
self.set_is_playing(False)
- @pyqtSlot("float")
+ @Slot("float")
def on_set_playback_rate(self, playback_rate: float):
self.set_playback_rate(playback_rate)
- @pyqtSlot(str)
+ @Slot(str)
def on_error(self, message: str) -> None:
self.display_error(message)
@@ -118,7 +118,7 @@ def load_media(
initial_duration: float = 0.0,
):
"""
- Returns True if media loading has *started* succesfully, False otherwise.
+ Returns True if media loading has *started* successfully, False otherwise.
Loading is asynchronous, and self.on_media_load_done will be called
when it is completed.
If initial_duration is provided, it will be available when returning.
diff --git a/tilia/parsers/csv/beat.py b/tilia/parsers/csv/beat.py
index 594c70974..3eb2bd821 100644
--- a/tilia/parsers/csv/beat.py
+++ b/tilia/parsers/csv/beat.py
@@ -55,7 +55,7 @@ def beats_from_csv(
measures_to_force_display = []
def check_params() -> bool:
- for param, parser in zip(params, parsers):
+ for param, parser in zip(params, parsers, strict=True):
if param in params_to_indices:
try:
index = params_to_indices[param]
diff --git a/tilia/parsers/csv/common.py b/tilia/parsers/csv/common.py
index f04ff9528..08ee6ba6d 100644
--- a/tilia/parsers/csv/common.py
+++ b/tilia/parsers/csv/common.py
@@ -73,8 +73,8 @@ def _get_attr_data(
def _parse_measure_fraction(value: str):
try:
value = float(value)
- except ValueError:
- raise ValueError("APPEND:Must be a number between 0 and 1.")
+ except ValueError as e:
+ raise ValueError("APPEND:Must be a number between 0 and 1.") from e
if not 0 <= value <= 1:
raise ValueError("APPEND:Must be a number between 0 and 1.")
diff --git a/tilia/parsers/csv/hierarchy.py b/tilia/parsers/csv/hierarchy.py
index e3ee5a431..ea5632dca 100644
--- a/tilia/parsers/csv/hierarchy.py
+++ b/tilia/parsers/csv/hierarchy.py
@@ -41,7 +41,7 @@ def import_by_time(
"formal_type",
"formal_function",
]
- parsers = [float, float, int, float, float, str, str, str]
+ parsers = [float, float, int, float, float, str, str, str, str, str]
params_to_indices = get_params_indices(params, next(reader))
for attr in ["start", "end", "level"]:
@@ -61,7 +61,7 @@ def import_by_time(
errors.append(f"'{value}' is not a valid {attr.replace('_', ' ')}")
continue
- for param, parser in zip(params, parsers):
+ for param, parser in zip(params, parsers, strict=True):
if param in params_to_indices:
index = params_to_indices[param]
constructor_args[param] = parser(row[index])
diff --git a/tilia/parsers/csv/marker.py b/tilia/parsers/csv/marker.py
index 95ec0e0ac..1a96e3a1b 100644
--- a/tilia/parsers/csv/marker.py
+++ b/tilia/parsers/csv/marker.py
@@ -50,7 +50,7 @@ def import_by_time(
parsers = [float, str, str]
constructor_kwargs = {}
- for param, parser in zip(params, parsers):
+ for param, parser in zip(params, parsers, strict=True):
if param in params_to_indices:
index = params_to_indices[param]
constructor_kwargs[param] = parser(row[index])
@@ -124,7 +124,7 @@ def import_by_measure(
parsers = [str, str]
constructor_kwargs = {}
- for param, parser in zip(params, parsers):
+ for param, parser in zip(params, parsers, strict=True):
if param in params_to_indices:
index = params_to_indices[param]
constructor_kwargs[param] = parser(row[index])
diff --git a/tilia/parsers/score/musicxml.py b/tilia/parsers/score/musicxml.py
index 2769226a2..a78236734 100644
--- a/tilia/parsers/score/musicxml.py
+++ b/tilia/parsers/score/musicxml.py
@@ -148,11 +148,11 @@ def _parse_attributes(part: etree._Element, part_id: str):
):
match prev_note.tag:
case "backup":
- if not prev_note.find("duration") is None:
+ if prev_note.find("duration") is not None:
# grace notes have no duration
cur_div -= int(prev_note.find("duration").text)
case _:
- if not prev_note.find("duration") is None:
+ if prev_note.find("duration") is not None:
# grace notes have no duration
cur_div += int(prev_note.find("duration").text)
diff --git a/tilia/parsers/score/musicxml_to_svg.py b/tilia/parsers/score/musicxml_to_svg.py
index 21289b173..0a565f8e3 100644
--- a/tilia/parsers/score/musicxml_to_svg.py
+++ b/tilia/parsers/score/musicxml_to_svg.py
@@ -4,14 +4,14 @@
from pathlib import Path
from re import sub
-from PyQt6.QtCore import (
- pyqtSlot,
+from PySide6.QtCore import (
+ Slot,
QObject,
QUrl,
)
-from PyQt6.QtWebChannel import QWebChannel
-from PyQt6.QtWebEngineCore import QWebEngineSettings
-from PyQt6.QtWebEngineWidgets import QWebEngineView
+from PySide6.QtWebChannel import QWebChannel
+from PySide6.QtWebEngineCore import QWebEngineSettings
+from PySide6.QtWebEngineWidgets import QWebEngineView
from tilia.requests import (
get,
Get,
@@ -26,11 +26,11 @@ def __init__(self, page, on_svg_loaded, display_error) -> None:
self.on_svg_loaded = on_svg_loaded
self.display_error = display_error
- @pyqtSlot(str)
+ @Slot(str)
def set_svg(self, svg: str) -> None:
self.on_svg_loaded(svg)
- @pyqtSlot(str)
+ @Slot(str)
def on_error(self, message: str) -> None:
self.display_error(message)
diff --git a/tilia/parsers/score/timewise_to_partwise.xsl b/tilia/parsers/score/timewise_to_partwise.xsl
index f0e0925e4..b61adf97f 100644
--- a/tilia/parsers/score/timewise_to_partwise.xsl
+++ b/tilia/parsers/score/timewise_to_partwise.xsl
@@ -52,7 +52,7 @@
diff --git a/tilia/requests/get.py b/tilia/requests/get.py
index ce3bc1e30..6d3fb9eb5 100644
--- a/tilia/requests/get.py
+++ b/tilia/requests/get.py
@@ -80,8 +80,8 @@ def get(request: Get, *args, **kwargs) -> Any:
try:
return _requests_to_callbacks[request](*args, **kwargs)
- except KeyError:
- raise NoReplyToRequest(f"{request} has no repliers attached.")
+ except KeyError as e:
+ raise NoReplyToRequest(f"{request} has no repliers attached.") from e
except Exception as exc:
raise Exception(
f"Exception when processing {request} with {args=}, {kwargs=}"
@@ -110,12 +110,12 @@ def server(request: Get) -> tuple[Any | None, Callable | None]:
def stop_serving(replier: Any, request: Get) -> None:
"""
- Detaches a calback from a request.
+ Detaches a callback from a request.
"""
try:
_requests_to_callbacks.pop(request)
- except KeyError:
- raise NoCallbackAttached()
+ except KeyError as e:
+ raise NoCallbackAttached() from e
_servers_to_requests[replier].remove(request)
if not _servers_to_requests[replier]:
diff --git a/tilia/requests/post.py b/tilia/requests/post.py
index 85a85a1a0..bf70f9715 100644
--- a/tilia/requests/post.py
+++ b/tilia/requests/post.py
@@ -104,17 +104,14 @@ class Post(Enum):
] = weakref.WeakKeyDictionary()
-def _get_posts_excluded_from_log() -> list[Post]:
- result = []
- for name in os.environ.get("EXCLUDE_FROM_LOG", "").split(";"):
- result.append(Post[name])
- return result
+EXCLUDED_POSTS = [
+ Post[post] for post in os.environ.get("EXCLUDE_FROM_LOG", "").split(";")
+]
+LOG_REQUESTS = os.environ.get("LOG_REQUESTS", 0)
def _log_post(post, *args, **kwargs):
- log_message = (
- f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners[post])}"
- )
+ log_message = f"{post.name:<40} {str((args, kwargs)):<100} {list(_posts_to_listeners.get(post, ''))}"
if post is Post.DISPLAY_ERROR:
logger.warning(log_message)
return
@@ -124,14 +121,14 @@ def _log_post(post, *args, **kwargs):
def post(post: Post, *args, **kwargs) -> None:
- if os.environ.get("LOG_REQUESTS", 0) and post not in _get_posts_excluded_from_log():
+ if LOG_REQUESTS and post not in EXCLUDED_POSTS:
_log_post(post, args, kwargs)
# Returning a result is an experimental feature.
# This can be very useful to check if the request was successful.
# Should be used only when a single listener is expected.
# If there are multiple listeners, the result of the last listener is returned.
result = None
- for listener, callback in _posts_to_listeners[post].copy().items():
+ for callback in _posts_to_listeners[post].copy().values():
result = callback(*args, **kwargs)
return result
diff --git a/tilia/settings.py b/tilia/settings.py
index 9c143b224..386d22331 100644
--- a/tilia/settings.py
+++ b/tilia/settings.py
@@ -1,4 +1,4 @@
-from PyQt6.QtCore import QSettings, QObject
+from PySide6.QtCore import QSettings, QObject
from pathlib import Path
import tilia.constants
@@ -6,7 +6,6 @@
class SettingsManager(QObject):
-
DEFAULT_SETTINGS = {
"general": {
"auto-scroll": ScrollType.OFF,
@@ -90,7 +89,9 @@ class SettingsManager(QObject):
}
def __init__(self):
- self._settings = QSettings(tilia.constants.APP_NAME, "Desktop Settings")
+ self._settings = QSettings(
+ tilia.constants.APP_NAME, application="Desktop Settings", parent=None
+ )
self._files_updated_callbacks = set()
self._cache = {}
self._check_all_default_settings_present()
@@ -119,7 +120,10 @@ def link_file_update(self, updating_function) -> None:
def _get(self, group_name: str, setting: str, in_default=True):
key = self._get_key(group_name, setting, in_default)
- value = self._settings.value(key, None)
+ try:
+ value = self._settings.value(key, None)
+ except EOFError: # happens when the group in self._settings is not initiated, but setting a value solves this.
+ value = None
if not value or not isinstance(
value, type(self.DEFAULT_SETTINGS[group_name][setting])
):
@@ -151,8 +155,8 @@ def set(self, group_name: str, setting: str, value):
try:
self._cache[group_name][setting] = value
self._set(group_name, setting, value)
- except AttributeError:
- raise AttributeError(f"{group_name}.{setting} not found in cache.")
+ except AttributeError as e:
+ raise AttributeError(f"{group_name}.{setting} not found in cache.") from e
@staticmethod
def _get_key(group_name: str, setting: str, in_default: bool) -> str:
diff --git a/tilia/timelines/base/component/base.py b/tilia/timelines/base/component/base.py
index 6dc6d30b9..6e1b8e4f2 100644
--- a/tilia/timelines/base/component/base.py
+++ b/tilia/timelines/base/component/base.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-from abc import ABC
from typing import Any, Callable
from tilia.exceptions import SetComponentDataError, GetComponentDataError
@@ -10,7 +9,7 @@
from tilia.utils import get_tilia_class_string
-class TimelineComponent(ABC):
+class TimelineComponent:
SERIALIZABLE = []
ORDERING_ATTRS = tuple()
@@ -54,10 +53,10 @@ def validate_set_data(self, attr, value):
)
try:
return self.validators[attr](value)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"{self} has no validator for attribute {attr}. Can't set to '{value}'."
- )
+ ) from e
def set_data(self, attr: str, value: Any):
if not self.validate_set_data(attr, value):
@@ -71,11 +70,11 @@ def set_data(self, attr: str, value: Any):
def get_data(self, attr: str):
try:
return getattr(self, attr)
- except AttributeError:
+ except AttributeError as e:
raise GetComponentDataError(
"AttributeError while getting data from component."
f"Does {type(self)} have a {attr} attribute?"
- )
+ ) from e
@classmethod
def validate_creation(cls, *args, **kwargs) -> tuple[bool, str]:
@@ -83,7 +82,7 @@ def validate_creation(cls, *args, **kwargs) -> tuple[bool, str]:
@staticmethod
def compose_validators(
- validators: list[Callable[[], tuple[bool, str]]]
+ validators: list[Callable[[], tuple[bool, str]]],
) -> tuple[bool, str]:
"""Calls validators in order and returns (False, reason) if any fails. Returns (True, '') if all succeed."""
for validator in validators:
diff --git a/tilia/timelines/base/timeline.py b/tilia/timelines/base/timeline.py
index d96f649b3..2d4b65a83 100644
--- a/tilia/timelines/base/timeline.py
+++ b/tilia/timelines/base/timeline.py
@@ -164,10 +164,10 @@ def validate_set_data(self, attr, value):
)
try:
return self.validators[attr](value)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"{self} has no validator for attribute {attr}. Can't set to '{value}'."
- )
+ ) from e
def set_data(self, attr: str, value: Any):
if not self.validate_set_data(attr, value):
@@ -272,7 +272,7 @@ def _get_base_state(self) -> dict:
string_to_hash += str(value) + "|"
string_to_hash += str(value) + "|"
- state["hash"] = hash_function(f'{state["kind"]}|{string_to_hash}')
+ state["hash"] = hash_function(f"{state['kind']}|{string_to_hash}")
return state
@@ -329,7 +329,7 @@ def associate_to_timeline(self, timeline: Timeline):
@staticmethod
def _compose_validators(
- validators: list[Callable[[], tuple[bool, str]]]
+ validators: list[Callable[[], tuple[bool, str]]],
) -> tuple[bool, str]:
"""Calls validators in order and returns (False, reason) if any fails."""
for validator in validators:
@@ -470,11 +470,11 @@ def _remove_from_components_set(self, component: TC) -> None:
try:
self._components.remove(component)
self.id_to_component.pop(component.id)
- except KeyError:
+ except KeyError as e:
raise KeyError(
f"Can't remove component '{component}' from {self}: not in"
" self.components."
- )
+ ) from e
def update_component_order(self, component: TC):
self._components.remove(component)
diff --git a/tilia/timelines/base/validators.py b/tilia/timelines/base/validators.py
index 5edf87d8a..fc36e080a 100644
--- a/tilia/timelines/base/validators.py
+++ b/tilia/timelines/base/validators.py
@@ -1,7 +1,7 @@
import math
from typing import Any
-from PyQt6.QtGui import QColor
+from PySide6.QtGui import QColor
def validate_time(value):
diff --git a/tilia/timelines/beat/timeline.py b/tilia/timelines/beat/timeline.py
index a954058b9..fa13f3f6b 100644
--- a/tilia/timelines/beat/timeline.py
+++ b/tilia/timelines/beat/timeline.py
@@ -577,7 +577,7 @@ def get_beat_index(self, beat: Beat) -> int:
return self.components.index(beat)
def propagate_measure_number_change(self, start_index: int):
- for j, measure in enumerate(self.measure_numbers[start_index + 1 :]):
+ for j in range(len(self.measure_numbers[start_index + 1 :])):
propagate_index = j + start_index + 1
if propagate_index in self.measures_to_force_display:
break
diff --git a/tilia/timelines/collection/collection.py b/tilia/timelines/collection/collection.py
index 36cb62cf1..962f13dc3 100644
--- a/tilia/timelines/collection/collection.py
+++ b/tilia/timelines/collection/collection.py
@@ -86,10 +86,10 @@ def _validate_timeline_kind(kind: TlKind | str):
if isinstance(kind, str):
try:
kind = TlKind(kind)
- except ValueError:
+ except ValueError as e:
raise TimelineValidationError(
f"Can't create timeline: invalid timeline kind '{kind}'"
- )
+ ) from e
if not isinstance(kind, TlKind):
raise TimelineValidationError(
f"Can't create timeline: invalid timeline kind '{kind}'"
@@ -204,11 +204,11 @@ def _remove_from_timelines(self, timeline: Timeline) -> None:
for tl in self:
if tl.ordinal > timeline.ordinal:
tl.ordinal -= 1
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline '{timeline}' from {self}: not in"
" self._timelines."
- )
+ ) from e
def get_export_data(self):
return [
diff --git a/tilia/timelines/harmony/components/harmony.py b/tilia/timelines/harmony/components/harmony.py
index 8b3de430a..182925e39 100644
--- a/tilia/timelines/harmony/components/harmony.py
+++ b/tilia/timelines/harmony/components/harmony.py
@@ -103,9 +103,11 @@ def __repr__(self):
@classmethod
def from_string(
- cls, time: float, string: str, key: music21.key.Key = music21.key.Key("C")
+ cls, time: float, string: str, key: music21.key.Key | str = "C major"
):
- music21_object, object_type = _get_music21_object_from_text(string, key)
+ music21_object, object_type = _get_music21_object_from_text(
+ string, key.__str__()
+ )
if not string:
return None
@@ -159,9 +161,10 @@ def _replace_special_abbreviations(text):
def _get_music21_object_from_text(
text: str, key: str
-) -> tuple[music21.harmony.ChordSymbol | music21.roman.RomanNumeral, str] | tuple[
- None, None
-]:
+) -> (
+ tuple[music21.harmony.ChordSymbol | music21.roman.RomanNumeral, str]
+ | tuple[None, None]
+):
text, prefixed_accidental = _extract_prefixed_accidental(text)
text = _format_postfix_accidental(text)
text = _replace_special_abbreviations(text)
diff --git a/tilia/timelines/hierarchy/timeline.py b/tilia/timelines/hierarchy/timeline.py
index 68797c1b9..d4422f16d 100644
--- a/tilia/timelines/hierarchy/timeline.py
+++ b/tilia/timelines/hierarchy/timeline.py
@@ -95,7 +95,7 @@ def get_boundary_conflicts(self) -> list[tuple[Hierarchy, Hierarchy]]:
and hrc1.end == hrc2.end
and hrc1.level == hrc2.level
):
- # if hierachies have same times and level, there's a conflict
+ # if hierarchies have same times and level, there's a conflict
conflicts.append((hrc1, hrc2))
return conflicts
diff --git a/tilia/timelines/slider/timeline.py b/tilia/timelines/slider/timeline.py
index 49fd61560..02c5a296e 100644
--- a/tilia/timelines/slider/timeline.py
+++ b/tilia/timelines/slider/timeline.py
@@ -32,7 +32,7 @@ def is_empty(self):
return True
def _validate_delete_components(self, component: TimelineComponent):
- """Nothing to do. Must impement abstract method."""
+ """Nothing to do. Must implement abstract method."""
def get_state(self) -> dict:
result = {}
diff --git a/tilia/ui/actions.py b/tilia/ui/actions.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tilia/ui/cli/generate_scripts.py b/tilia/ui/cli/generate_scripts.py
index 04210bbee..6e5b5f3c7 100644
--- a/tilia/ui/cli/generate_scripts.py
+++ b/tilia/ui/cli/generate_scripts.py
@@ -136,12 +136,12 @@ def _get_script_for_folder(
if not (media_data or "set_media_length.txt" in filenames):
print(
- f'{"No suitable media found in " + folder_name + ".":<100}{"Folder skipped.":>14}'
+ f"{'No suitable media found in ' + folder_name + '.':<100}{'Folder skipped.':>14}"
)
return
if len(media_data) > 1:
print(
- f'{"Multiple media files found in " + folder_name + ".":<100}{"Folder skipped.":>14}'
+ f"{'Multiple media files found in ' + folder_name + '.':<100}{'Folder skipped.':>14}"
)
return
@@ -151,7 +151,7 @@ def _get_script_for_folder(
if "set_media_length.txt" in filenames:
to_write.append(
- f'metadata set-media-length {open(os.path.join(folder_name, "set_media_length.txt"), "r").read()}\n'
+ f"metadata set-media-length {open(os.path.join(folder_name, 'set_media_length.txt'), 'r').read()}\n"
)
filenames.remove("set_media_length.txt")
@@ -173,7 +173,7 @@ def _get_script_for_folder(
reference_beat = args.ref_name
elif reference_beat and args.ref_name:
print(
- f'{"Multiple beat timelines found. Using " + reference_beat + " as reference instead of " + args.ref_name + ".":<100}{os.path.join(folder_name, file):>20}'
+ f"{'Multiple beat timelines found. Using ' + reference_beat + ' as reference instead of ' + args.ref_name + '.':<100}{os.path.join(folder_name, file):>20}"
)
except ValueError as e:
@@ -218,7 +218,7 @@ def get_scripts(directory: str | Path) -> list[Path]:
List of paths to individual scripts (str): The absolute path to each script.
"""
saved_scripts = []
- for folder_name, sub_folders, filenames in os.walk(directory):
+ for folder_name, _, filenames in os.walk(directory):
saved_scripts += filter(None, [_get_script_for_folder(folder_name, filenames)])
return saved_scripts
diff --git a/tilia/ui/cli/player.py b/tilia/ui/cli/player.py
index fc5e9fa1e..1b8a86730 100644
--- a/tilia/ui/cli/player.py
+++ b/tilia/ui/cli/player.py
@@ -162,7 +162,6 @@ def get_youtube_duration(self, id: str) -> tuple[bool, str | float]:
return False, self.DECODE_ERROR_MESSAGE
try:
-
duration = response_json["items"][0]["contentDetails"]["duration"]
except IndexError:
return False, self.VIDEO_NOT_FOUND_MESSAGE.format(id)
diff --git a/tilia/ui/cli/timelines/imp.py b/tilia/ui/cli/timelines/imp.py
index 8ca8e1fb5..f9ce5a5d0 100644
--- a/tilia/ui/cli/timelines/imp.py
+++ b/tilia/ui/cli/timelines/imp.py
@@ -14,7 +14,7 @@
def setup_parser(subparsers):
# Import command
import_parser = subparsers.add_parser(
- "import", help="Import data from a file into a " "timeline"
+ "import", help="Import data from a file into a timeline"
)
import_parser.set_defaults(func=import_timeline)
diff --git a/tilia/ui/cli/timelines/list.py b/tilia/ui/cli/timelines/list.py
index dc8d7ba8c..1f140e417 100644
--- a/tilia/ui/cli/timelines/list.py
+++ b/tilia/ui/cli/timelines/list.py
@@ -4,7 +4,7 @@
def pprint_tlkind(kind: TlKind) -> str:
- return kind.value.strip("_TIMELINE").capitalize()
+ return kind.value.replace("_TIMELINE", "").capitalize()
def setup_parser(subparser):
diff --git a/tilia/ui/cli/ui.py b/tilia/ui/cli/ui.py
index e4bcd6762..b5c33d358 100644
--- a/tilia/ui/cli/ui.py
+++ b/tilia/ui/cli/ui.py
@@ -111,7 +111,7 @@ def parse_and_run(self, cmd):
def run(self, cmd: str) -> bool:
"""
Parses the commands entered by the user.
- Return True if an uncaught exception ocurred.
+ Return True if an uncaught exception occurred.
The exception is stored in self.exception.
"""
try:
@@ -150,6 +150,10 @@ def get_player_class(media_type: str):
def show_crash_dialog(exc_message) -> None:
post(Post.DISPLAY_ERROR, "CLI has crashed", "Error: " + exc_message)
+ @staticmethod
+ def exit(code: int):
+ raise SystemExit(code)
+
def on_ask_yes_or_no(title: str, prompt: str) -> bool:
return ask_yes_or_no(f"{title}: {prompt}")
diff --git a/tilia/ui/color.py b/tilia/ui/color.py
index 13af1443c..9f4b5702d 100644
--- a/tilia/ui/color.py
+++ b/tilia/ui/color.py
@@ -1,4 +1,4 @@
-from PyQt6.QtGui import QColor
+from PySide6.QtGui import QColor
def get_tinted_color(color: str, factor: int) -> str:
diff --git a/tilia/ui/commands.py b/tilia/ui/commands.py
index b95f6215e..9e5e0a48f 100644
--- a/tilia/ui/commands.py
+++ b/tilia/ui/commands.py
@@ -37,8 +37,8 @@
import tilia.errors
from tilia.dirs import IMG_DIR
-from PyQt6.QtWidgets import QMainWindow, QWidget
-from PyQt6.QtGui import QAction, QKeySequence, QIcon
+from PySide6.QtWidgets import QMainWindow, QWidget
+from PySide6.QtGui import QAction, QKeySequence, QIcon
class CommandQAction(QAction):
diff --git a/tilia/ui/dialogs/add_timeline_without_media.py b/tilia/ui/dialogs/add_timeline_without_media.py
index c93c627a9..de79665af 100644
--- a/tilia/ui/dialogs/add_timeline_without_media.py
+++ b/tilia/ui/dialogs/add_timeline_without_media.py
@@ -1,7 +1,7 @@
from enum import Enum
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QRadioButton,
diff --git a/tilia/ui/dialogs/basic.py b/tilia/ui/dialogs/basic.py
index 8549f5ba4..eef71ba19 100644
--- a/tilia/ui/dialogs/basic.py
+++ b/tilia/ui/dialogs/basic.py
@@ -1,5 +1,5 @@
-from PyQt6.QtGui import QColor
-from PyQt6.QtWidgets import QColorDialog, QInputDialog, QMessageBox
+from PySide6.QtGui import QColor
+from PySide6.QtWidgets import QColorDialog, QInputDialog, QMessageBox
def ask_for_color(
diff --git a/tilia/ui/dialogs/by_time_or_by_measure.py b/tilia/ui/dialogs/by_time_or_by_measure.py
index b95f2d4e8..388b1ff1a 100644
--- a/tilia/ui/dialogs/by_time_or_by_measure.py
+++ b/tilia/ui/dialogs/by_time_or_by_measure.py
@@ -1,5 +1,5 @@
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
QDialog,
QLabel,
QRadioButton,
diff --git a/tilia/ui/dialogs/choose.py b/tilia/ui/dialogs/choose.py
index d141eb20d..b7fa97cf1 100644
--- a/tilia/ui/dialogs/choose.py
+++ b/tilia/ui/dialogs/choose.py
@@ -1,6 +1,6 @@
from typing import Any
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDialog,
QMainWindow,
QLabel,
diff --git a/tilia/ui/dialogs/crash.py b/tilia/ui/dialogs/crash.py
index 69f61fc6b..3446c205b 100644
--- a/tilia/ui/dialogs/crash.py
+++ b/tilia/ui/dialogs/crash.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import Qt, QRegularExpression
-from PyQt6.QtGui import QRegularExpressionValidator
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt, QRegularExpression
+from PySide6.QtGui import QRegularExpressionValidator
+from PySide6.QtWidgets import (
QDialog,
QFormLayout,
QVBoxLayout,
diff --git a/tilia/ui/dialogs/file.py b/tilia/ui/dialogs/file.py
index 649f75f04..3e59fa975 100644
--- a/tilia/ui/dialogs/file.py
+++ b/tilia/ui/dialogs/file.py
@@ -1,9 +1,9 @@
from pathlib import Path
from typing import Sequence
-from PyQt6 import QtCore
-from PyQt6.QtCore import QDir
-from PyQt6.QtWidgets import QFileDialog
+from PySide6 import QtCore
+from PySide6.QtCore import QDir
+from PySide6.QtWidgets import QFileDialog
from tilia.media import constants as media_constants
from tilia.constants import APP_NAME, FILE_EXTENSION
diff --git a/tilia/ui/dialogs/harmony_params.py b/tilia/ui/dialogs/harmony_params.py
index 4edc69d01..c0e38577b 100644
--- a/tilia/ui/dialogs/harmony_params.py
+++ b/tilia/ui/dialogs/harmony_params.py
@@ -1,5 +1,5 @@
import music21
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDialog,
QComboBox,
QGridLayout,
diff --git a/tilia/ui/dialogs/mode_params.py b/tilia/ui/dialogs/mode_params.py
index 4d64671d4..3b895b703 100644
--- a/tilia/ui/dialogs/mode_params.py
+++ b/tilia/ui/dialogs/mode_params.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDialog,
QComboBox,
QGridLayout,
diff --git a/tilia/ui/dialogs/resize_rect.py b/tilia/ui/dialogs/resize_rect.py
index 45a2d2b8c..7f5c74559 100644
--- a/tilia/ui/dialogs/resize_rect.py
+++ b/tilia/ui/dialogs/resize_rect.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QDoubleSpinBox,
diff --git a/tilia/ui/menubar.py b/tilia/ui/menubar.py
index 478456359..e995c1694 100644
--- a/tilia/ui/menubar.py
+++ b/tilia/ui/menubar.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import QMainWindow
+from PySide6.QtWidgets import QMainWindow
from tilia.ui.menus import FileMenu, EditMenu, TimelinesMenu, ViewMenu, HelpMenu
diff --git a/tilia/ui/menus.py b/tilia/ui/menus.py
index 0a1ede5f4..df34ce205 100644
--- a/tilia/ui/menus.py
+++ b/tilia/ui/menus.py
@@ -3,8 +3,8 @@
from typing import TypeAlias
from enum import Enum, auto
-from PyQt6.QtWidgets import QMenu
-from PyQt6.QtGui import QAction
+from PySide6.QtWidgets import QMenu
+from PySide6.QtGui import QAction
from tilia.timelines.timeline_kinds import get_timeline_name, TimelineKind
from tilia.ui import commands
diff --git a/tilia/ui/options_toolbar.py b/tilia/ui/options_toolbar.py
index 41a6716a1..f1542c2c4 100644
--- a/tilia/ui/options_toolbar.py
+++ b/tilia/ui/options_toolbar.py
@@ -1,5 +1,5 @@
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QToolBar, QComboBox, QLabel
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QToolBar, QComboBox, QLabel
from tilia.settings import settings
from tilia.ui.enums import ScrollType
diff --git a/tilia/ui/player.py b/tilia/ui/player.py
index f8faa2e77..8cb65ac2a 100644
--- a/tilia/ui/player.py
+++ b/tilia/ui/player.py
@@ -1,11 +1,11 @@
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDoubleSpinBox,
QLabel,
QSlider,
QToolBar,
)
-from PyQt6.QtGui import QIcon, QAction, QPixmap
-from PyQt6.QtCore import Qt
+from PySide6.QtGui import QIcon, QAction, QPixmap
+from PySide6.QtCore import Qt
from tilia.dirs import IMG_DIR
from tilia.ui import commands
diff --git a/tilia/ui/qtui.py b/tilia/ui/qtui.py
index a39b0047e..bd610d7f1 100644
--- a/tilia/ui/qtui.py
+++ b/tilia/ui/qtui.py
@@ -6,10 +6,10 @@
from typing import Optional
-from PyQt6 import QtGui
-from PyQt6.QtCore import QKeyCombination, Qt, qInstallMessageHandler, QUrl, QtMsgType
-from PyQt6.QtGui import QIcon, QFontDatabase, QDesktopServices, QPainter, QPixmap
-from PyQt6.QtWidgets import (
+from PySide6 import QtGui
+from PySide6.QtCore import QKeyCombination, Qt, qInstallMessageHandler, QUrl, QtMsgType
+from PySide6.QtGui import QIcon, QFontDatabase, QDesktopServices, QPainter, QPixmap
+from PySide6.QtWidgets import (
QMainWindow,
QApplication,
QToolBar,
@@ -313,7 +313,7 @@ def launch(self):
return self.q_application.exec()
def exit(self, code: int):
- # Code = 0 means a succesful run, code = 1 means an unhandled exception.
+ # Code = 0 means a successful run, code = 1 means an unhandled exception.
self.q_application.exit(code)
def get_window_geometry(self):
@@ -416,7 +416,7 @@ def on_media_load_youtube():
def on_clear_ui(self):
"""Closes all UI windows."""
- for kind, window in self._windows.items():
+ for window in self._windows.values():
if window is not None:
window.close()
self.main_window.setFocus()
diff --git a/tilia/ui/smooth_scroll.py b/tilia/ui/smooth_scroll.py
index 78f7756dd..64b853378 100644
--- a/tilia/ui/smooth_scroll.py
+++ b/tilia/ui/smooth_scroll.py
@@ -3,7 +3,7 @@
# - apply smoothing curve to input - currently linear
from typing import Any, Callable
-from PyQt6.QtCore import QVariantAnimation, QVariant
+from PySide6.QtCore import QVariantAnimation
from tilia.settings import settings
@@ -13,7 +13,7 @@ def setup_smooth(self):
self.animation.setDuration(125)
-def smooth(self: Any, args_getter: Callable[[], QVariant]):
+def smooth(self: Any, args_getter: Callable[[], object]):
"""
Function Wrapper
Smooths changes made by `args_setter` by inputting smaller changes over time.
@@ -26,15 +26,15 @@ def smooth(self: Any, args_getter: Callable[[], QVariant]):
`args_getter` and `args_setter` must refer to the same variables in `args_setpoint` in the same order.
"""
- def wrapper(args_setter: Callable[[QVariant], None]) -> Callable:
- def wrapped_setter(args_setpoint: QVariant) -> None:
+ def wrapper(args_setter: Callable[[object], None]) -> Callable:
+ def wrapped_setter(args_setpoint: object) -> None:
if self.animation.state() is QVariantAnimation.State.Running:
self.animation.pause()
self.animation.setStartValue(args_getter())
self.animation.setEndValue(args_setpoint)
self.animation.start()
- def timeout(value: QVariant) -> None:
+ def timeout(value: object) -> None:
args_setter(value)
if settings.get("general", "prioritise_performance") is True:
diff --git a/tilia/ui/timelines/audiowave/element.py b/tilia/ui/timelines/audiowave/element.py
index 949158e6a..1404bea95 100644
--- a/tilia/ui/timelines/audiowave/element.py
+++ b/tilia/ui/timelines/audiowave/element.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from PyQt6.QtCore import Qt, QPointF, QLineF
-from PyQt6.QtGui import QPen, QColor
-from PyQt6.QtWidgets import QGraphicsLineItem
+from PySide6.QtCore import Qt, QPointF, QLineF
+from PySide6.QtGui import QPen, QColor
+from PySide6.QtWidgets import QGraphicsLineItem
from tilia.requests import Post, post, get, Get
from ..cursors import CursorMixIn
diff --git a/tilia/ui/timelines/base/element.py b/tilia/ui/timelines/base/element.py
index 59c54181c..4cb4b5a34 100644
--- a/tilia/ui/timelines/base/element.py
+++ b/tilia/ui/timelines/base/element.py
@@ -4,12 +4,12 @@
from typing import Optional, Any, Callable
-from PyQt6.QtCore import QPoint
+from PySide6.QtCore import QPoint
from tilia.ui.timelines.base.context_menus import TimelineUIElementContextMenu
from tilia.ui.coords import time_x_converter
-from PyQt6.QtWidgets import QGraphicsScene
+from PySide6.QtWidgets import QGraphicsScene
from tilia.requests import stop_listening_to_all
@@ -64,7 +64,7 @@ def is_selected(self):
return self in self.timeline_ui.selected_elements
@abstractmethod
- def child_items(self):
+ def child_items(self) -> list[Any]:
...
def selection_triggers(self):
@@ -86,12 +86,18 @@ def on_right_click(self, x, y, _):
menu = self.CONTEXT_MENU_CLASS(self)
menu.exec(QPoint(x, y))
+ @abstractmethod
def on_select(self):
...
+ @abstractmethod
def on_deselect(self):
...
+ @abstractmethod
+ def update_position(self):
+ ...
+
def delete(self):
for item in self.child_items():
if item.parentItem():
diff --git a/tilia/ui/timelines/base/element_manager.py b/tilia/ui/timelines/base/element_manager.py
index 302aaf528..4a331b27e 100644
--- a/tilia/ui/timelines/base/element_manager.py
+++ b/tilia/ui/timelines/base/element_manager.py
@@ -3,7 +3,7 @@
import bisect
from typing import Any, Callable, TYPE_CHECKING, TypeVar, Generic, Iterable
-from PyQt6.QtWidgets import QGraphicsItem
+from PySide6.QtWidgets import QGraphicsItem
from tilia.timelines.component_kinds import ComponentKind
from tilia.ui.timelines.element_kinds import get_element_class_by_kind
@@ -70,10 +70,10 @@ def _remove_from_elements_set(self, element: TE) -> None:
try:
self._elements.remove(element)
del self.id_to_element[element.id]
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove element '{element}' from {self}: not in self._elements."
- )
+ ) from e
def get_element(self, id: int) -> TE:
return self.id_to_element[id]
@@ -173,11 +173,11 @@ def _add_to_selected_elements_set(self, element: TE) -> None:
def _remove_from_selected_elements_set(self, element: TE) -> None:
try:
self._selected_elements.remove(element)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove element '{element}' from selected objects of {self}: not"
" in self._selected_elements."
- )
+ ) from e
def get_selected_elements(self) -> list[TE]:
return self._selected_elements
diff --git a/tilia/ui/timelines/base/timeline.py b/tilia/ui/timelines/base/timeline.py
index 0606fd2d5..8d011197f 100644
--- a/tilia/ui/timelines/base/timeline.py
+++ b/tilia/ui/timelines/base/timeline.py
@@ -12,8 +12,8 @@
Callable,
)
-from PyQt6.QtCore import Qt, QPoint
-from PyQt6.QtWidgets import QGraphicsItem
+from PySide6.QtCore import Qt, QPoint
+from PySide6.QtWidgets import QGraphicsItem
from tilia.timelines.component_kinds import ComponentKind
from .context_menus import TimelineUIContextMenu
@@ -538,7 +538,7 @@ def delete_element(self, element: T):
self.element_manager.delete_element(element)
def validate_copy(self, elements: list[T]) -> None:
- """Can be overwritten by subclsses"""
+ """Can be overwritten by subclasses"""
def validate_paste(
self, paste_data: dict, elements_to_receive_paste: list[T]
diff --git a/tilia/ui/timelines/beat/dialogs.py b/tilia/ui/timelines/beat/dialogs.py
index 06c9a422f..c6a28192e 100644
--- a/tilia/ui/timelines/beat/dialogs.py
+++ b/tilia/ui/timelines/beat/dialogs.py
@@ -21,7 +21,7 @@ def validate_result():
return True, list(map(int, result))
-def ask_beat_timeline_fill_method() -> (
- tuple[bool, None | tuple[BeatTimeline, BeatTimeline.FillMethod, float]]
-):
+def ask_beat_timeline_fill_method() -> tuple[
+ bool, None | tuple[BeatTimeline, BeatTimeline.FillMethod, float]
+]:
return FillBeatTimeline.select()
diff --git a/tilia/ui/timelines/beat/element.py b/tilia/ui/timelines/beat/element.py
index 9a5e6b5ab..164d93213 100644
--- a/tilia/ui/timelines/beat/element.py
+++ b/tilia/ui/timelines/beat/element.py
@@ -1,9 +1,9 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Any
-from PyQt6.QtCore import Qt, QLineF, QPointF
-from PyQt6.QtGui import QPen, QColor, QFont
-from PyQt6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsTextItem
+from PySide6.QtCore import Qt, QLineF, QPointF
+from PySide6.QtGui import QPen, QColor, QFont
+from PySide6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsTextItem
from tilia.requests import Post, post, Get, get
from .context_menu import BeatContextMenu
@@ -42,7 +42,8 @@ class BeatUI(TimelineUIElement):
FIELD_NAMES_TO_ATTRIBUTES: dict[
str, str
- ] = {} # only needed if attrs will be set by Inspect
+ ] = {}
+ # only needed if attrs will be set by Inspect
DEFAULT_COPY_ATTRIBUTES = CopyAttributes(
by_element_value=[],
@@ -114,7 +115,7 @@ def seek_time(self):
return self.time
def child_items(self):
- return self.body, self.label
+ return [self.body, self.label]
def update_time(self):
self.update_position()
@@ -132,16 +133,16 @@ def update_label(self):
self.label.set_position(self.x, self.label_y)
def selection_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def left_click_triggers(self):
- return (self.body,)
+ return [self.body]
def on_left_click(self, _) -> None:
self.setup_drag()
def double_left_click_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def on_double_left_click(self, _) -> None:
if self.drag_manager:
@@ -149,9 +150,8 @@ def on_double_left_click(self, _) -> None:
self.drag_manager = None
post(Post.PLAYER_SEEK, self.seek_time)
- @property
def right_click_triggers(self):
- return self.body, self.label
+ return [self.body, self.label]
def setup_drag(self):
self.drag_manager = DragManager(
@@ -265,7 +265,7 @@ def set_position(self, x, y):
def set_text(self, value: str):
if not value:
- # Settting plain text to empty string
+ # Setting plain text to empty string
# keeps the graphics item interactable,
# so we need to hide it, instead.
self.setVisible(False)
diff --git a/tilia/ui/timelines/beat/timeline.py b/tilia/ui/timelines/beat/timeline.py
index cba78b600..c895e3186 100644
--- a/tilia/ui/timelines/beat/timeline.py
+++ b/tilia/ui/timelines/beat/timeline.py
@@ -116,7 +116,7 @@ def on_set_measure_number(self, elements: list[BeatUI] | None = None):
Get.FROM_USER_INT,
"Change measure number",
"Insert measure number",
- min=0,
+ minValue=0,
)
if not accepted:
return False
@@ -143,7 +143,7 @@ def on_set_amount_in_measure(self, elements: list[BeatUI] | None = None):
Get.FROM_USER_INT,
"Change beats in measure",
"Insert amount of beats in measure",
- min=1,
+ minValue=1,
)
if not accepted:
return False
@@ -275,7 +275,7 @@ def update_measure_numbers(self):
# State is being restored and
# beats in measure has not been
# updated yet. This is a dangerous
- # workaroung, as it might conceal
+ # workaround, as it might conceal
# other exceptions. Let's fix this ASAP.
continue
diff --git a/tilia/ui/timelines/collection/collection.py b/tilia/ui/timelines/collection/collection.py
index ded463510..9dfa0a9ac 100644
--- a/tilia/ui/timelines/collection/collection.py
+++ b/tilia/ui/timelines/collection/collection.py
@@ -5,9 +5,8 @@
from enum import Enum, auto
from typing import Any, Optional, Callable, cast
-from PyQt6.QtCore import Qt, QPoint
-from PyQt6.QtWidgets import (
- QGraphicsView,
+from PySide6.QtCore import Qt, QPoint
+from PySide6.QtWidgets import (
QMainWindow,
QGraphicsItem,
QGraphicsScene,
@@ -74,7 +73,7 @@ def __init__(
kind: None for kind in TimelineKind if kind != TlKind.SLIDER_TIMELINE
}
- self._timeline_uis = set()
+ self._timeline_uis: set[TimelineUI] = set()
self._select_order = []
self._timeline_uis_to_playback_line_ids = {}
self.sb_items_to_selected_items = {}
@@ -386,7 +385,7 @@ def _handle_media_not_loaded() -> bool:
"Set duration",
"Insert duration (s)",
value=60,
- min=1,
+ minValue=1,
)
if not success:
return False
@@ -569,11 +568,11 @@ def _add_to_timeline_uis_set(self, timeline_ui: TimelineUI) -> None:
def _remove_from_timeline_uis_set(self, timeline_ui: TimelineUI) -> None:
try:
self._timeline_uis.remove(timeline_ui)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline ui '{timeline_ui}' from {self}: not in"
" self.timeline_uis."
- )
+ ) from e
def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
self._select_order.insert(0, tl_ui)
@@ -581,17 +580,17 @@ def _add_to_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
def _remove_from_timeline_ui_select_order(self, tl_ui: TimelineUI) -> None:
try:
self._select_order.remove(tl_ui)
- except ValueError:
+ except ValueError as e:
raise ValueError(
f"Can't remove timeline ui '{tl_ui}' from select order: not in select"
" order."
- )
+ ) from e
def _send_to_top_of_select_order(self, tl_ui: TimelineUI):
self._select_order.remove(tl_ui)
self._select_order.insert(0, tl_ui)
- def add_timeline_view_to_scene(self, view: QGraphicsView, ordinal: int) -> None:
+ def add_timeline_view_to_scene(self, view: TimelineView, ordinal: int) -> None:
view.proxy = self.scene.addWidget(view)
y = sum(tlui.get_data("height") for tlui in sorted(self)[: ordinal - 1])
view.move(0, y)
@@ -646,7 +645,7 @@ def update_timeline_ui_ordinal(self):
@staticmethod
def update_timeline_times(tlui: TimelineUI):
if tlui.TIMELINE_KIND == TlKind.SLIDER_TIMELINE:
- tlui: SliderTimelineUI
+ tlui = cast(SliderTimelineUI, tlui)
tlui.update_items_position()
else:
tlui.element_manager.update_time_on_elements()
@@ -693,7 +692,7 @@ def _get_timeline_ui_by_view(self, view):
def _on_timeline_ui_right_click(
self,
- view: QGraphicsView,
+ view: TimelineView,
x: int,
y: int,
item: Optional[QGraphicsItem],
@@ -704,8 +703,7 @@ def _on_timeline_ui_right_click(
if not timeline_ui:
raise ValueError(
- f"Can't process left click: no timeline with view '{view}' on"
- f" {self}"
+ f"Can't process left click: no timeline with view '{view}' on {self}"
)
if (
@@ -719,7 +717,7 @@ def _on_timeline_ui_right_click(
def _on_timeline_ui_left_click(
self,
- view: QGraphicsView,
+ view: TimelineView,
x: int,
y: int,
item: Optional[QGraphicsItem],
@@ -916,7 +914,10 @@ def on_hierarchy_merge_split(self, new_units: list, old_units: list):
self._update_loop_elements()
def on_harmony_timeline_components_deserialized(self, id):
- self.get_timeline_ui(id).on_timeline_components_deserialized() # noqa
+ from ..harmony import HarmonyTimelineUI
+
+ timeline_ui = cast(HarmonyTimelineUI, self.get_timeline_ui(id))
+ timeline_ui.on_timeline_components_deserialized()
def on_beat_timeline_components_deserialized(self, id: int):
from tilia.ui.timelines.beat import BeatTimelineUI
@@ -1122,8 +1123,8 @@ def filter_for_pasting(_) -> list[TimelineUI]:
try:
return selector_to_func[selector](get_by_kinds(kinds))
- except KeyError:
- raise NotImplementedError(f"Can't select with {selector=}")
+ except KeyError as e:
+ raise NotImplementedError(f"Can't select with {selector=}") from e
@command_callback
def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI):
@@ -1131,14 +1132,12 @@ def on_timeline_ordinal_permute(self, tlui1: TimelineUI, tlui2: TimelineUI):
@staticmethod
@command_callback
- def on_timeline_delete(timeline_ui: TimelineUI):
- confirmed = get(
+ def on_timeline_delete(timeline_ui: TimelineUI, confirm: bool = True) -> bool:
+ if confirm and not get(
Get.FROM_USER_YES_OR_NO,
"Delete timeline",
"Are you sure you want to delete the selected timeline? This can be undone later.",
- )
-
- if not confirmed:
+ ):
return False
get(Get.TIMELINE_COLLECTION).delete_timeline(timeline_ui.timeline)
@@ -1195,7 +1194,7 @@ def on_timeline_set_height(
"Change timeline height",
"Insert new timeline height",
value=timeline_ui.get_data("height"),
- min=10,
+ minValue=10,
)
if not accepted:
return False
diff --git a/tilia/ui/timelines/collection/import_.py b/tilia/ui/timelines/collection/import_.py
index 06c1089b2..eac6aa614 100644
--- a/tilia/ui/timelines/collection/import_.py
+++ b/tilia/ui/timelines/collection/import_.py
@@ -3,7 +3,6 @@
from typing import TYPE_CHECKING
import tilia.errors
-import tilia.parsers
from tilia.requests import get, Get
from tilia.timelines.timeline_kinds import TimelineKind as TlKind
from tilia.ui.dialogs.by_time_or_by_measure import ByTimeOrByMeasure
@@ -47,7 +46,7 @@ def _on_import_to_timeline(
success, path = get(
Get.FROM_USER_FILE_PATH,
"Import components",
- ["musicXML files (*.musicxml *.mxl, *.xml)"],
+ ["musicXML files (*.musicxml *.mxl *.xml)"],
)
else:
diff --git a/tilia/ui/timelines/collection/requests/args.py b/tilia/ui/timelines/collection/requests/args.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tilia/ui/timelines/collection/scene.py b/tilia/ui/timelines/collection/scene.py
index 7d14d73fb..921cd8f8a 100644
--- a/tilia/ui/timelines/collection/scene.py
+++ b/tilia/ui/timelines/collection/scene.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from PyQt6.QtWidgets import QGraphicsScene
+from PySide6.QtWidgets import QGraphicsScene
class TimelineUIsScene(QGraphicsScene):
diff --git a/tilia/ui/timelines/collection/view.py b/tilia/ui/timelines/collection/view.py
index d0d4d9de4..6e7387649 100644
--- a/tilia/ui/timelines/collection/view.py
+++ b/tilia/ui/timelines/collection/view.py
@@ -1,7 +1,7 @@
from __future__ import annotations
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QGraphicsView, QAbstractSlider
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QGraphicsView, QAbstractSlider
from tilia.ui import commands
from tilia.ui.smooth_scroll import setup_smooth, smooth
diff --git a/tilia/ui/timelines/copy_paste.py b/tilia/ui/timelines/copy_paste.py
index 48fc104bc..a25886d2d 100644
--- a/tilia/ui/timelines/copy_paste.py
+++ b/tilia/ui/timelines/copy_paste.py
@@ -8,10 +8,10 @@
def get_copy_data_from_elements(
- elements: list[tuple[TimelineUIElement, CopyAttributes]]
+ elements: list[tuple[TimelineUIElement, CopyAttributes]],
) -> list[dict]:
copy_data = []
- for element, kind, copy_attrs in elements:
+ for element, copy_attrs in elements:
copy_data.append(get_copy_data_from_element(element, copy_attrs))
return copy_data
diff --git a/tilia/ui/timelines/cursors.py b/tilia/ui/timelines/cursors.py
index 95949187a..f66d417e1 100644
--- a/tilia/ui/timelines/cursors.py
+++ b/tilia/ui/timelines/cursors.py
@@ -1,4 +1,4 @@
-from PyQt6.QtGui import QGuiApplication
+from PySide6.QtGui import QGuiApplication
# noinspection PyUnresolvedReferences
diff --git a/tilia/ui/timelines/harmony/elements/harmony.py b/tilia/ui/timelines/harmony/elements/harmony.py
index ec5bf195d..f496c521d 100644
--- a/tilia/ui/timelines/harmony/elements/harmony.py
+++ b/tilia/ui/timelines/harmony/elements/harmony.py
@@ -1,9 +1,9 @@
from __future__ import annotations
import music21
-from PyQt6.QtCore import QPointF
-from PyQt6.QtGui import QFont, QColor
-from PyQt6.QtWidgets import QGraphicsItem, QGraphicsTextItem
+from PySide6.QtCore import QPointF
+from PySide6.QtGui import QFont, QColor
+from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem
from music21.roman import RomanNumeral
from . import harmony_attrs
diff --git a/tilia/ui/timelines/harmony/elements/mode.py b/tilia/ui/timelines/harmony/elements/mode.py
index 05df9a375..89076aa4a 100644
--- a/tilia/ui/timelines/harmony/elements/mode.py
+++ b/tilia/ui/timelines/harmony/elements/mode.py
@@ -1,7 +1,7 @@
import music21
-from PyQt6.QtCore import QPointF
-from PyQt6.QtGui import QFont, QColor
-from PyQt6.QtWidgets import QGraphicsItem, QGraphicsTextItem
+from PySide6.QtCore import QPointF
+from PySide6.QtGui import QFont, QColor
+from PySide6.QtWidgets import QGraphicsItem, QGraphicsTextItem
from tilia.requests import get, Get, post, Post
from tilia.ui.coords import time_x_converter
diff --git a/tilia/ui/timelines/harmony/level_label.py b/tilia/ui/timelines/harmony/level_label.py
index 0f87c6ba1..4506a8874 100644
--- a/tilia/ui/timelines/harmony/level_label.py
+++ b/tilia/ui/timelines/harmony/level_label.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import QPointF
-from PyQt6.QtGui import QFont, QColor
-from PyQt6.QtWidgets import QGraphicsTextItem
+from PySide6.QtCore import QPointF
+from PySide6.QtGui import QFont, QColor
+from PySide6.QtWidgets import QGraphicsTextItem
class LevelLabel(QGraphicsTextItem):
diff --git a/tilia/ui/timelines/hierarchy/element.py b/tilia/ui/timelines/hierarchy/element.py
index c7f1f62e7..20f851e2e 100644
--- a/tilia/ui/timelines/hierarchy/element.py
+++ b/tilia/ui/timelines/hierarchy/element.py
@@ -3,9 +3,9 @@
from enum import Enum
from typing import Literal
-from PyQt6.QtCore import Qt, QRectF, QPointF
-from PyQt6.QtGui import QColor, QPen, QFont, QFontMetrics, QPixmap
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt, QRectF, QPointF
+from PySide6.QtGui import QColor, QPen, QFont, QFontMetrics, QPixmap
+from PySide6.QtWidgets import (
QGraphicsPixmapItem,
QGraphicsRectItem,
QGraphicsTextItem,
@@ -76,7 +76,7 @@ class HierarchyUI(TimelineUIElement):
support_by_component_value=["start", "pre_start", "end", "level"],
)
- NAME_WHEN_UNLABELED = "Unnamed"
+ NAME_WHEN_UNLABELLED = "Unnamed"
FULL_NAME_SEPARATOR = "-"
UPDATE_TRIGGERS = [
@@ -188,12 +188,12 @@ def get_cropped_label(self, start_x, end_x, label):
@property
def full_name(self) -> str:
- partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELED
+ partial_name = self.get_data("label") or self.NAME_WHEN_UNLABELLED
next_parent = self.get_data("parent")
while next_parent:
- parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELED
+ parent_name = next_parent.get_data("label") or self.NAME_WHEN_UNLABELLED
partial_name = parent_name + self.FULL_NAME_SEPARATOR + partial_name
next_parent = next_parent.parent
@@ -454,8 +454,8 @@ def extremity_to_handle(
HierarchyUI.Extremity.PRE_START: self.pre_start_handle,
HierarchyUI.Extremity.POST_END: self.post_end_handle,
}[extremity]
- except KeyError:
- raise ValueError("Unrecognized extremity")
+ except KeyError as e:
+ raise ValueError("Unrecognized extremity") from e
@staticmethod
def frame_to_body_extremity(
@@ -468,8 +468,8 @@ def frame_to_body_extremity(
HierarchyUI.Extremity.PRE_START: HierarchyUI.Extremity.START,
HierarchyUI.Extremity.POST_END: HierarchyUI.Extremity.END,
}[extremity]
- except KeyError:
- raise ValueError("Unrecognized extremity")
+ except KeyError as e:
+ raise ValueError("Unrecognized extremity") from e
def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle):
try:
@@ -479,8 +479,8 @@ def handle_to_extremity(self, handle: HierarchyBodyHandle | HierarchyFrameHandle
self.pre_start_handle: HierarchyUI.Extremity.PRE_START,
self.post_end_handle: HierarchyUI.Extremity.POST_END,
}[handle]
- except KeyError:
- raise ValueError(f"{handle} if not a handle of {self}")
+ except KeyError as e:
+ raise ValueError(f"{handle} if not a handle of {self}") from e
@staticmethod
def extremity_to_x(extremity: HierarchyUI.Extremity, start_x, end_x):
@@ -501,7 +501,7 @@ def _setup_handle(self, extremity: HierarchyUI.Extremity):
)
def selection_triggers(self):
- return self.body, self.label, self.comments_icon
+ return [self.body, self.label, self.comments_icon]
def left_click_triggers(self):
triggers = [
@@ -531,7 +531,7 @@ def on_double_left_click(self, _) -> None:
post(Post.PLAYER_SEEK, self.seek_time)
def right_click_triggers(self):
- return self.body, self.label, self.comments_icon
+ return [self.body, self.label, self.comments_icon]
def on_select(self) -> None:
self.body.on_select()
@@ -554,7 +554,7 @@ def on_deselect(self) -> None:
def selected_ascendants(self) -> list[HierarchyUI]:
"""Returns hierarchies in the same branch that
- are both selected and higher-leveled than self"""
+ are both selected and higher-levelled than self"""
uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x)
selected_uis = self.timeline_ui.selected_elements
@@ -566,7 +566,7 @@ def selected_ascendants(self) -> list[HierarchyUI]:
def selected_descendants(self) -> list[HierarchyUI]:
"""Returns hierarchies in the same branch that are both
- selected and lower-leveled than self"""
+ selected and lower-levelled than self"""
uis_at_start = self.timeline_ui.get_elements_by_attr("start_x", self.start_x)
selected_uis = self.timeline_ui.selected_elements
diff --git a/tilia/ui/timelines/hierarchy/handles.py b/tilia/ui/timelines/hierarchy/handles.py
index 033fe3c6f..36495f97a 100644
--- a/tilia/ui/timelines/hierarchy/handles.py
+++ b/tilia/ui/timelines/hierarchy/handles.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from PyQt6.QtCore import Qt, QRectF, QPointF, QLineF
-from PyQt6.QtGui import QColor, QPen
-from PyQt6.QtWidgets import QGraphicsRectItem, QGraphicsLineItem, QGraphicsItemGroup
+from PySide6.QtCore import Qt, QRectF, QPointF, QLineF
+from PySide6.QtGui import QColor, QPen
+from PySide6.QtWidgets import QGraphicsRectItem, QGraphicsLineItem, QGraphicsItemGroup
from tilia.ui.timelines.cursors import CursorMixIn
diff --git a/tilia/ui/timelines/hierarchy/key_press_manager.py b/tilia/ui/timelines/hierarchy/key_press_manager.py
index f0f7a34b1..30e7c544d 100644
--- a/tilia/ui/timelines/hierarchy/key_press_manager.py
+++ b/tilia/ui/timelines/hierarchy/key_press_manager.py
@@ -41,10 +41,12 @@ def on_vertical_arrow_press(self, direction: str):
def on_horizontal_arrow_press(self, side: str):
def _get_next_element_in_same_level(elm):
- is_later_at_same_level = (
- lambda h: h.tl_component.start > elm.tl_component.start
- and h.tl_component.level == elm.tl_component.level
- )
+ def is_later_at_same_level(h):
+ return (
+ h.tl_component.start > elm.tl_component.start
+ and h.tl_component.level == elm.tl_component.level
+ )
+
later_elements = self.element_manager.get_elements_by_condition(
is_later_at_same_level
)
@@ -54,10 +56,12 @@ def _get_next_element_in_same_level(elm):
return None
def _get_previous_element_in_same_level(elm):
- is_earlier_at_same_level = (
- lambda h: h.tl_component.start < elm.tl_component.start
- and h.tl_component.level == elm.tl_component.level
- )
+ def is_earlier_at_same_level(h):
+ return (
+ h.tl_component.start < elm.tl_component.start
+ and h.tl_component.level == elm.tl_component.level
+ )
+
earlier_elements = self.element_manager.get_elements_by_condition(
is_earlier_at_same_level
)
diff --git a/tilia/ui/timelines/hierarchy/timeline.py b/tilia/ui/timelines/hierarchy/timeline.py
index 7a9210b88..2cad39416 100644
--- a/tilia/ui/timelines/hierarchy/timeline.py
+++ b/tilia/ui/timelines/hierarchy/timeline.py
@@ -230,10 +230,12 @@ def paste_with_children_into_elements(
self, elements: list[HierarchyUI], data: list[dict]
):
def get_descendants(parent: HierarchyUI):
- is_in_branch = (
- lambda e: e.tl_component.start >= parent.tl_component.start
- and e.tl_component.end <= parent.tl_component.end
- )
+ def is_in_branch(e):
+ return (
+ e.tl_component.start >= parent.tl_component.start
+ and e.tl_component.end <= parent.tl_component.end
+ )
+
elements_in_branch = self.element_manager.get_elements_by_condition(
is_in_branch
)
diff --git a/tilia/ui/timelines/marker/element.py b/tilia/ui/timelines/marker/element.py
index 6871c7856..578e7be1e 100644
--- a/tilia/ui/timelines/marker/element.py
+++ b/tilia/ui/timelines/marker/element.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from PyQt6.QtCore import QPointF, Qt
-from PyQt6.QtGui import QPolygonF, QPen, QColor, QFont
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import QPointF, Qt
+from PySide6.QtGui import QPolygonF, QPen, QColor, QFont
+from PySide6.QtWidgets import (
QGraphicsItem,
QGraphicsPolygonItem,
QGraphicsTextItem,
diff --git a/tilia/ui/timelines/pdf/element.py b/tilia/ui/timelines/pdf/element.py
index 0a1c8c739..759d5e503 100644
--- a/tilia/ui/timelines/pdf/element.py
+++ b/tilia/ui/timelines/pdf/element.py
@@ -1,14 +1,14 @@
from __future__ import annotations
-from PyQt6.QtCore import QPointF, Qt
-from PyQt6.QtGui import (
+from PySide6.QtCore import QPointF, Qt
+from PySide6.QtGui import (
QPen,
QColor,
QFont,
QPixmap,
QFontMetrics,
)
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QGraphicsItem,
QGraphicsTextItem,
QGraphicsPixmapItem,
diff --git a/tilia/ui/timelines/pdf/timeline.py b/tilia/ui/timelines/pdf/timeline.py
index 896f24e9f..d9245cf0e 100644
--- a/tilia/ui/timelines/pdf/timeline.py
+++ b/tilia/ui/timelines/pdf/timeline.py
@@ -3,9 +3,9 @@
import copy
from typing import TYPE_CHECKING
-from PyQt6.QtCore import QPointF
-from PyQt6.QtPdf import QPdfDocument
-from PyQt6.QtPdfWidgets import QPdfView
+from PySide6.QtCore import QPointF
+from PySide6.QtPdf import QPdfDocument
+from PySide6.QtPdfWidgets import QPdfView
import tilia.errors
from tilia.media.player.base import MediaTimeChangeReason
@@ -99,7 +99,7 @@ def page_total(self):
# prevents it from capping the
# number at a value that might be lower than
# the page total in a PDF loaded in the future.
- # Can't use math.inf because PyQt requires an int.
+ # Can't use math.inf because pyside requires an int.
return 99999999
return self.timeline.get_data("page_total")
diff --git a/tilia/ui/timelines/scene.py b/tilia/ui/timelines/scene.py
index b80c1bca5..7601dbeab 100644
--- a/tilia/ui/timelines/scene.py
+++ b/tilia/ui/timelines/scene.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QGraphicsScene, QGraphicsRectItem
-from PyQt6.QtGui import QColor, QPen, QBrush, QFont, QFontMetrics
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QGraphicsScene, QGraphicsRectItem
+from PySide6.QtGui import QColor, QPen, QBrush, QFont, QFontMetrics
from tilia.settings import settings
from tilia.requests import Get, get, listen, Post
diff --git a/tilia/ui/timelines/score/element/barline.py b/tilia/ui/timelines/score/element/barline.py
index a3bdcb2cd..f277087ee 100644
--- a/tilia/ui/timelines/score/element/barline.py
+++ b/tilia/ui/timelines/score/element/barline.py
@@ -1,5 +1,5 @@
-from PyQt6.QtGui import QPen, QColor
-from PyQt6.QtWidgets import QGraphicsLineItem
+from PySide6.QtGui import QPen, QColor
+from PySide6.QtWidgets import QGraphicsLineItem
from tilia.ui.coords import time_x_converter
from tilia.ui.timelines.base.element import TimelineUIElement
@@ -12,6 +12,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.body = None
+ @property
def x(self):
return time_x_converter.get_x_by_time(self.get_data("time"))
@@ -23,7 +24,7 @@ def child_items(self):
def get_body_args(self):
return (
- self.x(),
+ self.x,
self.timeline_ui.staff_y_cache.values(),
)
@@ -41,6 +42,12 @@ def on_components_deserialized(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class BarLineBody:
def __init__(self, x, ys: list[tuple[float, float]]):
@@ -56,5 +63,5 @@ def _setup_lines(self, x: float, ys: list[tuple[float, float]]):
self.set_position(x, ys)
def set_position(self, x: float, ys: list[tuple[float, float]]):
- for line, (y0, y1) in zip(self.lines, ys):
+ for line, (y0, y1) in zip(self.lines, ys, strict=True):
line.setLine(x, y0, x, y1)
diff --git a/tilia/ui/timelines/score/element/clef.py b/tilia/ui/timelines/score/element/clef.py
index 377350d44..f671df1f8 100644
--- a/tilia/ui/timelines/score/element/clef.py
+++ b/tilia/ui/timelines/score/element/clef.py
@@ -1,8 +1,8 @@
from pathlib import Path
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QPixmap
-from PyQt6.QtWidgets import QGraphicsPixmapItem
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QPixmap
+from PySide6.QtWidgets import QGraphicsPixmapItem
from tilia.dirs import IMG_DIR
from tilia.timelines.score.components import Clef
@@ -61,6 +61,12 @@ def selection_triggers(self):
def shorthand(self) -> Clef.Shorthand | None:
return self.tl_component.shorthand()
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class ClefBody(QGraphicsPixmapItem):
def __init__(self, x: float, y: float, height: float, path: Path):
diff --git a/tilia/ui/timelines/score/element/key_signature.py b/tilia/ui/timelines/score/element/key_signature.py
index 2d7577aef..ff6520d6a 100644
--- a/tilia/ui/timelines/score/element/key_signature.py
+++ b/tilia/ui/timelines/score/element/key_signature.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QPixmap
-from PyQt6.QtWidgets import QGraphicsPixmapItem
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QPixmap
+from PySide6.QtWidgets import QGraphicsPixmapItem
from tilia.dirs import IMG_DIR
from tilia.timelines.score.components import Clef
@@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs):
@staticmethod
def _clef_shorthand_to_icon_path_string(shorthand: Clef.Shorthand | None) -> str:
if not shorthand:
- # Key signature not implmemented
+ # Key signature not implemented
# for custom clefs. Using "treble"
# just to prevent a crash
return "treble"
@@ -84,6 +84,12 @@ def on_components_deserialized(self):
self.scene.removeItem(self.body)
self._setup_body()
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class KeySignatureBody(QGraphicsPixmapItem):
def __init__(self, x: float, y: float, height: int, path: str):
diff --git a/tilia/ui/timelines/score/element/note/accidental.py b/tilia/ui/timelines/score/element/note/accidental.py
index e6b9c4fb5..0f79297a4 100644
--- a/tilia/ui/timelines/score/element/note/accidental.py
+++ b/tilia/ui/timelines/score/element/note/accidental.py
@@ -1,8 +1,8 @@
from pathlib import Path
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QPixmap
-from PyQt6.QtWidgets import QGraphicsPixmapItem
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QPixmap
+from PySide6.QtWidgets import QGraphicsPixmapItem
class NoteAccidental(QGraphicsPixmapItem):
diff --git a/tilia/ui/timelines/score/element/note/body.py b/tilia/ui/timelines/score/element/note/body.py
index 056944f29..03c7da390 100644
--- a/tilia/ui/timelines/score/element/note/body.py
+++ b/tilia/ui/timelines/score/element/note/body.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QColor, QPen
-from PyQt6.QtWidgets import QGraphicsRectItem
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor, QPen
+from PySide6.QtWidgets import QGraphicsRectItem
from tilia.timelines.score.components import Note
from tilia.ui.color import get_tinted_color, get_untinted_color
diff --git a/tilia/ui/timelines/score/element/note/ledger_line.py b/tilia/ui/timelines/score/element/note/ledger_line.py
index cb093d7ad..4b60a594f 100644
--- a/tilia/ui/timelines/score/element/note/ledger_line.py
+++ b/tilia/ui/timelines/score/element/note/ledger_line.py
@@ -1,8 +1,8 @@
from enum import Enum
-from PyQt6.QtCore import QLineF
-from PyQt6.QtGui import QPen, QColor
-from PyQt6.QtWidgets import QGraphicsLineItem
+from PySide6.QtCore import QLineF
+from PySide6.QtGui import QPen, QColor
+from PySide6.QtWidgets import QGraphicsLineItem
class NoteLedgerLines:
diff --git a/tilia/ui/timelines/score/element/note/ui.py b/tilia/ui/timelines/score/element/note/ui.py
index 3b9f624d6..684dc21cc 100644
--- a/tilia/ui/timelines/score/element/note/ui.py
+++ b/tilia/ui/timelines/score/element/note/ui.py
@@ -1,7 +1,7 @@
import math
from pathlib import Path
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QGraphicsItem,
)
@@ -242,20 +242,20 @@ def get_accidental_height(accidental: int, scale_factor: float) -> int:
def get_accidental_scale_factor(self):
"""
Scales accidental according to amw = average measure width.
- If amw < visibility_treshold, returns 0, indicating accidentals should be hidden.
- If visibility_treshold < amw < max_size_treshold, scales proportionally with min_scale as a minimum.
- If amw > max_size_treshold, returns 1, indicating accidentals should be fully visible.
+ If amw < visibility_threshold, returns 0, indicating accidentals should be hidden.
+ If visibility_threshold < amw < max_size_threshold, scales proportionally with min_scale as a minimum.
+ If amw > max_size_threshold, returns 1, indicating accidentals should be fully visible.
"""
- visibility_treshold = 30
- max_size_treshold = 180
+ visibility_threshold = 30
+ max_size_threshold = 180
min_scale = 0.5
average_measure_width = self.timeline_ui.average_measure_width()
if not average_measure_width:
return 1
- if average_measure_width < visibility_treshold:
+ if average_measure_width < visibility_threshold:
return 0
return min(
- 1, min_scale + (average_measure_width / max_size_treshold * min_scale)
+ 1, min_scale + (average_measure_width / max_size_threshold * min_scale)
)
def update_color(self):
diff --git a/tilia/ui/timelines/score/element/staff.py b/tilia/ui/timelines/score/element/staff.py
index 856500456..05d31bcf5 100644
--- a/tilia/ui/timelines/score/element/staff.py
+++ b/tilia/ui/timelines/score/element/staff.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import QLineF
-from PyQt6.QtGui import QColor, QPen
-from PyQt6.QtWidgets import QGraphicsLineItem
+from PySide6.QtCore import QLineF
+from PySide6.QtGui import QColor, QPen
+from PySide6.QtWidgets import QGraphicsLineItem
from tilia.requests import get, Get
from tilia.timelines.component_kinds import ComponentKind
@@ -51,6 +51,12 @@ def child_items(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class StaffLines:
COLOR = "gray"
diff --git a/tilia/ui/timelines/score/element/time_signature.py b/tilia/ui/timelines/score/element/time_signature.py
index 7a5c187fc..0443f6fa5 100644
--- a/tilia/ui/timelines/score/element/time_signature.py
+++ b/tilia/ui/timelines/score/element/time_signature.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import QPixmap
-from PyQt6.QtWidgets import QGraphicsItem, QGraphicsPixmapItem
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QPixmap
+from PySide6.QtWidgets import QGraphicsItem, QGraphicsPixmapItem
from tilia.ui.coords import time_x_converter
from tilia.ui.timelines.score.element.with_collision import (
@@ -68,6 +68,12 @@ def on_components_deserialized(self):
def selection_triggers(self):
return []
+ def on_deselect(self):
+ return
+
+ def on_select(self):
+ return
+
class TimeSignatureBody(QGraphicsItem):
def __init__(
@@ -96,7 +102,7 @@ def get_scaled_pixmap(self, digit: int | str, height: int):
def set_numerator_items(self, numerator: int, height: int):
self.numerator_items = []
for i, digit in enumerate(str(numerator)):
- item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self)
+ item = NumberPixmap(self.get_scaled_pixmap(digit, height), self)
item.digit = int(digit)
item.setPos(i * item.pixmap().width(), 0)
self.numerator_items.append(item)
@@ -104,7 +110,7 @@ def set_numerator_items(self, numerator: int, height: int):
def set_denominator_items(self, denominator: int, height: int):
self.denominator_items = []
for i, digit in enumerate(str(denominator)):
- item = QGraphicsPixmapItem(self.get_scaled_pixmap(digit, height), self)
+ item = NumberPixmap(self.get_scaled_pixmap(digit, height), self)
item.digit = int(digit)
item.setPos(i * item.pixmap().width(), item.pixmap().height())
self.denominator_items.append(item)
@@ -145,5 +151,6 @@ def boundingRect(self):
bounding_rect = bounding_rect.united(item.boundingRect())
return bounding_rect
- def paint(self, painter, option, widget):
- ...
+
+class NumberPixmap(QGraphicsPixmapItem):
+ digit = 0
diff --git a/tilia/ui/timelines/score/timeline.py b/tilia/ui/timelines/score/timeline.py
index bf0151e5a..301fc2943 100644
--- a/tilia/ui/timelines/score/timeline.py
+++ b/tilia/ui/timelines/score/timeline.py
@@ -3,9 +3,9 @@
import math
from typing import Callable, Any, Iterable
-from PyQt6.QtCore import Qt, QRectF, QPointF
-from PyQt6.QtGui import QPixmap, QColor
-from PyQt6.QtWidgets import QGraphicsRectItem
+from PySide6.QtCore import Qt, QRectF, QPointF
+from PySide6.QtGui import QPixmap, QColor
+from PySide6.QtWidgets import QGraphicsRectItem
from tilia.dirs import IMG_DIR
import tilia.errors
@@ -448,8 +448,8 @@ def _validate_staff_numbers(self) -> bool:
def average_measure_width(self) -> float:
if self._measure_count == 0:
return 0
- x0 = self.first_bar_line.x()
- x1 = self.last_bar_line.x()
+ x0 = self.first_bar_line.x
+ x1 = self.last_bar_line.x
return (x1 - x0) / self._measure_count
def on_audio_time_change(self, time: float, _) -> None:
diff --git a/tilia/ui/timelines/selection_box.py b/tilia/ui/timelines/selection_box.py
index 296a2d960..8549fbd17 100644
--- a/tilia/ui/timelines/selection_box.py
+++ b/tilia/ui/timelines/selection_box.py
@@ -1,6 +1,6 @@
-from PyQt6.QtCore import QRectF, QPointF, Qt
-from PyQt6.QtGui import QColor, QPen
-from PyQt6.QtWidgets import QGraphicsRectItem
+from PySide6.QtCore import QRectF, QPointF, Qt
+from PySide6.QtGui import QColor, QPen
+from PySide6.QtWidgets import QGraphicsRectItem
from tilia.requests import Post, stop_listening_to_all, post
diff --git a/tilia/ui/timelines/slider/timeline.py b/tilia/ui/timelines/slider/timeline.py
index 19ef75802..65afbebcc 100644
--- a/tilia/ui/timelines/slider/timeline.py
+++ b/tilia/ui/timelines/slider/timeline.py
@@ -1,9 +1,9 @@
from __future__ import annotations
from typing import TYPE_CHECKING
-from PyQt6.QtCore import QRectF, QLineF, Qt
-from PyQt6.QtGui import QPen, QColor, QBrush
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import QRectF, QLineF, Qt
+from PySide6.QtGui import QPen, QColor, QBrush
+from PySide6.QtWidgets import (
QGraphicsEllipseItem,
QGraphicsLineItem,
QGraphicsDropShadowEffect,
diff --git a/tilia/ui/timelines/toolbar.py b/tilia/ui/timelines/toolbar.py
index 00232bb87..e92efd09a 100644
--- a/tilia/ui/timelines/toolbar.py
+++ b/tilia/ui/timelines/toolbar.py
@@ -1,5 +1,5 @@
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QToolBar
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QToolBar
from tilia.ui import commands
diff --git a/tilia/ui/timelines/view.py b/tilia/ui/timelines/view.py
index 9ad1581f8..a0393b0e6 100644
--- a/tilia/ui/timelines/view.py
+++ b/tilia/ui/timelines/view.py
@@ -1,14 +1,20 @@
from typing import Optional
-from PyQt6.QtCore import Qt
-from PyQt6.QtGui import (
+from PySide6.QtCore import Qt
+from PySide6.QtGui import (
QPainter,
QMouseEvent,
QGuiApplication,
QColor,
QBrush,
)
-from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QSizePolicy, QFrame
+from PySide6.QtWidgets import (
+ QFrame,
+ QGraphicsProxyWidget,
+ QGraphicsScene,
+ QGraphicsView,
+ QSizePolicy,
+)
from tilia.settings import settings
from tilia.requests import post, Post, Get, get, listen
@@ -39,7 +45,7 @@ def __init__(self, scene: QGraphicsScene):
)
self.dragging = False
- self.proxy = None # will be set by TimelineUIs
+ self.proxy = QGraphicsProxyWidget() # will be set by TimelineUIs
def on_settings_updated(self, updated_settings):
if "general" in updated_settings:
diff --git a/tilia/ui/windows/about.py b/tilia/ui/windows/about.py
index f715874f6..dd4e28aeb 100644
--- a/tilia/ui/windows/about.py
+++ b/tilia/ui/windows/about.py
@@ -1,6 +1,5 @@
-from pathlib import Path
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
QDialog,
QFrame,
QLabel,
@@ -8,7 +7,7 @@
QScrollArea,
QVBoxLayout,
)
-from re import split, sub
+from re import sub
import tilia.constants
@@ -84,14 +83,9 @@ def __init__(self, parent):
notice = QLabel(tilia.constants.NOTICE)
notice.setWordWrap(True)
- with open(
- Path(__file__).parent.parent.parent.parent / "LICENSE", encoding="utf-8"
- ) as license_file:
- license_text = split(
- "How to Apply These Terms to Your New Programs", license_file.read()
- )[0]
-
- formatted_text = f'{license_text}'
+ formatted_text = (
+ f'{tilia.constants.LICENSE}'
+ )
text_with_links = sub(
"]+)>",
lambda y: f'{y[0][1:-1]}',
diff --git a/tilia/ui/windows/beat_pattern.py b/tilia/ui/windows/beat_pattern.py
index 1ed9566f8..edf495b8c 100644
--- a/tilia/ui/windows/beat_pattern.py
+++ b/tilia/ui/windows/beat_pattern.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import QInputDialog
+from PySide6.QtWidgets import QInputDialog
import tilia.ui.strings
diff --git a/tilia/ui/windows/fill_beat_timeline.py b/tilia/ui/windows/fill_beat_timeline.py
index 330f63a7b..65511eae9 100644
--- a/tilia/ui/windows/fill_beat_timeline.py
+++ b/tilia/ui/windows/fill_beat_timeline.py
@@ -1,6 +1,6 @@
import sys
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
QButtonGroup,
QGridLayout,
QRadioButton,
diff --git a/tilia/ui/windows/inspect.py b/tilia/ui/windows/inspect.py
index 0ba467f31..4cec20757 100644
--- a/tilia/ui/windows/inspect.py
+++ b/tilia/ui/windows/inspect.py
@@ -5,7 +5,7 @@
from typing import Any, Callable, cast
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDockWidget,
QFormLayout,
QLabel,
@@ -19,7 +19,7 @@
QFrame,
)
-from PyQt6.QtCore import Qt, QKeyCombination
+from PySide6.QtCore import Qt, QKeyCombination
from tilia.requests import Post, listen, stop_listening_to_all, post, Get, get
from tilia.utils import get_tilia_class_string
@@ -49,6 +49,7 @@ class Inspect(QDockWidget):
def __init__(self, main_window) -> None:
super().__init__(main_window)
+ self.setObjectName("inspector")
self.setWindowTitle("Inspector")
self.setMinimumWidth(250)
self.setFeatures(
diff --git a/tilia/ui/windows/manage_timelines.py b/tilia/ui/windows/manage_timelines.py
index 321befe52..3840e3608 100644
--- a/tilia/ui/windows/manage_timelines.py
+++ b/tilia/ui/windows/manage_timelines.py
@@ -1,9 +1,9 @@
from typing import Optional
import typing
-from PyQt6 import QtGui
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import (
+from PySide6 import QtGui
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import (
QDialog,
QHBoxLayout,
QVBoxLayout,
diff --git a/tilia/ui/windows/metadata.py b/tilia/ui/windows/metadata.py
index d721c7b44..7eff335e3 100644
--- a/tilia/ui/windows/metadata.py
+++ b/tilia/ui/windows/metadata.py
@@ -1,6 +1,6 @@
import functools
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QDialog,
QFormLayout,
QLabel,
diff --git a/tilia/ui/windows/metadata_edit_fields.py b/tilia/ui/windows/metadata_edit_fields.py
index a4df3a08a..9159c1b03 100644
--- a/tilia/ui/windows/metadata_edit_fields.py
+++ b/tilia/ui/windows/metadata_edit_fields.py
@@ -1,6 +1,6 @@
import functools
-from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox
+from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox
from tilia.requests import Get, get
diff --git a/tilia/ui/windows/metadata_edit_notes.py b/tilia/ui/windows/metadata_edit_notes.py
index b10d590a0..d67764bb0 100644
--- a/tilia/ui/windows/metadata_edit_notes.py
+++ b/tilia/ui/windows/metadata_edit_notes.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox
+from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextEdit, QDialogButtonBox
from tilia.requests import Get, get
diff --git a/tilia/ui/windows/settings.py b/tilia/ui/windows/settings.py
index 368729070..afb0d81e9 100644
--- a/tilia/ui/windows/settings.py
+++ b/tilia/ui/windows/settings.py
@@ -1,4 +1,4 @@
-from PyQt6.QtWidgets import (
+from PySide6.QtWidgets import (
QCheckBox,
QColorDialog,
QComboBox,
@@ -15,7 +15,7 @@
QVBoxLayout,
QWidget,
)
-from PyQt6.QtGui import QColor
+from PySide6.QtGui import QColor
from tilia.settings import settings
from tilia.requests import post, Post
@@ -195,7 +195,7 @@ def pretty_label(input_string: str):
)
-def select_color_button(value, text=None):
+def select_color_button(value, text=""):
def select_color(old_color):
new_color = QColorDialog.getColor(
QColor(old_color),
@@ -212,7 +212,7 @@ def set_color(color):
)
def get_value():
- return button.styleSheet().lstrip("background-color: ").split(";")[0]
+ return button.styleSheet().replace("background-color: ", "").split(";")[0]
button = QPushButton()
button.setText(text)
@@ -231,7 +231,7 @@ def combobox(options: list, current_value: str):
return combobox
-def get_widget_for_value(value, text=None) -> QWidget:
+def get_widget_for_value(value, text="") -> QWidget:
match value:
case bool():
checkbox = QCheckBox()
@@ -284,7 +284,15 @@ def get_widget_for_value(value, text=None) -> QWidget:
raise NotImplementedError
-def get_value_for_widget(widget: QWidget):
+def get_value_for_widget(
+ widget: QSpinBox
+ | QTextEdit
+ | QWidget
+ | QCheckBox
+ | QPushButton
+ | QComboBox
+ | QLineEdit,
+):
match widget.objectName():
case "int":
return widget.value()
@@ -304,7 +312,7 @@ def get_value_for_widget(widget: QWidget):
return widget.isChecked()
case "color":
- return widget.styleSheet().lstrip("background-color: ").split(";")[0]
+ return widget.styleSheet().replace("background-color: ", "").split(";")[0]
case "combobox":
text = widget.currentText().lower()
diff --git a/tilia/ui/windows/svg_viewer.py b/tilia/ui/windows/svg_viewer.py
index f08f01f16..594a673c3 100644
--- a/tilia/ui/windows/svg_viewer.py
+++ b/tilia/ui/windows/svg_viewer.py
@@ -6,14 +6,14 @@
from bisect import bisect
from typing import Callable
-from PyQt6.QtCore import (
+from PySide6.QtCore import (
Qt,
QKeyCombination,
QPointF,
)
-from PyQt6.QtGui import QFont
-from PyQt6.QtSvgWidgets import QGraphicsSvgItem
-from PyQt6.QtWidgets import (
+from PySide6.QtGui import QFont
+from PySide6.QtSvgWidgets import QGraphicsSvgItem
+from PySide6.QtWidgets import (
QFrame,
QGraphicsView,
QGraphicsItem,
@@ -24,7 +24,7 @@
QVBoxLayout,
QWidget,
)
-from PyQt6.QtSvg import QSvgRenderer
+from PySide6.QtSvg import QSvgRenderer
from tilia.ui import commands
from tilia.ui.commands import get_qaction
@@ -408,9 +408,8 @@ def annotation_font_inc(self) -> None:
def _get_time_from_scene_x(self, xs: dict[int, float]) -> dict[int, list[float]]:
output = {}
beat_pos = {}
- beats, x_pos = list(self.beat_x_position.keys()), list(
- self.beat_x_position.values()
- )
+ beats = list(self.beat_x_position.keys())
+ x_pos = list(self.beat_x_position.values())
for key, x in xs.items():
x = round(x, 3)
if x in x_pos:
@@ -476,9 +475,8 @@ def _get_scene_x_from_time(self, time: float) -> float:
beat = beat_tl.get_metric_fraction_by_time(time)
if x := self.beat_x_position.get(beat):
return x
- beats, x_pos = list(self.beat_x_position.keys()), list(
- self.beat_x_position.values()
- )
+ beats = list(self.beat_x_position.keys())
+ x_pos = list(self.beat_x_position.values())
idx = bisect(beats, beat)
if idx == 0:
return x_pos[0]
diff --git a/tilia/ui/windows/view_window.py b/tilia/ui/windows/view_window.py
index 1b7da4458..68268a349 100644
--- a/tilia/ui/windows/view_window.py
+++ b/tilia/ui/windows/view_window.py
@@ -1,7 +1,7 @@
from typing import TypeVar, Generic
-from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QDockWidget, QDialog, QWidget
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QDockWidget, QDialog, QWidget
from tilia.requests import get, Get, post, Post, listen
from tilia.ui.enums import WindowState
diff --git a/tilia/utils.py b/tilia/utils.py
index 0753f03e2..5b0766d1d 100644
--- a/tilia/utils.py
+++ b/tilia/utils.py
@@ -21,3 +21,26 @@ def open_with_os(path: Path) -> None:
subprocess.Popen(["open", path.resolve()], shell=True)
else:
raise OSError(f"Unsupported platform: {sys.platform}")
+
+
+def load_dotenv() -> None:
+ root = Path(__file__).parents[1]
+ dotenv_paths = [root / fn for fn in os.listdir(root) if fn.endswith(".env")]
+
+ if dotenv_paths:
+ import dotenv
+
+ for p in dotenv_paths:
+ with open(p) as f:
+ dotenv.load_dotenv(stream=f)
+
+ else: # setup some basic values
+ os.environ["LOG_REQUESTS"] = "1"
+ os.environ[
+ "EXCLUDE_FROM_LOG"
+ ] = "TIMELINE_VIEW_LEFT_BUTTON_DRAG;PLAYER_CURRENT_TIME_CHANGED;APP_RECORD_STATE"
+ if not os.environ.get("ENVIRONMENT"):
+ os.environ["ENVIRONMENT"] = "dev"
+
+
+load_dotenv()