diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml
index 2cc32c45..dfcbdc49 100644
--- a/.github/workflows/lint-and-build.yml
+++ b/.github/workflows/lint-and-build.yml
@@ -40,7 +40,7 @@ jobs:
runs-on: windows-latest
strategy:
fail-fast: false
- # Ruff is version and platform sensible
+ # Ruff is version sensible
matrix:
python-version: ["3.9", "3.10", "3.11"]
steps:
@@ -95,7 +95,7 @@ jobs:
fail-fast: false
# Only the Python version we plan on shipping matters.
matrix:
- python-version: ["3.10", "3.11"]
+ python-version: ["3.11"]
steps:
- name: Checkout ${{ github.repository }}/${{ github.ref }}
uses: actions/checkout@v3
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d82bf1b4..8405b12c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -59,8 +59,7 @@
},
"[python]": {
// Ruff is not yet a formatter: https://github.com/charliermarsh/ruff/issues/1904
- // Cannot use autotpep8 until https://github.com/microsoft/vscode-autopep8/issues/32 is fixed
- "editor.defaultFormatter": "ms-python.python",
+ "editor.defaultFormatter": "ms-python.autopep8",
"editor.tabSize": 4,
"editor.rulers": [
72, // PEP8-17 docstrings
@@ -73,7 +72,6 @@
// Important to follow the config in pyrightconfig.json
"python.analysis.useLibraryCodeForTypes": false,
"python.analysis.diagnosticMode": "workspace",
- "python.formatting.provider": "autopep8",
"python.linting.enabled": true,
"ruff.importStrategy": "fromEnvironment",
// Use the Ruff extension instead
@@ -84,6 +82,8 @@
"python.linting.pycodestyleEnabled": false,
"python.linting.pylamaEnabled": false,
"python.linting.pylintEnabled": false,
+ // Use the autopep8 extension instead
+ "python.formatting.provider": "none",
// Use Pyright/Pylance instead
"python.linting.mypyEnabled": false,
"powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline",
@@ -96,4 +96,18 @@
"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"
+ ]
}
diff --git a/README.md b/README.md
index 2b17ebc6..8a62ba64 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,7 @@ Refer to the [build instructions](build%20instructions.md) if you'd like to buil
- Perceptual Hash: An explanation on pHash comparison can be found [here](http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html). It is highly recommended to NOT use pHash if you use masked images, or it'll be very inaccurate.
#### Capture Method
+
- **Windows Graphics Capture** (fast, most compatible, capped at 60fps)
Only available in Windows 10.0.17134 and up.
@@ -94,7 +95,8 @@ Refer to the [build instructions](build%20instructions.md) if you'd like to buil
#### Capture Device
-Select the Video Capture Device that you wanna use if selecting the `Video Capture Device` Capture Method.
+Select the Video Capture Device that you wanna use if selecting the `Video Capture Device` Capture Method.
+
#### Show Live Similarity
diff --git a/pyproject.toml b/pyproject.toml
index f42167f0..a69cfa99 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -97,7 +97,6 @@ max-branches = 15
# https://github.com/hhatto/autopep8#more-advanced-usage
[tool.autopep8]
max_line_length = 120
-recursive = true
aggressive = 3
ignore = [
"E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma)
diff --git a/res/about.ui b/res/about.ui
index 5aa56a75..329dac8c 100644
--- a/res/about.ui
+++ b/res/about.ui
@@ -32,9 +32,7 @@
- :/resources/icon.ico
- :/resources/icon.ico
-
+ :/resources/icon.ico:/resources/icon.ico
@@ -115,7 +113,7 @@ Thank you!
- 181
+ 190
17
64
64
diff --git a/res/design.ui b/res/design.ui
index afb56b1d..34f92d5a 100644
--- a/res/design.ui
+++ b/res/design.ui
@@ -6,20 +6,20 @@
0
0
- 777
- 424
+ 786
+ 426
- 777
- 424
+ 786
+ 426
- 777
- 424
+ 786
+ 426
@@ -39,8 +39,8 @@
11
- 143
- 44
+ 140
+ 49
20
@@ -56,7 +56,7 @@
10
67
- 101
+ 107
23
@@ -70,7 +70,7 @@
- 650
+ 657
369
121
27
@@ -89,7 +89,7 @@
- 650
+ 657
339
121
27
@@ -108,7 +108,7 @@
- 650
+ 657
310
59
27
@@ -127,7 +127,7 @@
- 712
+ 719
310
59
27
@@ -144,7 +144,7 @@
10
- 253
+ 270
53
23
@@ -163,8 +163,8 @@
92
- 254
- 20
+ 272
+ 21
20
@@ -175,7 +175,7 @@
- 120
+ 127
67
320
240
@@ -203,7 +203,7 @@
- 449
+ 456
67
320
240
@@ -222,7 +222,7 @@
- 449
+ 456
31
318
20
@@ -239,8 +239,8 @@
11
- 183
- 44
+ 190
+ 49
20
@@ -255,8 +255,8 @@
66
- 183
- 44
+ 190
+ 49
20
@@ -271,7 +271,7 @@
65
- 254
+ 272
26
20
@@ -284,8 +284,8 @@
11
- 200
- 44
+ 210
+ 51
24
@@ -306,8 +306,8 @@
66
- 200
- 44
+ 210
+ 51
24
@@ -327,7 +327,7 @@
- 120
+ 127
31
318
20
@@ -343,7 +343,7 @@
- 477
+ 484
49
264
20
@@ -360,8 +360,8 @@
10
- 227
- 101
+ 240
+ 107
23
@@ -377,7 +377,7 @@
11
160
- 44
+ 51
24
@@ -405,7 +405,7 @@
66
160
- 44
+ 51
24
@@ -426,8 +426,8 @@
66
- 143
- 44
+ 140
+ 49
20
@@ -443,7 +443,7 @@
10
119
- 101
+ 107
23
@@ -462,7 +462,7 @@
10
93
- 101
+ 107
23
@@ -478,7 +478,7 @@
696
5
- 75
+ 81
24
@@ -494,7 +494,7 @@
10
9
- 98
+ 111
16
@@ -505,9 +505,9 @@
- 119
+ 127
6
- 574
+ 561
22
@@ -518,7 +518,7 @@
- 120
+ 127
49
318
20
@@ -534,9 +534,9 @@
- 451
+ 458
313
- 67
+ 81
20
@@ -547,7 +547,7 @@
- 120
+ 127
312
321
84
@@ -798,9 +798,9 @@
- 450
+ 457
369
- 121
+ 131
27
@@ -814,10 +814,10 @@
- 449
+ 458
344
- 101
- 16
+ 121
+ 20
@@ -827,10 +827,10 @@
- 560
+ 577
344
- 98
- 16
+ 81
+ 20
@@ -840,9 +840,9 @@
- 520
+ 537
313
- 131
+ 121
20
@@ -856,7 +856,7 @@
- 448
+ 455
49
27
18
@@ -878,7 +878,7 @@
- 743
+ 750
49
27
18
@@ -936,7 +936,7 @@
0
0
- 777
+ 786
22
diff --git a/res/settings.ui b/res/settings.ui
index ec00fb6b..8681dc44 100644
--- a/res/settings.ui
+++ b/res/settings.ui
@@ -6,20 +6,20 @@
0
0
- 291
- 661
+ 290
+ 664
- 291
- 661
+ 290
+ 664
- 291
- 661
+ 290
+ 664
@@ -32,16 +32,14 @@
- :/resources/icon.ico
- :/resources/icon.ico
-
+ :/resources/icon.ico:/resources/icon.ico
10
200
- 271
+ 270
181
@@ -51,10 +49,10 @@
- 138
- 25
+ 150
+ 24
51
- 22
+ 24
@@ -75,7 +73,7 @@
6
27
- 121
+ 141
16
@@ -87,14 +85,11 @@
-
- true
-
6
49
- 129
+ 141
20
@@ -166,8 +161,8 @@
10
390
- 271
- 261
+ 270
+ 266
@@ -185,9 +180,9 @@
- 167
+ 170
25
- 88
+ 91
22
@@ -202,8 +197,8 @@ Histograms:
An explanation on Histograms comparison can be found here
https://mpatacchiola.github.io/blog/2016/11/12/the-simplest-classifier-histogram-intersection.html
This is a great method to use if you are using several masked images.
-> This algorithm is particular reliable when the colour is a strong predictor of the object identity.
-> The histogram intersection [...] is robust to occluding objects in the foreground.
+> This algorithm is particular reliable when the colour is a strong predictor of the object identity.
+> The histogram intersection [...] is robust to occluding objects in the foreground.
Perceptual Hash:
An explanation on pHash comparison can be found here
@@ -231,7 +226,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
28
- 161
+ 171
16
@@ -244,7 +239,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
118
- 161
+ 171
16
@@ -255,10 +250,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 167
+ 170
115
- 87
- 22
+ 91
+ 24
@@ -285,7 +280,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
58
- 151
+ 171
16
@@ -296,10 +291,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 167
+ 170
55
- 52
- 22
+ 51
+ 24
@@ -319,14 +314,11 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
-
- true
-
6
143
- 235
+ 261
20
@@ -344,9 +336,9 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
- 190
+ 193
261
- 61
+ 71
@@ -367,7 +359,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
88
- 161
+ 171
16
@@ -378,10 +370,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 167
+ 170
85
- 87
- 22
+ 91
+ 24
@@ -398,13 +390,14 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
140
- 210
+ 218
71
31
+ Segoe UI
8
true
@@ -427,7 +420,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
6
168
- 151
+ 171
20
@@ -444,7 +437,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
10
10
- 271
+ 270
191
@@ -466,7 +459,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
180
130
81
- 21
+ 22
@@ -477,15 +470,12 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
-
- true
-
- 76
+ 80
30
94
- 20
+ 22
@@ -498,10 +488,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 76
+ 80
80
94
- 20
+ 22
@@ -527,10 +517,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 76
+ 80
55
94
- 20
+ 22
@@ -546,7 +536,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
180
80
81
- 21
+ 22
@@ -591,7 +581,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
180
30
81
- 21
+ 22
@@ -617,10 +607,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 76
+ 80
130
94
- 20
+ 22
@@ -649,7 +639,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
180
105
81
- 21
+ 22
@@ -675,10 +665,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
- 76
+ 80
105
94
- 20
+ 22
@@ -694,7 +684,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be
180
155
81
- 21
+ 22
@@ -721,10 +711,10 @@ reset image
- 76
+ 80
155
94
- 20
+ 22
@@ -737,11 +727,11 @@ reset image
- split_input
- reset_input
- undo_split_input
- skip_split_input
- pause_input
+ set_split_hotkey_button
+ set_reset_hotkey_button
+ set_undo_split_hotkey_button
+ set_skip_split_hotkey_button
+ set_pause_hotkey_button
fps_limit_spinbox
live_capture_region_checkbox
capture_method_combobox
diff --git a/res/update_checker.ui b/res/update_checker.ui
index 75fbc852..6ff6ea85 100644
--- a/res/update_checker.ui
+++ b/res/update_checker.ui
@@ -9,20 +9,20 @@
0
0
- 313
- 133
+ 318
+ 132
- 313
- 133
+ 318
+ 132
- 313
- 133
+ 318
+ 132
@@ -43,9 +43,9 @@
- 20
+ 10
10
- 218
+ 251
16
@@ -62,9 +62,9 @@
- 20
+ 10
30
- 91
+ 101
16
@@ -75,9 +75,9 @@
- 20
+ 10
50
- 81
+ 101
16
@@ -88,9 +88,9 @@
- 20
+ 10
80
- 119
+ 141
16
@@ -101,9 +101,9 @@
- 150
+ 160
100
- 75
+ 71
24
@@ -117,9 +117,9 @@
- 230
+ 240
100
- 75
+ 71
24
@@ -130,9 +130,9 @@
- 120
+ 110
30
- 181
+ 191
16
@@ -143,9 +143,9 @@
- 120
+ 110
50
- 181
+ 191
16
@@ -156,9 +156,9 @@
- 20
+ 10
102
- 131
+ 151
20
diff --git a/scripts/build.ps1 b/scripts/build.ps1
index 840db0c2..f9825aaf 100644
--- a/scripts/build.ps1
+++ b/scripts/build.ps1
@@ -1,10 +1,11 @@
& "$PSScriptRoot/compile_resources.ps1"
$arguments = @(
+ "$PSScriptRoot/../src/AutoSplit.py",
'--onefile',
'--windowed',
'--additional-hooks-dir=Pyinstaller/hooks',
'--icon=res/icon.ico',
'--splash=res/splash.png')
-pyinstaller $arguments "$PSScriptRoot/../src/AutoSplit.py"
+Start-Process -Wait -NoNewWindow pyinstaller -ArgumentList $arguments
diff --git a/scripts/designer.ps1 b/scripts/designer.ps1
index 13d2450a..a6a159f6 100644
--- a/scripts/designer.ps1
+++ b/scripts/designer.ps1
@@ -1,4 +1,10 @@
-$qt6_applications_path = python3 -c 'import qt6_applications; print(qt6_applications.__path__[0])'
+$qt6_applications_import = 'import qt6_applications; print(qt6_applications.__path__[0])'
+$qt6_applications_path = python -c $qt6_applications_import
+if ($null -eq $qt6_applications_path) {
+ Write-Host 'Designer not found, installing qt6_applications'
+ python -m pip install qt6_applications
+}
+$qt6_applications_path = python -c $qt6_applications_import
& "$qt6_applications_path/Qt/bin/designer" `
"$PSScriptRoot/../res/design.ui" `
"$PSScriptRoot/../res/about.ui" `
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index cac7c468..1a513d35 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -1,14 +1,7 @@
-# Alias python3 to python on Windows
-If ($IsWindows) {
- $python = (Get-Command python).Source
- $python3 = "$((Get-Item $python).Directory.FullName)/python3.exe"
- New-Item -ItemType SymbolicLink -Path $python3 -Target $python -ErrorAction SilentlyContinue
-}
-
# Installing Python dependencies
$dev = If ($Env:GITHUB_JOB -eq 'Build') { '' } Else { '-dev' }
# Ensures installation tools are up to date. This also aliases pip to pip3 on MacOS.
-python3 -m pip install wheel pip setuptools --upgrade
+python -m pip install wheel pip setuptools --upgrade
pip install -r "$PSScriptRoot/requirements$dev.txt" --upgrade
# Don't compile resources on the Build CI job as it'll do so in build script
diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt
index 8b02dc5f..3f8d5b76 100644
--- a/scripts/requirements-dev.txt
+++ b/scripts/requirements-dev.txt
@@ -15,12 +15,12 @@ ruff>=0.0.262 # Avoid N802 violations for @override + Avoid adding required impo
# Can also be downloaded externally as a non-python package
# qt6-applications
# Types
-types-d3dshot
+types-D3DShot ; sys_platform == 'win32'
types-keyboard
types-Pillow
types-psutil
types-PyAutoGUI
types-pyinstaller
-types-pywin32
+types-pywin32 ; sys_platform == 'win32'
types-requests
types-toml
diff --git a/scripts/requirements.txt b/scripts/requirements.txt
index a611e047..2d7bb755 100644
--- a/scripts/requirements.txt
+++ b/scripts/requirements.txt
@@ -21,6 +21,7 @@ packaging
Pillow>=9.2 # gnome-screeshot checks
psutil
PyAutoGUI
+PyWinCtl>=0.0.42 # py.typed
PySide6-Essentials>=6.5 # fixes https://bugreports.qt.io/browse/PYSIDE-2189 and https://bugreports.qt.io/browse/PYSIDE-1603
requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534
toml
diff --git a/scripts/start.ps1 b/scripts/start.ps1
index d1e8ec08..70d6fd8b 100644
--- a/scripts/start.ps1
+++ b/scripts/start.ps1
@@ -1,3 +1,3 @@
param ([string]$p1)
& "$PSScriptRoot/compile_resources.ps1"
-python3 "$PSScriptRoot/../src/AutoSplit.py" $p1
+python "$PSScriptRoot/../src/AutoSplit.py" $p1
diff --git a/src/AutoSplit.py b/src/AutoSplit.py
index f245078f..e7cf0663 100644
--- a/src/AutoSplit.py
+++ b/src/AutoSplit.py
@@ -56,7 +56,7 @@
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
myappid = f"Toufool.AutoSplit.v{AUTOSPLIT_VERSION}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
-# qt.qpa.window: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5: Access is denied. # noqa: E501 # pylint: disable=line-too-long
+# qt.qpa.window: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5: Access is denied. # noqa: E501
# ctypes.windll.user32.SetProcessDpiAwarenessContext(2)
@@ -432,7 +432,7 @@ def __is_current_split_out_of_range(self):
or self.split_image_number > len(self.split_images_and_loop_number) - 1
def undo_split(self, navigate_image_only: bool = False):
- """"Undo Split" and "Prev. Img." buttons connect to here."""
+ """Undo Split" and "Prev. Img." buttons connect to here."""
# Can't undo until timer is started
# or Undoing past the first image
if not self.is_running \
@@ -454,7 +454,7 @@ def undo_split(self, navigate_image_only: bool = False):
send_command(self, "undo")
def skip_split(self, navigate_image_only: bool = False):
- """"Skip Split" and "Next Img." buttons connect to here."""
+ """Skip Split" and "Next Img." buttons connect to here."""
# Can't skip or split until timer is started
# or Splitting/skipping when there are no images left
if not self.is_running \
diff --git a/src/capture_method/BitBltCaptureMethod.py b/src/capture_method/BitBltCaptureMethod.py
index eed9f739..b15c9fbd 100644
--- a/src/capture_method/BitBltCaptureMethod.py
+++ b/src/capture_method/BitBltCaptureMethod.py
@@ -22,6 +22,14 @@
class BitBltCaptureMethod(CaptureMethodBase):
+ name = "BitBlt"
+ short_description = "fastest, least compatible"
+ description = (
+ "\nThe best option when compatible. But it cannot properly record "
+ + "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. "
+ + "\nThe smaller the selected region, the more efficient it is. "
+ )
+
_render_full_content = False
def get_frame(self, autosplit: AutoSplit) -> tuple[cv2.Mat | None, bool]:
diff --git a/src/capture_method/CaptureMethodBase.py b/src/capture_method/CaptureMethodBase.py
index 759946f1..b82dc9ca 100644
--- a/src/capture_method/CaptureMethodBase.py
+++ b/src/capture_method/CaptureMethodBase.py
@@ -11,6 +11,10 @@
class CaptureMethodBase():
+ name = "None"
+ short_description = ""
+ description = ""
+
def __init__(self, autosplit: AutoSplit | None = None):
# Some capture methods don't need an initialization process
pass
diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py
index 744480d6..a00ec7e9 100644
--- a/src/capture_method/DesktopDuplicationCaptureMethod.py
+++ b/src/capture_method/DesktopDuplicationCaptureMethod.py
@@ -9,13 +9,25 @@
from win32 import win32gui
from capture_method.BitBltCaptureMethod import BitBltCaptureMethod
-from utils import get_window_bounds
+from utils import GITHUB_REPOSITORY, get_window_bounds
if TYPE_CHECKING:
from AutoSplit import AutoSplit
class DesktopDuplicationCaptureMethod(BitBltCaptureMethod):
+ name = "Direct3D Desktop Duplication"
+ short_description = "slower, bound to display"
+ description = (
+ "\nDuplicates the desktop using Direct3D. "
+ + "\nIt can record OpenGL and Hardware Accelerated windows. "
+ + "\nAbout 10-15x slower than BitBlt. Not affected by window size. "
+ + "\nOverlapping windows will show up and can't record across displays. "
+ + "\nThis option may not be available for hybrid GPU laptops, "
+ + "\nsee D3DDD-Note-Laptops.md for a solution. "
+ + f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method "
+ )
+
def __init__(self):
super().__init__()
# Must not set statically as some laptops will throw an error
diff --git a/src/capture_method/ForceFullContentRenderingCaptureMethod.py b/src/capture_method/ForceFullContentRenderingCaptureMethod.py
index 384ef027..6bbcd70e 100644
--- a/src/capture_method/ForceFullContentRenderingCaptureMethod.py
+++ b/src/capture_method/ForceFullContentRenderingCaptureMethod.py
@@ -4,4 +4,12 @@
class ForceFullContentRenderingCaptureMethod(BitBltCaptureMethod):
+ name = "Force Full Content Rendering"
+ short_description = "very slow, can affect rendering"
+ description = (
+ "\nUses BitBlt behind the scene, but passes a special flag "
+ + "\nto PrintWindow to force rendering the entire desktop. "
+ + "\nAbout 10-15x slower than BitBlt based on original window size "
+ + "\nand can mess up some applications' rendering pipelines. "
+ )
_render_full_content = True
diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py
index 427fb0e9..0addeb7b 100644
--- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py
+++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py
@@ -25,6 +25,15 @@ def is_blank(image: cv2.Mat):
class VideoCaptureDeviceCaptureMethod(CaptureMethodBase):
+ name = "Video Capture Device"
+ short_description = "see below"
+ description = (
+ "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. "
+ + "\nYou can select one below. "
+ + "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead "
+ + "\nhttps://github.com/Avasam/obs-virtual-cam/releases"
+ )
+
capture_device: cv2.VideoCapture
capture_thread: Thread | None
stop_thread: Event
@@ -114,8 +123,5 @@ def get_frame(self, autosplit: AutoSplit):
]
return cv2.cvtColor(image, cv2.COLOR_BGR2BGRA), is_old_image
- def recover_window(self, captured_window_title: str, autosplit: AutoSplit) -> bool:
- raise NotImplementedError
-
def check_selected_region_exists(self, autosplit: AutoSplit):
return bool(self.capture_device.isOpened())
diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py
index 68315f8e..e38f7957 100644
--- a/src/capture_method/WindowsGraphicsCaptureMethod.py
+++ b/src/capture_method/WindowsGraphicsCaptureMethod.py
@@ -13,15 +13,28 @@
from winsdk.windows.graphics.imaging import BitmapBufferAccessMode, SoftwareBitmap
from capture_method.CaptureMethodBase import CaptureMethodBase
-from utils import RGBA_CHANNEL_COUNT, WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd
+from utils import RGBA_CHANNEL_COUNT, WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd
if TYPE_CHECKING:
from AutoSplit import AutoSplit
WGC_NO_BORDER_MIN_BUILD = 20348
+LEARNING_MODE_DEVICE_BUILD = 17763
+"""https://learn.microsoft.com/en-us/uwp/api/windows.ai.machinelearning.learningmodeldevice"""
class WindowsGraphicsCaptureMethod(CaptureMethodBase):
+ name = "Windows Graphics Capture"
+ short_description = "fast, most compatible, capped at 60fps"
+ description = (
+ f"\nOnly available in Windows 10.0.{WGC_MIN_BUILD} and up. "
+ + f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}"
+ + "\nrequire having at least one audio or video Capture Device connected and enabled."
+ + "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. "
+ + "\nAdds a yellow border on Windows 10 (not on Windows 11)."
+ + "\nCaps at around 60 FPS. "
+ )
+
size: SizeInt32
frame_pool: Direct3D11CaptureFramePool | None = None
session: GraphicsCaptureSession | None = None
diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py
index e179cd9e..1ee2401a 100644
--- a/src/capture_method/__init__.py
+++ b/src/capture_method/__init__.py
@@ -15,16 +15,11 @@
from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod
from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod
from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod
-from utils import GITHUB_REPOSITORY, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device
+from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device
if TYPE_CHECKING:
from AutoSplit import AutoSplit
-WGC_MIN_BUILD = 17134
-"""https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker#applies-to"""
-LEARNING_MODE_DEVICE_BUILD = 17763
-"""https://learn.microsoft.com/en-us/uwp/api/windows.ai.machinelearning.learningmodeldevice"""
-
class Region(TypedDict):
x: int
@@ -33,14 +28,6 @@ class Region(TypedDict):
height: int
-@dataclass
-class CaptureMethodInfo():
- name: str
- short_description: str
- description: str
- implementation: type[CaptureMethodBase]
-
-
class CaptureMethodMeta(EnumMeta):
# Allow checking if simple string is enum
def __contains__(self, other: str):
@@ -74,7 +61,7 @@ def __hash__(self):
VIDEO_CAPTURE_DEVICE = "VIDEO_CAPTURE_DEVICE"
-class CaptureMethodDict(OrderedDict[CaptureMethodEnum, CaptureMethodInfo]):
+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:
@@ -99,99 +86,37 @@ def get_method_by_index(self, index: int):
def get(self, __key: CaptureMethodEnum):
"""
- Returns the `CaptureMethodInfo` for `CaptureMethodEnum` if `CaptureMethodEnum` is available,
+ Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available,
else defaults to the first available `CaptureMethodEnum`.
- Returns the `CaptureMethodBase` (default) implementation if there's no capture methods.
+ Returns `CaptureMethodBase` (default) directly if there's no capture methods.
"""
if __key == CaptureMethodEnum.NONE or len(self) <= 0:
- return NONE_CAPTURE_METHOD
+ return CaptureMethodBase
return super().get(__key, first(self.values()))
-NONE_CAPTURE_METHOD = CaptureMethodInfo(
- name="None",
- short_description="",
- description="",
- implementation=CaptureMethodBase,
-)
-
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] = CaptureMethodInfo(
- name="Windows Graphics Capture",
- short_description="fast, most compatible, capped at 60fps",
- description=(
- f"\nOnly available in Windows 10.0.{WGC_MIN_BUILD} and up. "
- + f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}"
- + "\nrequire having at least one audio or video Capture Device connected and enabled."
- + "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. "
- + "\nAdds a yellow border on Windows 10 (not on Windows 11)."
- + "\nCaps at around 60 FPS. "
- ),
- implementation=WindowsGraphicsCaptureMethod,
- )
-CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = CaptureMethodInfo(
- name="BitBlt",
- short_description="fastest, least compatible",
- description=(
- "\nThe best option when compatible. But it cannot properly record "
- + "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. "
- + "\nThe smaller the selected region, the more efficient it is. "
- ),
-
- implementation=BitBltCaptureMethod,
-)
+ CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod
+CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod
try:
import d3dshot
d3dshot.create(capture_output="numpy")
except (ModuleNotFoundError, COMError):
pass
else:
- CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = CaptureMethodInfo(
- name="Direct3D Desktop Duplication",
- short_description="slower, bound to display",
- description=(
- "\nDuplicates the desktop using Direct3D. "
- + "\nIt can record OpenGL and Hardware Accelerated windows. "
- + "\nAbout 10-15x slower than BitBlt. Not affected by window size. "
- + "\nOverlapping windows will show up and can't record across displays. "
- + "\nThis option may not be available for hybrid GPU laptops, "
- + "\nsee D3DDD-Note-Laptops.md for a solution. "
- + f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method "
- ),
- implementation=DesktopDuplicationCaptureMethod,
- )
-CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = CaptureMethodInfo(
- name="Force Full Content Rendering",
- short_description="very slow, can affect rendering",
- description=(
- "\nUses BitBlt behind the scene, but passes a special flag "
- + "\nto PrintWindow to force rendering the entire desktop. "
- + "\nAbout 10-15x slower than BitBlt based on original window size "
- + "\nand can mess up some applications' rendering pipelines. "
- ),
- implementation=ForceFullContentRenderingCaptureMethod,
-)
-CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = CaptureMethodInfo(
- name="Video Capture Device",
- short_description="see below",
- description=(
- "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. "
- + "\nYou can select one below. "
- + "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead "
- + "\nhttps://github.com/Avasam/obs-virtual-cam/releases"
- ),
- implementation=VideoCaptureDeviceCaptureMethod,
-)
+ 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).implementation(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)
@@ -209,6 +134,11 @@ class CameraInfo():
resolution: tuple[int, int] | None
+def get_input_devices():
+ # https://github.com/andreaschiavinato/python_grabber/pull/24
+ return cast(list[str], FilterGraph().get_input_devices())
+
+
def get_input_device_resolution(index: int):
filter_graph = FilterGraph()
filter_graph.add_video_input_device(index)
@@ -218,8 +148,7 @@ def get_input_device_resolution(index: int):
async def get_all_video_capture_devices() -> list[CameraInfo]:
- # TODO: Fix partially Unknown list upstream
- named_video_inputs: list[str] = FilterGraph().get_input_devices()
+ named_video_inputs = get_input_devices()
async def get_camera_info(index: int, device_name: str):
backend = ""
@@ -242,7 +171,8 @@ async def get_camera_info(index: int, device_name: str):
return CameraInfo(index, device_name, False, backend, get_input_device_resolution(index))
- future = asyncio.gather(
+ # https://github.com/python/typeshed/issues/2652
+ future: asyncio.Future[list[CameraInfo | None]] = asyncio.gather(
*[
get_camera_info(index, name) for index, name
in enumerate(named_video_inputs)
diff --git a/src/hotkeys.py b/src/hotkeys.py
index b7c95632..b0575467 100644
--- a/src/hotkeys.py
+++ b/src/hotkeys.py
@@ -105,7 +105,7 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent)
NOTE: This is a workaround very specific to numpads.
Windows reports different physical keys with the same scan code.
For example, "Home", "Num Home" and "Num 7" are all `71`.
- See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684.
+ See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684 .
Since we reuse the key string we set to send to LiveSplit, we can't use fake names like "num home".
We're also trying to achieve the same hotkey behaviour as LiveSplit has.
@@ -153,7 +153,7 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent):
def __get_hotkey_name(names: list[str]):
"""
Uses keyboard.get_hotkey_name but works with non-english modifiers and keypad
- See: https://github.com/boppreh/keyboard/issues/516.
+ See: https://github.com/boppreh/keyboard/issues/516 .
"""
def sorting_key(key: str):
return not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0])
diff --git a/src/region_selection.py b/src/region_selection.py
index d0d73870..310b71f1 100644
--- a/src/region_selection.py
+++ b/src/region_selection.py
@@ -10,15 +10,16 @@
import numpy as np
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtTest import QTest
+from pywinctl import getTopWindowAt
from typing_extensions import override
from win32 import win32gui
from win32con import SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN
-from winsdk._winrt import initialize_with_window # pylint: disable=no-name-in-module
+from winsdk._winrt import initialize_with_window
from winsdk.windows.foundation import AsyncStatus, IAsyncOperation
from winsdk.windows.graphics.capture import GraphicsCaptureItem, GraphicsCapturePicker
import error_messages
-from utils import MAXBYTE, get_window_bounds, getTopWindowAt, is_valid_hwnd, is_valid_image
+from utils import MAXBYTE, get_window_bounds, is_valid_hwnd, is_valid_image
user32 = ctypes.windll.user32
@@ -327,7 +328,7 @@ def mouseReleaseEvent(self, event: QtGui.QMouseEvent):
class SelectRegionWidget(BaseSelectWidget):
"""
Widget for dragging screen region
- https://github.com/harupy/snipping-tool.
+ Originated from https://github.com/harupy/snipping-tool .
"""
_right: int = 0
diff --git a/src/user_profile.py b/src/user_profile.py
index cfa3ee96..5d86ef8d 100644
--- a/src/user_profile.py
+++ b/src/user_profile.py
@@ -202,7 +202,9 @@ def load_check_for_updates_on_open(autosplit: AutoSplit):
value = QtCore \
.QSettings("AutoSplit", "Check For Updates On Open") \
.value("check_for_updates_on_open", True, type=bool)
- autosplit.action_check_for_updates_on_open.setChecked(value) # pyright: ignore[reportGeneralTypeIssues] # Type not infered by PySide6 # noqa: E501 # pylint: disable=line-too-long
+ # Type not infered by PySide6
+ # TODO: Report this issue upstream
+ autosplit.action_check_for_updates_on_open.setChecked(value) # pyright: ignore[reportGeneralTypeIssues]
def set_check_for_updates_on_open(design_window: design.Ui_MainWindow, value: bool):
diff --git a/src/utils.py b/src/utils.py
index 9989c263..4dd16158 100644
--- a/src/utils.py
+++ b/src/utils.py
@@ -141,7 +141,8 @@ def fire_and_forget(func: Callable[..., Any]):
"""
Runs synchronous function asynchronously without waiting for a response.
- Uses threads on Windows because `RuntimeError: There is no current event loop in thread 'MainThread'.`
+ Uses threads on Windows because ~~`RuntimeError: There is no current event loop in thread 'MainThread'.`~~
+ Because maybe asyncio has issues. Unsure. See alpha.5 and https://github.com/Avasam/AutoSplit/issues/36
Uses asyncio on Linux because of a `Segmentation fault (core dumped)`
"""
@@ -155,31 +156,12 @@ def wrapped(*args: Any, **kwargs: Any):
return wrapped
-def getTopWindowAt(x: int, y: int): # noqa: N802
- # Immitating PyWinCTL's function
- class Win32Window():
- def __init__(self, hwnd: int) -> None:
- self._hWnd = hwnd
-
- def getHandle(self): # noqa: N802
- return self._hWnd
-
- @property
- def title(self):
- return win32gui.GetWindowText(self._hWnd)
- hwnd = win32gui.WindowFromPoint((x, y))
-
- # Want to pull the parent window from the window handle
- # By using GetAncestor we are able to get the parent window instead of the owner window.
- while win32gui.IsChild(win32gui.GetParent(hwnd), hwnd):
- hwnd = ctypes.windll.user32.GetAncestor(hwnd, 2)
- return Win32Window(hwnd) if hwnd else None
-
-
# Environment specifics
WINDOWS_BUILD_NUMBER = int(version().split(".")[-1]) if sys.platform == "win32" else -1
FIRST_WIN_11_BUILD = 22000
"""AutoSplit Version number"""
+WGC_MIN_BUILD = 17134
+"""https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker#applies-to"""
FROZEN = hasattr(sys, "frozen")
"""Running from build made by PyInstaller"""
auto_split_directory = os.path.dirname(sys.executable if FROZEN else os.path.abspath(__file__))
diff --git a/typings/cv2/mat_wrapper/__init__.pyi b/typings/cv2/mat_wrapper/__init__.pyi
index 15d667e9..fa91b2f9 100644
--- a/typings/cv2/mat_wrapper/__init__.pyi
+++ b/typings/cv2/mat_wrapper/__init__.pyi
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import numpy as np
from typing_extensions import TypeAlias