diff --git a/.editorconfig b/.editorconfig
index a9b080f1..cd5c1f1c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,14 +1,14 @@
-root = true
-
-[*]
-end_of_line = crlf
-insert_final_newline = true
-charset = utf-8
-indent_style = space
-indent_size = 2
-
-[*.py]
-indent_size = 4
-
-[*.ui]
-indent_size = 1
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+
+[*.py]
+indent_size = 4
+
+[*.ui]
+indent_size = 1
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5c0023fe..5d64c44d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,38 +1,38 @@
-repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
- hooks:
- - id: pretty-format-json
- exclude: ".vscode/.*" # Exclude jsonc
- args: [--autofix, --no-sort-keys]
- - id: trailing-whitespace
- args: [--markdown-linebreak-ext=md]
- - id: end-of-file-fixer
- - id: mixed-line-ending
- args: [--fix=crlf]
- - id: check-case-conflict
- - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
- rev: v2.10.0
- hooks:
- - id: pretty-format-ini
- args: [--autofix]
- - repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.1.1" # Must match requirements-dev.txt
- hooks:
- - id: ruff
- args: [--fix]
- - repo: https://github.com/hhatto/autopep8
- rev: "v2.0.4" # Must match requirements-dev.txt
- hooks:
- - id: autopep8
- - repo: https://github.com/asottile/add-trailing-comma
- rev: v3.1.0 # Must match requirements-dev.txt
- hooks:
- - id: add-trailing-comma
-
-ci:
- autoupdate_branch: dev
- skip:
- # Ignore until Linux support. We don't want lf everywhere yet
- # And crlf fails on CI because pre-commit runs on linux
- - "mixed-line-ending"
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: pretty-format-json
+ exclude: ".vscode/.*" # Exclude jsonc
+ args: [--autofix, --no-sort-keys]
+ - id: trailing-whitespace
+ args: [--markdown-linebreak-ext=md]
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args: [--fix=crlf]
+ - id: check-case-conflict
+ - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
+ rev: v2.10.0
+ hooks:
+ - id: pretty-format-ini
+ args: [--autofix]
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: "v0.1.1" # Must match requirements-dev.txt
+ hooks:
+ - id: ruff
+ args: [--fix]
+ - repo: https://github.com/hhatto/autopep8
+ rev: "v2.0.4" # Must match requirements-dev.txt
+ hooks:
+ - id: autopep8
+ - repo: https://github.com/asottile/add-trailing-comma
+ rev: v3.1.0 # Must match requirements-dev.txt
+ hooks:
+ - id: add-trailing-comma
+
+ci:
+ autoupdate_branch: dev
+ skip:
+ # Ignore until Linux support. We don't want lf everywhere yet
+ # And crlf fails on CI because pre-commit runs on linux
+ - "mixed-line-ending"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9ea48ddb..e6590170 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,148 +1,149 @@
-{
- "editor.rulers": [
- 80,
- 120
- ],
- "[git-commit]": {
- "editor.rulers": [
- 72
- ]
- },
- "[markdown]": {
- "files.trimTrailingWhitespace": false,
- },
- "files.trimTrailingWhitespace": true,
- "files.insertFinalNewline": true,
- "files.trimFinalNewlines": true,
- "editor.comments.insertSpace": true,
- "editor.insertSpaces": true,
- "editor.detectIndentation": false,
- "editor.tabSize": 2,
- "editor.formatOnSave": true,
- "editor.codeActionsOnSave": {
- "source.fixAll": true,
- // Let dedicated linter (Ruff) organize imports
- "source.organizeImports": false,
- },
- "emeraldwalk.runonsave": {
- "commands": [
- {
- "match": "\\.pyi?",
- "cmd": "add-trailing-comma ${file}"
- },
- ]
- },
- "files.associations": {
- ".flake8": "properties",
- "*.qrc": "xml",
- "*.ui": "xml"
- },
- "files.exclude": {
- "**/.git": true,
- "**/.svn": true,
- "**/.hg": true,
- "**/CVS": true,
- "**/.DS_Store": true,
- "**/Thumbs.db": true,
- "**/.*_cache": true, // mypy and Ruff cache
- "**/__pycache__": true,
- // Only show useful PyInstaller logs
- "build/*.*": true,
- "build/[b-z]*": true,
- "build/**/localpycs": true,
- "build/**/Tree-*": true,
- "build/**/*.{manifest,pkg,zip,tcl,res,pyz}": true,
- },
- "search.exclude": {
- "**/*.code-search": true,
- "*.lock": true,
- },
- // Set the default formatter to help avoid Prettier
- "[json][jsonc]": {
- "editor.defaultFormatter": "vscode.json-language-features",
- },
- "[python]": {
- // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310
- "editor.defaultFormatter": "ms-python.autopep8",
- "editor.tabSize": 4,
- "editor.rulers": [
- 72, // PEP8-17 docstrings
- // 79, // PEP8-17 default max
- // 88, // Black default
- // 99, // PEP8-17 acceptable max
- 120, // Our hard rule
- ],
- },
- "mypy-type-checker.importStrategy": "fromEnvironment",
- "mypy-type-checker.args": [
- // https://github.com/microsoft/vscode-mypy/issues/37#issuecomment-1602702174
- "--config-file=mypy.ini",
- ],
- "python.terminal.activateEnvironment": true,
- // python.analysis is Pylance (pyright) configurations
- "python.analysis.fixAll": [
- "source.convertImportFormat"
- // Explicitly omiting "source.unusedImports", can be annoying when commenting code for debugging
- ],
- // Important to follow the config in pyrightconfig.json
- "python.analysis.useLibraryCodeForTypes": false,
- "python.analysis.diagnosticMode": "workspace",
- "ruff.importStrategy": "fromEnvironment",
- // Use the Ruff extension instead
- "isort.check": false,
- // linting/formatting options deprecated, use dedicated extensions instead
- // https://github.com/microsoft/vscode-python/wiki/Migration-to-Python-Tools-Extensions
- "python.linting.enabled": false,
- "python.linting.banditEnabled": false,
- "python.linting.flake8Enabled": false,
- "python.linting.prospectorEnabled": false,
- "python.linting.pycodestyleEnabled": false,
- "python.linting.pylamaEnabled": false,
- "python.linting.pylintEnabled": false,
- "python.linting.mypyEnabled": false,
- "python.formatting.provider": "none",
- "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
- "powershell.codeFormatting.autoCorrectAliases": true,
- "powershell.codeFormatting.trimWhitespaceAroundPipe": true,
- "powershell.codeFormatting.useConstantStrings": true,
- "powershell.codeFormatting.useCorrectCasing": true,
- "powershell.codeFormatting.whitespaceBetweenParameters": true,
- "powershell.integratedConsole.showOnStartup": false,
- "terminal.integrated.defaultProfile.windows": "PowerShell",
- "xml.codeLens.enabled": true,
- "xml.format.spaceBeforeEmptyCloseTag": false,
- "xml.format.preserveSpace": [
- // Default
- "xsl:text",
- "xsl:comment",
- "xsl:processing-instruction",
- "literallayout",
- "programlisting",
- "screen",
- "synopsis",
- "pre",
- "xd:pre",
- // Custom
- "string"
- ],
- "[toml]": {
- "editor.defaultFormatter": "tamasfe.even-better-toml"
- },
- "evenBetterToml.formatter.alignComments": false,
- "evenBetterToml.formatter.alignEntries": false,
- "evenBetterToml.formatter.allowedBlankLines": 1,
- "evenBetterToml.formatter.arrayAutoCollapse": true,
- "evenBetterToml.formatter.arrayAutoExpand": true,
- "evenBetterToml.formatter.arrayTrailingComma": true,
- "evenBetterToml.formatter.columnWidth": 80,
- "evenBetterToml.formatter.compactArrays": true,
- "evenBetterToml.formatter.compactEntries": false,
- "evenBetterToml.formatter.compactInlineTables": false,
- "evenBetterToml.formatter.indentEntries": false,
- "evenBetterToml.formatter.indentTables": false,
- "evenBetterToml.formatter.inlineTableExpand": false,
- "evenBetterToml.formatter.reorderArrays": true,
- "evenBetterToml.formatter.trailingNewline": true,
- // We like keeping TOML keys in a certain non-alphabetical order that feels more natural
- "evenBetterToml.formatter.reorderKeys": false
-}
+{
+ "editor.rulers": [
+ 80,
+ 120
+ ],
+ "[git-commit]": {
+ "editor.rulers": [
+ 72
+ ]
+ },
+ "[markdown]": {
+ "files.trimTrailingWhitespace": false,
+ },
+ "files.trimTrailingWhitespace": true,
+ "files.insertFinalNewline": true,
+ "files.trimFinalNewlines": true,
+ "files.eol": "\n",
+ "editor.comments.insertSpace": true,
+ "editor.insertSpaces": true,
+ "editor.detectIndentation": false,
+ "editor.tabSize": 2,
+ "editor.formatOnSave": true,
+ "editor.codeActionsOnSave": {
+ "source.fixAll": true,
+ // Let dedicated linter (Ruff) organize imports
+ "source.organizeImports": false,
+ },
+ "emeraldwalk.runonsave": {
+ "commands": [
+ {
+ "match": "\\.pyi?",
+ "cmd": "add-trailing-comma ${file}"
+ },
+ ]
+ },
+ "files.associations": {
+ ".flake8": "properties",
+ "*.qrc": "xml",
+ "*.ui": "xml"
+ },
+ "files.exclude": {
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true,
+ "**/.*_cache": true, // mypy and Ruff cache
+ "**/__pycache__": true,
+ // Only show useful PyInstaller logs
+ "build/*.*": true,
+ "build/[b-z]*": true,
+ "build/**/localpycs": true,
+ "build/**/Tree-*": true,
+ "build/**/*.{manifest,pkg,zip,tcl,res,pyz}": true,
+ },
+ "search.exclude": {
+ "**/*.code-search": true,
+ "*.lock": true,
+ },
+ // Set the default formatter to help avoid Prettier
+ "[json][jsonc]": {
+ "editor.defaultFormatter": "vscode.json-language-features",
+ },
+ "[python]": {
+ // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310
+ "editor.defaultFormatter": "ms-python.autopep8",
+ "editor.tabSize": 4,
+ "editor.rulers": [
+ 72, // PEP8-17 docstrings
+ // 79, // PEP8-17 default max
+ // 88, // Black default
+ // 99, // PEP8-17 acceptable max
+ 120, // Our hard rule
+ ],
+ },
+ "mypy-type-checker.importStrategy": "fromEnvironment",
+ "mypy-type-checker.args": [
+ // https://github.com/microsoft/vscode-mypy/issues/37#issuecomment-1602702174
+ "--config-file=mypy.ini",
+ ],
+ "python.terminal.activateEnvironment": true,
+ // python.analysis is Pylance (pyright) configurations
+ "python.analysis.fixAll": [
+ "source.convertImportFormat"
+ // Explicitly omiting "source.unusedImports", can be annoying when commenting code for debugging
+ ],
+ // Important to follow the config in pyrightconfig.json
+ "python.analysis.useLibraryCodeForTypes": false,
+ "python.analysis.diagnosticMode": "workspace",
+ "ruff.importStrategy": "fromEnvironment",
+ // Use the Ruff extension instead
+ "isort.check": false,
+ // linting/formatting options deprecated, use dedicated extensions instead
+ // https://github.com/microsoft/vscode-python/wiki/Migration-to-Python-Tools-Extensions
+ "python.linting.enabled": false,
+ "python.linting.banditEnabled": false,
+ "python.linting.flake8Enabled": false,
+ "python.linting.prospectorEnabled": false,
+ "python.linting.pycodestyleEnabled": false,
+ "python.linting.pylamaEnabled": false,
+ "python.linting.pylintEnabled": false,
+ "python.linting.mypyEnabled": false,
+ "python.formatting.provider": "none",
+ "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
+ "powershell.codeFormatting.autoCorrectAliases": true,
+ "powershell.codeFormatting.trimWhitespaceAroundPipe": true,
+ "powershell.codeFormatting.useConstantStrings": true,
+ "powershell.codeFormatting.useCorrectCasing": true,
+ "powershell.codeFormatting.whitespaceBetweenParameters": true,
+ "powershell.integratedConsole.showOnStartup": false,
+ "terminal.integrated.defaultProfile.windows": "PowerShell",
+ "xml.codeLens.enabled": true,
+ "xml.format.spaceBeforeEmptyCloseTag": false,
+ "xml.format.preserveSpace": [
+ // Default
+ "xsl:text",
+ "xsl:comment",
+ "xsl:processing-instruction",
+ "literallayout",
+ "programlisting",
+ "screen",
+ "synopsis",
+ "pre",
+ "xd:pre",
+ // Custom
+ "string"
+ ],
+ "[toml]": {
+ "editor.defaultFormatter": "tamasfe.even-better-toml"
+ },
+ "evenBetterToml.formatter.alignComments": false,
+ "evenBetterToml.formatter.alignEntries": false,
+ "evenBetterToml.formatter.allowedBlankLines": 1,
+ "evenBetterToml.formatter.arrayAutoCollapse": true,
+ "evenBetterToml.formatter.arrayAutoExpand": true,
+ "evenBetterToml.formatter.arrayTrailingComma": true,
+ "evenBetterToml.formatter.columnWidth": 80,
+ "evenBetterToml.formatter.compactArrays": true,
+ "evenBetterToml.formatter.compactEntries": false,
+ "evenBetterToml.formatter.compactInlineTables": false,
+ "evenBetterToml.formatter.indentEntries": false,
+ "evenBetterToml.formatter.indentTables": false,
+ "evenBetterToml.formatter.inlineTableExpand": false,
+ "evenBetterToml.formatter.reorderArrays": true,
+ "evenBetterToml.formatter.trailingNewline": true,
+ // We like keeping TOML keys in a certain non-alphabetical order that feels more natural
+ "evenBetterToml.formatter.reorderKeys": false
+}
diff --git a/pyproject.toml b/pyproject.toml
index 6eba3768..0e872573 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,178 +1,178 @@
-# https://docs.astral.sh/ruff/configuration/
-[tool.ruff]
-target-version = "py39"
-line-length = 120
-select = ["ALL"]
-preview = true
-# https://docs.astral.sh/ruff/rules/
-ignore = [
- ###
- # Not needed or wanted
- ###
- "D1", # pydocstyle Missing doctring
- "D401", # pydocstyle: non-imperative-mood
- "EM", # flake8-errmsg
- "FBT", # flake8-boolean-trap
- "INP", # flake8-no-pep420
- "ISC003", # flake8-implicit-str-concat: explicit-string-concatenation
- # Short messages are still considered "long" messages
- "TRY003", # tryceratops : raise-vanilla-args
- # Don't remove commented code, also too inconsistant
- "ERA001", # eradicate: commented-out-code
- # contextlib.suppress is roughly 3x slower than try/except
- "SIM105", # flake8-simplify: use-contextlib-suppress
- # Checked by type-checker (pyright)
- "ANN", # flake-annotations
- "PGH003", # blanket-type-ignore
- "TCH", # flake8-type-checking
- # Already shown by Pylance, checked by pyright, and can be caused by overloads.
- "ARG002", # Unused method argument
- # We want D213: multi-line-summary-second-line and D211: no-blank-line-before-class
- "D203", # pydocstyle: one-blank-line-before-class
- "D212", # pydocstyle: multi-line-summary-first-line
- # Allow differentiating between broken (FIXME) and to be done/added/completed (TODO)
- "TD001", # flake8-todos: invalid-todo-tag
-
- ###
- # These should be warnings (https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774)
- ###
- "FIX", # flake8-fixme
- # Not all TODOs are worth an issue, this would be better as a warning
- "TD003", # flake8-todos: missing-todo-link
-
- # False-positives
- "TCH004", # https://github.com/astral-sh/ruff/issues/3821
-
- ###
- # Specific to this project
- ###
- "D205", # Not all docstrings have a short description + desrciption
- # We have some Pascal case module names
- "N999", # pep8-naming: Invalid module name
- # Print are used as debug logs
- "T20", # flake8-print
- # This is a relatively small, low contributors project. Git blame suffice.
- "TD002", # missing-todo-author
- # Python 3.11, introduced "zero cost" exception handling
- "PERF203", # try-except-in-loop
-
- ### FIXME/TODO (no warnings in Ruff yet: https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774):
- "CPY001", # flake8-copyright
- "PTH", # flake8-use-pathlib
- # Ignore until linux support
- "EXE", # flake8-executable
-]
-
-[tool.ruff.per-file-ignores]
-"typings/**/*.pyi" = [
- "F811", # Re-exports false positives
- "F821", # https://github.com/astral-sh/ruff/issues/3011
- # The following can't be controlled for external libraries:
- "A", # Shadowing builtin names
- "ICN001", # unconventional-import-alias
- "N8", # Naming conventions
- "PLR0904", # Too many public methods
- "PLR0913", # Argument count
- "PLW3201", # misspelled dunder method name
- "PYI042", # CamelCase TypeAlias
-]
-
-# https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat
-[tool.ruff.flake8-implicit-str-concat]
-allow-multiline = false
-
-# https://docs.astral.sh/ruff/settings/#isort
-[tool.ruff.isort]
-combine-as-imports = true
-split-on-trailing-comma = false
-required-imports = ["from __future__ import annotations"]
-# Unlike isort, Ruff only counts relative imports as local-folder by default for know.
-# https://github.com/astral-sh/ruff/issues/3115
-known-local-folder = [
- "AutoControlledThread",
- "AutoSplit",
- "AutoSplitImage",
- "capture_method",
- "compare",
- "error_messages",
- "gen",
- "hotkeys",
- "menu_bar",
- "region_selection",
- "split_parser",
- "user_profile",
- "utils",
-]
-
-# https://docs.astral.sh/ruff/settings/#mccabe
-[tool.ruff.mccabe]
-# Hard limit, arbitrary to 4 bytes
-max-complexity = 31
-# Arbitrary to 2 bytes, same as SonarLint
-# max-complexity = 15
-
-[tool.ruff.pylint]
-# Arbitrary to 1 byte, same as SonarLint
-max-args = 7
-# At least same as max-complexity
-max-branches = 15
-
-# https://github.com/hhatto/autopep8#usage
-# https://github.com/hhatto/autopep8#more-advanced-usage
-[tool.autopep8]
-max_line_length = 120
-aggressive = 3
-exclude = ".venv/*,src/gen/*"
-ignore = [
- "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma)
- "E70", # Allow ... on same line as def
- # Autofixed by Ruff
- # Check for the "Fix" flag https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
- "E20", # whitespace-after-* & whitespace-before-*
- "E211", # whitespace-before-parameters
- "E231", # missing-whitespace
- "E401", # I001: unsorted-imports
- "E71", # Comparisons
- "E731", # lambda-assignment
- "W29", # Whitespaces
- "W605", # invalid-escape-sequence
-]
-
-# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file
-[tool.pyright]
-typeCheckingMode = "strict"
-# Prefer `pyright: ignore`
-enableTypeIgnoreComments = false
-# Extra strict
-reportImplicitOverride = "error"
-reportImplicitStringConcatenation = "error"
-reportCallInDefaultInitializer = "error"
-reportMissingSuperCall = "none" # False positives on base classes
-reportPropertyTypeMismatch = "error"
-reportUninitializedInstanceVariable = "error"
-reportUnnecessaryTypeIgnoreComment = "error"
-# Exclude from scanning when running pyright
-exclude = [
- ".venv/",
- # Auto generated, fails some strict pyright checks
- "build/",
- "src/gen/",
-]
-# Ignore must be specified for Pylance to stop displaying errors
-ignore = [
- # We expect stub files to be incomplete or contain useless statements
- "**/*.pyi",
-]
-reportUnusedCallResult = "none"
-# Type stubs may not be completable
-reportMissingTypeStubs = "warning"
-# False positives with TYPE_CHECKING
-reportImportCycles = "information"
-# False positives with PySide .connect
-reportFunctionMemberAccess = "none"
-# Extra runtime safety
-reportUnnecessaryComparison = "warning"
-# Using Flake8/Ruff instead
-reportUnusedImport = "none"
-# numpy has way too many complex types that triggers this
-reportUnknownMemberType = "none"
+# https://docs.astral.sh/ruff/configuration/
+[tool.ruff]
+target-version = "py39"
+line-length = 120
+select = ["ALL"]
+preview = true
+# https://docs.astral.sh/ruff/rules/
+ignore = [
+ ###
+ # Not needed or wanted
+ ###
+ "D1", # pydocstyle Missing doctring
+ "D401", # pydocstyle: non-imperative-mood
+ "EM", # flake8-errmsg
+ "FBT", # flake8-boolean-trap
+ "INP", # flake8-no-pep420
+ "ISC003", # flake8-implicit-str-concat: explicit-string-concatenation
+ # Short messages are still considered "long" messages
+ "TRY003", # tryceratops : raise-vanilla-args
+ # Don't remove commented code, also too inconsistant
+ "ERA001", # eradicate: commented-out-code
+ # contextlib.suppress is roughly 3x slower than try/except
+ "SIM105", # flake8-simplify: use-contextlib-suppress
+ # Checked by type-checker (pyright)
+ "ANN", # flake-annotations
+ "PGH003", # blanket-type-ignore
+ "TCH", # flake8-type-checking
+ # Already shown by Pylance, checked by pyright, and can be caused by overloads.
+ "ARG002", # Unused method argument
+ # We want D213: multi-line-summary-second-line and D211: no-blank-line-before-class
+ "D203", # pydocstyle: one-blank-line-before-class
+ "D212", # pydocstyle: multi-line-summary-first-line
+ # Allow differentiating between broken (FIXME) and to be done/added/completed (TODO)
+ "TD001", # flake8-todos: invalid-todo-tag
+
+ ###
+ # These should be warnings (https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774)
+ ###
+ "FIX", # flake8-fixme
+ # Not all TODOs are worth an issue, this would be better as a warning
+ "TD003", # flake8-todos: missing-todo-link
+
+ # False-positives
+ "TCH004", # https://github.com/astral-sh/ruff/issues/3821
+
+ ###
+ # Specific to this project
+ ###
+ "D205", # Not all docstrings have a short description + desrciption
+ # We have some Pascal case module names
+ "N999", # pep8-naming: Invalid module name
+ # Print are used as debug logs
+ "T20", # flake8-print
+ # This is a relatively small, low contributors project. Git blame suffice.
+ "TD002", # missing-todo-author
+ # Python 3.11, introduced "zero cost" exception handling
+ "PERF203", # try-except-in-loop
+
+ ### FIXME/TODO (no warnings in Ruff yet: https://github.com/astral-sh/ruff/issues/1256 & https://github.com/astral-sh/ruff/issues/1774):
+ "CPY001", # flake8-copyright
+ "PTH", # flake8-use-pathlib
+ # Ignore until linux support
+ "EXE", # flake8-executable
+]
+
+[tool.ruff.per-file-ignores]
+"typings/**/*.pyi" = [
+ "F811", # Re-exports false positives
+ "F821", # https://github.com/astral-sh/ruff/issues/3011
+ # The following can't be controlled for external libraries:
+ "A", # Shadowing builtin names
+ "ICN001", # unconventional-import-alias
+ "N8", # Naming conventions
+ "PLR0904", # Too many public methods
+ "PLR0913", # Argument count
+ "PLW3201", # misspelled dunder method name
+ "PYI042", # CamelCase TypeAlias
+]
+
+# https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat
+[tool.ruff.flake8-implicit-str-concat]
+allow-multiline = false
+
+# https://docs.astral.sh/ruff/settings/#isort
+[tool.ruff.isort]
+combine-as-imports = true
+split-on-trailing-comma = false
+required-imports = ["from __future__ import annotations"]
+# Unlike isort, Ruff only counts relative imports as local-folder by default for know.
+# https://github.com/astral-sh/ruff/issues/3115
+known-local-folder = [
+ "AutoControlledThread",
+ "AutoSplit",
+ "AutoSplitImage",
+ "capture_method",
+ "compare",
+ "error_messages",
+ "gen",
+ "hotkeys",
+ "menu_bar",
+ "region_selection",
+ "split_parser",
+ "user_profile",
+ "utils",
+]
+
+# https://docs.astral.sh/ruff/settings/#mccabe
+[tool.ruff.mccabe]
+# Hard limit, arbitrary to 4 bytes
+max-complexity = 31
+# Arbitrary to 2 bytes, same as SonarLint
+# max-complexity = 15
+
+[tool.ruff.pylint]
+# Arbitrary to 1 byte, same as SonarLint
+max-args = 7
+# At least same as max-complexity
+max-branches = 15
+
+# https://github.com/hhatto/autopep8#usage
+# https://github.com/hhatto/autopep8#more-advanced-usage
+[tool.autopep8]
+max_line_length = 120
+aggressive = 3
+exclude = ".venv/*,src/gen/*"
+ignore = [
+ "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma)
+ "E70", # Allow ... on same line as def
+ # Autofixed by Ruff
+ # Check for the "Fix" flag https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
+ "E20", # whitespace-after-* & whitespace-before-*
+ "E211", # whitespace-before-parameters
+ "E231", # missing-whitespace
+ "E401", # I001: unsorted-imports
+ "E71", # Comparisons
+ "E731", # lambda-assignment
+ "W29", # Whitespaces
+ "W605", # invalid-escape-sequence
+]
+
+# https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file
+[tool.pyright]
+typeCheckingMode = "strict"
+# Prefer `pyright: ignore`
+enableTypeIgnoreComments = false
+# Extra strict
+reportImplicitOverride = "error"
+reportImplicitStringConcatenation = "error"
+reportCallInDefaultInitializer = "error"
+reportMissingSuperCall = "none" # False positives on base classes
+reportPropertyTypeMismatch = "error"
+reportUninitializedInstanceVariable = "error"
+reportUnnecessaryTypeIgnoreComment = "error"
+# Exclude from scanning when running pyright
+exclude = [
+ ".venv/",
+ # Auto generated, fails some strict pyright checks
+ "build/",
+ "src/gen/",
+]
+# Ignore must be specified for Pylance to stop displaying errors
+ignore = [
+ # We expect stub files to be incomplete or contain useless statements
+ "**/*.pyi",
+]
+reportUnusedCallResult = "none"
+# Type stubs may not be completable
+reportMissingTypeStubs = "warning"
+# False positives with TYPE_CHECKING
+reportImportCycles = "information"
+# False positives with PySide .connect
+reportFunctionMemberAccess = "none"
+# Extra runtime safety
+reportUnnecessaryComparison = "warning"
+# Using Flake8/Ruff instead
+reportUnusedImport = "none"
+# numpy has way too many complex types that triggers this
+reportUnknownMemberType = "none"
diff --git a/res/about.ui b/res/about.ui
index b0e88fd9..861300cb 100644
--- a/res/about.ui
+++ b/res/about.ui
@@ -1,158 +1,158 @@
-
-
- Toufool
- AboutAutoSplitWidget
-
-
-
- 0
- 0
- 264
- 250
-
-
-
-
- 264
- 250
-
-
-
-
- 264
- 250
-
-
-
-
- 9
-
-
-
- About AutoSplit
-
-
-
- :/resources/icon.ico:/resources/icon.ico
-
-
-
-
- 180
- 220
- 75
- 24
-
-
-
- OK
-
-
-
-
-
- 10
- 44
- 241
- 32
-
-
-
- <html><head/><body><p>Created by <a href="https://twitter.com/toufool"><span style=" text-decoration: underline; color:#0000ff;">Toufool</span></a> and <a href="https://twitter.com/faschz"><span style=" text-decoration: underline; color:#0000ff;">Faschz</span></a><br/>Maintained by <a href="https://twitter.com/Avasam06"><span style=" text-decoration: underline; color:#0000ff;">Avasam</span></a></p></body></html>
-
-
-
-
-
- 10
- 21
- 241
- 16
-
-
-
- Version:
-
-
-
-
-
- 10
- 90
- 241
- 51
-
-
-
- If you enjoy using this program,
-please consider donating.
-Thank you!
-
-
- Qt::AlignCenter
-
-
-
-
-
- 60
- 150
- 147
- 51
-
-
-
- <html><head/><body><p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=BYRHQG69YRHBA&item_name=AutoSplit+development&currency_code=USD&source=url"><img src=":/resources/btn_donateCC_LG.png"/></a></p></body></html>
-
-
- Qt::AlignCenter
-
-
-
-
-
- 190
- 17
- 64
- 64
-
-
-
-
-
-
- :/resources/icon.ico
-
-
- true
-
-
- icon_label
- donate_text_label
- version_label
- created_by_label
- ok_button
- donate_button_label
-
-
-
-
-
-
- ok_button
- clicked()
- AboutAutoSplitWidget
- close()
-
-
- 225
- 210
-
-
- 153
- 114
-
-
-
-
-
+
+
+ Toufool
+ AboutAutoSplitWidget
+
+
+
+ 0
+ 0
+ 264
+ 250
+
+
+
+
+ 264
+ 250
+
+
+
+
+ 264
+ 250
+
+
+
+
+ 9
+
+
+
+ About AutoSplit
+
+
+
+ :/resources/icon.ico:/resources/icon.ico
+
+
+
+
+ 180
+ 220
+ 75
+ 24
+
+
+
+ OK
+
+
+
+
+
+ 10
+ 44
+ 241
+ 32
+
+
+
+ <html><head/><body><p>Created by <a href="https://twitter.com/toufool"><span style=" text-decoration: underline; color:#0000ff;">Toufool</span></a> and <a href="https://twitter.com/faschz"><span style=" text-decoration: underline; color:#0000ff;">Faschz</span></a><br/>Maintained by <a href="https://twitter.com/Avasam06"><span style=" text-decoration: underline; color:#0000ff;">Avasam</span></a></p></body></html>
+
+
+
+
+
+ 10
+ 21
+ 241
+ 16
+
+
+
+ Version:
+
+
+
+
+
+ 10
+ 90
+ 241
+ 51
+
+
+
+ If you enjoy using this program,
+please consider donating.
+Thank you!
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 60
+ 150
+ 147
+ 51
+
+
+
+ <html><head/><body><p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=BYRHQG69YRHBA&item_name=AutoSplit+development&currency_code=USD&source=url"><img src=":/resources/btn_donateCC_LG.png"/></a></p></body></html>
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ 190
+ 17
+ 64
+ 64
+
+
+
+
+
+
+ :/resources/icon.ico
+
+
+ true
+
+
+ icon_label
+ donate_text_label
+ version_label
+ created_by_label
+ ok_button
+ donate_button_label
+
+
+
+
+
+
+ ok_button
+ clicked()
+ AboutAutoSplitWidget
+ close()
+
+
+ 225
+ 210
+
+
+ 153
+ 114
+
+
+
+
+
diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt
index ab233e24..1f7aadf4 100644
--- a/scripts/requirements-dev.txt
+++ b/scripts/requirements-dev.txt
@@ -1,26 +1,26 @@
-# Usage: ./scripts/install.ps1
-#
-# If you're having issues with the libraries, you might want to first run:
-# pip uninstall -y -r ./scripts/requirements-dev.txt
-#
-# Dependencies
--r requirements.txt
-#
-# Linters & Formatters
-add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml
-autopep8>=2.0.4 # Must match .pre-commit-config.yaml
-ruff>=0.1.1 # New checks # Must match .pre-commit-config.yaml
-#
-# Run `./scripts/designer.ps1` to quickly open the bundled Qt Designer.
-# Can also be downloaded externally as a non-python package
-# qt6-applications
-# Types
-types-D3DShot ; sys_platform == 'win32'
-types-keyboard
-types-Pillow
-types-psutil
-types-PyAutoGUI
-types-pyinstaller
-types-pywin32 ; sys_platform == 'win32'
-types-requests
-types-toml
+# Usage: ./scripts/install.ps1
+#
+# If you're having issues with the libraries, you might want to first run:
+# pip uninstall -y -r ./scripts/requirements-dev.txt
+#
+# Dependencies
+-r requirements.txt
+#
+# Linters & Formatters
+add-trailing-comma>=3.1.0 # Must match .pre-commit-config.yaml
+autopep8>=2.0.4 # Must match .pre-commit-config.yaml
+ruff>=0.1.1 # New checks # Must match .pre-commit-config.yaml
+#
+# Run `./scripts/designer.ps1` to quickly open the bundled Qt Designer.
+# Can also be downloaded externally as a non-python package
+# qt6-applications
+# Types
+types-D3DShot ; sys_platform == 'win32'
+types-keyboard
+types-Pillow
+types-psutil
+types-PyAutoGUI
+types-pyinstaller
+types-pywin32 ; sys_platform == 'win32'
+types-requests
+types-toml
diff --git a/scripts/requirements.txt b/scripts/requirements.txt
index 564d7e67..11dea0e7 100644
--- a/scripts/requirements.txt
+++ b/scripts/requirements.txt
@@ -1,31 +1,31 @@
-# Requirements file for AutoSplit
-#
-# Read /docs/build%20instructions.md for more information on how to install, run and build the python code.
-#
-# Dependencies:
-certifi
-ImageHash>=4.3.1 # Contains type information + setup as package not module
-git+https://github.com/boppreh/keyboard.git#egg=keyboard # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568
-numpy>=1.23.2 # Python 3.11 wheels
-opencv-python-headless>=4.8.1.78 # Typing fixes
-packaging
-Pillow>=9.2 # gnome-screeshot checks
-psutil
-PyAutoGUI
-PyWinCtl>=0.0.42 # py.typed
-PySide6-Essentials>=6.5.1 # fixes incomplete tuple return types https://bugreports.qt.io/browse/PYSIDE-2285
-requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534
-toml
-typing-extensions>=4.4.0 # @override decorator support
-#
-# Build and compile resources
-pyinstaller>=5.5 # Python 3.11 support
-pyinstaller-hooks-contrib>=2022.9 # opencv-python 4.6 support. Changes for pywintypes and comtypes
-#
-# https://peps.python.org/pep-0508/#environment-markers
-#
-# Windows-only dependencies:
-pygrabber>=0.2 ; sys_platform == 'win32' # Completed types
-pywin32>=301 ; sys_platform == 'win32'
-winsdk>=v1.0.0b7 ; sys_platform == 'win32' # Python 3.11 support
-git+https://github.com/ranchen421/D3DShot.git#egg=D3DShot ; sys_platform == 'win32' # D3DShot from PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5
+# Requirements file for AutoSplit
+#
+# Read /docs/build%20instructions.md for more information on how to install, run and build the python code.
+#
+# Dependencies:
+certifi
+ImageHash>=4.3.1 # Contains type information + setup as package not module
+git+https://github.com/boppreh/keyboard.git#egg=keyboard # Fix install on macos and linux-ci https://github.com/boppreh/keyboard/pull/568
+numpy>=1.23.2 # Python 3.11 wheels
+opencv-python-headless>=4.8.1.78 # Typing fixes
+packaging
+Pillow>=9.2 # gnome-screeshot checks
+psutil
+PyAutoGUI
+PyWinCtl>=0.0.42 # py.typed
+PySide6-Essentials>=6.5.1 # fixes incomplete tuple return types https://bugreports.qt.io/browse/PYSIDE-2285
+requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534
+toml
+typing-extensions>=4.4.0 # @override decorator support
+#
+# Build and compile resources
+pyinstaller>=5.5 # Python 3.11 support
+pyinstaller-hooks-contrib>=2022.9 # opencv-python 4.6 support. Changes for pywintypes and comtypes
+#
+# https://peps.python.org/pep-0508/#environment-markers
+#
+# Windows-only dependencies:
+pygrabber>=0.2 ; sys_platform == 'win32' # Completed types
+pywin32>=301 ; sys_platform == 'win32'
+winsdk>=v1.0.0b7 ; sys_platform == 'win32' # Python 3.11 support
+git+https://github.com/ranchen421/D3DShot.git#egg=D3DShot ; sys_platform == 'win32' # D3DShot from PyPI with Pillow>=7.2.0 will install 0.1.3 instead of 0.1.5
diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py
index e68093db..1f62fd75 100644
--- a/src/capture_method/__init__.py
+++ b/src/capture_method/__init__.py
@@ -1,208 +1,208 @@
-from __future__ import annotations
-
-import asyncio
-from collections import OrderedDict
-from dataclasses import dataclass
-from enum import Enum, EnumMeta, auto, unique
-from itertools import starmap
-from typing import TYPE_CHECKING, NoReturn, TypedDict, cast
-
-from _ctypes import COMError
-from pygrabber.dshow_graph import FilterGraph
-from typing_extensions import Never, override
-
-from capture_method.BitBltCaptureMethod import BitBltCaptureMethod
-from capture_method.CaptureMethodBase import CaptureMethodBase
-from capture_method.DesktopDuplicationCaptureMethod import DesktopDuplicationCaptureMethod
-from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod
-from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod
-from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod
-from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device
-
-if TYPE_CHECKING:
- from AutoSplit import AutoSplit
-
-
-class Region(TypedDict):
- x: int
- y: int
- width: int
- height: int
-
-
-class CaptureMethodMeta(EnumMeta):
- # Allow checking if simple string is enum
- @override
- def __contains__(self, other: object):
- try:
- self(other)
- except ValueError:
- return False
- return True
-
-
-@unique
-# TODO: Try StrEnum in Python 3.11
-class CaptureMethodEnum(Enum, metaclass=CaptureMethodMeta):
- # Allow TOML to save as a simple string
- @override
- def __repr__(self):
- return self.value
-
- @override
- def __eq__(self, other: object):
- if isinstance(other, str):
- return self.value == other
- if isinstance(other, Enum):
- return self.value == other.value
- return other == self
-
- # Restore hashing functionality for use in Maps
- @override
- def __hash__(self):
- return self.value.__hash__()
-
- # https://github.com/python/typeshed/issues/10428
- @override
- def _generate_next_value_( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
- name: str | CaptureMethodEnum, # noqa: N805
- *_,
- ):
- return name
-
- NONE = ""
- BITBLT = auto()
- WINDOWS_GRAPHICS_CAPTURE = auto()
- PRINTWINDOW_RENDERFULLCONTENT = auto()
- DESKTOP_DUPLICATION = auto()
- VIDEO_CAPTURE_DEVICE = auto()
-
-
-class CaptureMethodDict(OrderedDict[CaptureMethodEnum, type[CaptureMethodBase]]):
- def get_index(self, capture_method: str | CaptureMethodEnum):
- """Returns 0 if the capture_method is invalid or unsupported."""
- try:
- return list(self.keys()).index(cast(CaptureMethodEnum, capture_method))
- except ValueError:
- return 0
-
- def get_method_by_index(self, index: int):
- """
- Returns the `CaptureMethodEnum` at index.
- If index is invalid, returns the first (default) `CaptureMethodEnum`.
- Returns `CaptureMethodEnum.NONE` if there are no capture methods available.
- """
- if len(self) <= 0:
- return CaptureMethodEnum.NONE
- if index <= 0:
- return first(self)
- return list(self.keys())[index]
-
- # Disallow unsafe get w/o breaking it at runtime
- @override
- def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
- self,
- __key: Never,
- ) -> NoReturn | type[CaptureMethodBase]:
- return super().__getitem__(__key)
-
- @override
- def get(self, key: CaptureMethodEnum, __default: object = None):
- """
- Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available,
- else defaults to the first available `CaptureMethodEnum`.
- Returns `CaptureMethodBase` directly if there's no capture methods.
- """
- if key == CaptureMethodEnum.NONE or len(self) <= 0:
- return CaptureMethodBase
- return super().get(key, first(self.values()))
-
-
-CAPTURE_METHODS = CaptureMethodDict()
-if ( # Windows Graphics Capture requires a minimum Windows Build
- WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD
- # Our current implementation of Windows Graphics Capture does not ensure we can get an ID3DDevice
- and try_get_direct3d_device()
-):
- CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod
-CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod
-try: # Test for laptop cross-GPU Desktop Duplication issue
- import d3dshot
-
- d3dshot.create(capture_output="numpy")
-except (ModuleNotFoundError, COMError):
- pass
-else:
- CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod
-CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod
-CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod
-
-
-def change_capture_method(selected_capture_method: CaptureMethodEnum, autosplit: AutoSplit):
- autosplit.capture_method.close(autosplit)
- autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method)(autosplit)
- if selected_capture_method == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE:
- autosplit.select_region_button.setDisabled(True)
- autosplit.select_window_button.setDisabled(True)
- else:
- autosplit.select_region_button.setDisabled(False)
- autosplit.select_window_button.setDisabled(False)
-
-
-@dataclass
-class CameraInfo:
- device_id: int
- name: str
- occupied: bool
- backend: str
- resolution: tuple[int, int]
-
-
-def get_input_device_resolution(index: int):
- filter_graph = FilterGraph()
- try:
- filter_graph.add_video_input_device(index)
- # This can happen with virtual cameras throwing errors.
- # For example since OBS 29.1 updated FFMPEG breaking VirtualCam 3.0
- # https://github.com/Toufool/AutoSplit/issues/238
- except COMError:
- return None
- resolution = filter_graph.get_input_device().get_current_format()
- filter_graph.remove_filters()
- return resolution
-
-
-async def get_all_video_capture_devices() -> list[CameraInfo]:
- named_video_inputs = FilterGraph().get_input_devices()
-
- async def get_camera_info(index: int, device_name: str):
- backend = ""
- # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use
- # #169
- # FIXME: Maybe offer the option to the user to obtain more info about their devies?
- # Off by default. With a tooltip to explain the risk.
- # video_capture = cv2.VideoCapture(index)
- # video_capture.setExceptionMode(True)
- # try:
- # # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d
- # backend = video_capture.getBackendName() # STS_ASSERT
- # video_capture.grab() # STS_ERROR
- # except cv2.error as error:
- # return CameraInfo(index, device_name, True, backend) \
- # if error.code in (cv2.Error.STS_ERROR, cv2.Error.STS_ASSERT) \
- # else None
- # finally:
- # video_capture.release()
-
- resolution = get_input_device_resolution(index)
- return CameraInfo(index, device_name, False, backend, resolution) \
- if resolution is not None \
- else None
-
- return [
- camera_info
- for camera_info
- # Note: Return type required https://github.com/python/typeshed/issues/2652
- in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs)))
- if camera_info is not None
- ]
+from __future__ import annotations
+
+import asyncio
+from collections import OrderedDict
+from dataclasses import dataclass
+from enum import Enum, EnumMeta, auto, unique
+from itertools import starmap
+from typing import TYPE_CHECKING, NoReturn, TypedDict, cast
+
+from _ctypes import COMError
+from pygrabber.dshow_graph import FilterGraph
+from typing_extensions import Never, override
+
+from capture_method.BitBltCaptureMethod import BitBltCaptureMethod
+from capture_method.CaptureMethodBase import CaptureMethodBase
+from capture_method.DesktopDuplicationCaptureMethod import DesktopDuplicationCaptureMethod
+from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod
+from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod
+from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod
+from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device
+
+if TYPE_CHECKING:
+ from AutoSplit import AutoSplit
+
+
+class Region(TypedDict):
+ x: int
+ y: int
+ width: int
+ height: int
+
+
+class CaptureMethodMeta(EnumMeta):
+ # Allow checking if simple string is enum
+ @override
+ def __contains__(self, other: object):
+ try:
+ self(other)
+ except ValueError:
+ return False
+ return True
+
+
+@unique
+# TODO: Try StrEnum in Python 3.11
+class CaptureMethodEnum(Enum, metaclass=CaptureMethodMeta):
+ # Allow TOML to save as a simple string
+ @override
+ def __repr__(self):
+ return self.value
+
+ @override
+ def __eq__(self, other: object):
+ if isinstance(other, str):
+ return self.value == other
+ if isinstance(other, Enum):
+ return self.value == other.value
+ return other == self
+
+ # Restore hashing functionality for use in Maps
+ @override
+ def __hash__(self):
+ return self.value.__hash__()
+
+ # https://github.com/python/typeshed/issues/10428
+ @override
+ def _generate_next_value_( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
+ name: str | CaptureMethodEnum, # noqa: N805
+ *_,
+ ):
+ return name
+
+ NONE = ""
+ BITBLT = auto()
+ WINDOWS_GRAPHICS_CAPTURE = auto()
+ PRINTWINDOW_RENDERFULLCONTENT = auto()
+ DESKTOP_DUPLICATION = auto()
+ VIDEO_CAPTURE_DEVICE = auto()
+
+
+class CaptureMethodDict(OrderedDict[CaptureMethodEnum, type[CaptureMethodBase]]):
+ def get_index(self, capture_method: str | CaptureMethodEnum):
+ """Returns 0 if the capture_method is invalid or unsupported."""
+ try:
+ return list(self.keys()).index(cast(CaptureMethodEnum, capture_method))
+ except ValueError:
+ return 0
+
+ def get_method_by_index(self, index: int):
+ """
+ Returns the `CaptureMethodEnum` at index.
+ If index is invalid, returns the first (default) `CaptureMethodEnum`.
+ Returns `CaptureMethodEnum.NONE` if there are no capture methods available.
+ """
+ if len(self) <= 0:
+ return CaptureMethodEnum.NONE
+ if index <= 0:
+ return first(self)
+ return list(self.keys())[index]
+
+ # Disallow unsafe get w/o breaking it at runtime
+ @override
+ def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
+ self,
+ __key: Never,
+ ) -> NoReturn | type[CaptureMethodBase]:
+ return super().__getitem__(__key)
+
+ @override
+ def get(self, key: CaptureMethodEnum, __default: object = None):
+ """
+ Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available,
+ else defaults to the first available `CaptureMethodEnum`.
+ Returns `CaptureMethodBase` directly if there's no capture methods.
+ """
+ if key == CaptureMethodEnum.NONE or len(self) <= 0:
+ return CaptureMethodBase
+ return super().get(key, first(self.values()))
+
+
+CAPTURE_METHODS = CaptureMethodDict()
+if ( # Windows Graphics Capture requires a minimum Windows Build
+ WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD
+ # Our current implementation of Windows Graphics Capture does not ensure we can get an ID3DDevice
+ and try_get_direct3d_device()
+):
+ CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod
+CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod
+try: # Test for laptop cross-GPU Desktop Duplication issue
+ import d3dshot
+
+ d3dshot.create(capture_output="numpy")
+except (ModuleNotFoundError, COMError):
+ pass
+else:
+ CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod
+CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod
+CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod
+
+
+def change_capture_method(selected_capture_method: CaptureMethodEnum, autosplit: AutoSplit):
+ autosplit.capture_method.close(autosplit)
+ autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method)(autosplit)
+ if selected_capture_method == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE:
+ autosplit.select_region_button.setDisabled(True)
+ autosplit.select_window_button.setDisabled(True)
+ else:
+ autosplit.select_region_button.setDisabled(False)
+ autosplit.select_window_button.setDisabled(False)
+
+
+@dataclass
+class CameraInfo:
+ device_id: int
+ name: str
+ occupied: bool
+ backend: str
+ resolution: tuple[int, int]
+
+
+def get_input_device_resolution(index: int):
+ filter_graph = FilterGraph()
+ try:
+ filter_graph.add_video_input_device(index)
+ # This can happen with virtual cameras throwing errors.
+ # For example since OBS 29.1 updated FFMPEG breaking VirtualCam 3.0
+ # https://github.com/Toufool/AutoSplit/issues/238
+ except COMError:
+ return None
+ resolution = filter_graph.get_input_device().get_current_format()
+ filter_graph.remove_filters()
+ return resolution
+
+
+async def get_all_video_capture_devices() -> list[CameraInfo]:
+ named_video_inputs = FilterGraph().get_input_devices()
+
+ async def get_camera_info(index: int, device_name: str):
+ backend = ""
+ # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use
+ # #169
+ # FIXME: Maybe offer the option to the user to obtain more info about their devies?
+ # Off by default. With a tooltip to explain the risk.
+ # video_capture = cv2.VideoCapture(index)
+ # video_capture.setExceptionMode(True)
+ # try:
+ # # https://docs.opencv.org/3.4/d4/d15/group__videoio__flags__base.html#ga023786be1ee68a9105bf2e48c700294d
+ # backend = video_capture.getBackendName() # STS_ASSERT
+ # video_capture.grab() # STS_ERROR
+ # except cv2.error as error:
+ # return CameraInfo(index, device_name, True, backend) \
+ # if error.code in (cv2.Error.STS_ERROR, cv2.Error.STS_ASSERT) \
+ # else None
+ # finally:
+ # video_capture.release()
+
+ resolution = get_input_device_resolution(index)
+ return CameraInfo(index, device_name, False, backend, resolution) \
+ if resolution is not None \
+ else None
+
+ return [
+ camera_info
+ for camera_info
+ # Note: Return type required https://github.com/python/typeshed/issues/2652
+ in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs)))
+ if camera_info is not None
+ ]