Skip to content

Commit 84a5bb4

Browse files
committed
Ability to recover window
Fixed regressions: Fix crash on clising window mid-run Forgot to migrate split_delay to image.delay Removed dead load_pyqt_settings code Fixed dummy groups Pause time from filename not set correctly Setting hotkeys on load
1 parent c07b771 commit 84a5bb4

File tree

8 files changed

+123
-128
lines changed

8 files changed

+123
-128
lines changed

.flake8

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ per-file-ignores=
1212
__init__.pyi:Q000
1313
; PyQt methods
1414
ignore-names=closeEvent,paintEvent,keyPressEvent,mousePressEvent,mouseMoveEvent,mouseReleaseEvent
15-
; TODO: Bring down to 15, same as SonarLint
16-
max-complexity=55
15+
; McCabe max-complexity is also taken care of by Pylint and doesn't fail teh build there
16+
; So this is the hard limit
17+
max-complexity=32
1718
inline-quotes="

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ typeCheckingMode = "strict"
1818
ignore = [
1919
# Auto generated
2020
"src/gen/",
21+
# We expect stub files to be incomplete or contain useless statements as they're external
2122
"typings/",
2223
]
2324
reportMissingTypeStubs = "information"
@@ -36,7 +37,7 @@ reportUnknownMemberType = "none"
3637
# https://pylint.pycqa.org/en/latest/technical_reference/features.html
3738
[tool.pylint.REPORTS]
3839
# Just like default but any error will make drop to 9 or less
39-
evaluation = "10.0 - error - ((float(warning + refactor + convention) / statement) * 10)"
40+
evaluation = "10.0 - error - ((float(warning * 10 + refactor + convention) / statement) * 10)"
4041
[tool.pylint.MASTER]
4142
fail-under = 9.0
4243
# https://pylint.pycqa.org/en/latest/technical_reference/extensions.html
@@ -65,10 +66,10 @@ load-plugins = [
6566
# "pylint.extensions.for_any_all",
6667
]
6768
ignore-paths = [
68-
# Haven't looked into disabling specific rules per file
69-
"^typings/.*$",
7069
# Auto generated
7170
"^src/gen/.*$",
71+
# We expect stub files to be incomplete or contain useless statements as they're external
72+
"^typings/.*$",
7273
]
7374
# No need to mention the fixmes
7475
disable = ["fixme"]

scripts/lint.ps1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,39 @@ $exitCodes = 0
66
Write-Host "`nRunning Pyright..."
77
pyright --warnings
88
$exitCodes += $LastExitCode
9+
if ($LastExitCode -gt 0) {
10+
Write-Host "`Pyright failed ($LastExitCode)" -ForegroundColor Red
11+
} else {
12+
Write-Host "`Pyright passed" -ForegroundColor Green
13+
}
914

1015
Write-Host "`nRunning Pylint..."
1116
pylint --score=n --output-format=colorized $(git ls-files '**/*.py*')
1217
$exitCodes += $LastExitCode
18+
if ($LastExitCode -gt 0) {
19+
Write-Host "`Pylint failed ($LastExitCode)" -ForegroundColor Red
20+
} else {
21+
Write-Host "`Pylint passed" -ForegroundColor Green
22+
}
1323

1424
Write-Host "`nRunning Flake8..."
1525
flake8
1626
$exitCodes += $LastExitCode
27+
if ($LastExitCode -gt 0) {
28+
Write-Host "`Flake8 failed ($LastExitCode)" -ForegroundColor Red
29+
} else {
30+
Write-Host "`Flake8 passed" -ForegroundColor Green
31+
}
1732

1833
Write-Host "`nRunning Bandit..."
1934
bandit -f custom --silent --recursive src
2035
# $exitCodes += $LastExitCode # Returns 1 on low
36+
if ($LastExitCode -gt 0) {
37+
Write-Host "`Bandit warning ($LastExitCode)" -ForegroundColor Yellow
38+
} else {
39+
Write-Host "`Bandit passed" -ForegroundColor Green
40+
}
41+
2142

2243
if ($exitCodes -gt 0) {
2344
Write-Host "`nLinting failed ($exitCodes)" -ForegroundColor Red

src/AutoSplit.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow):
9898
split_image_directory = ""
9999
hwnd = 0
100100
"""Window Handle used for Capture Region"""
101+
window_text = ""
101102
selection = Rect()
102103
last_saved_settings: list[Union[str, float, int, bool]] = []
103104
save_settings_file_path = ""
@@ -116,13 +117,11 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow):
116117
check_start_image_timestamp = 0.0
117118

118119
# Define all other attributes
119-
setting_check_for_updates_on_open: QtCore.QSettings
120120
start_image_split_below_threshold: bool
121121
waiting_for_split_delay: bool
122122
split_below_threshold: bool
123123
run_start_time: float
124124
similarity: float
125-
split_delay: float
126125
start_image: Optional[AutoSplitImage] = None
127126
reset_image: Optional[AutoSplitImage] = None
128127
split_images: list[AutoSplitImage] = []
@@ -138,7 +137,7 @@ def __init__(self, parent: Optional[QWidget] = None):
138137

139138
self.setupUi(self)
140139

141-
settings.load_pyqt_settings(self)
140+
settings.load_check_for_updates_on_open(self)
142141

143142
# close all processes when closing window
144143
self.action_view_help.triggered.connect(view_help)
@@ -255,18 +254,17 @@ def check_live_image(self):
255254

256255
def __live_image_function(self):
257256
try:
258-
window_text = win32gui.GetWindowText(self.hwnd)
259-
self.capture_region_window_label.setText(window_text)
260-
if not window_text:
257+
self.capture_region_window_label.setText(self.window_text)
258+
if not self.window_text:
261259
self.timer_live_image.stop()
262260
self.live_image.clear()
263261
if self.live_image_function_on_open:
264262
self.live_image_function_on_open = False
265263
return
266-
267264
# Set live image in UI
268-
capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked())
269-
set_ui_image(self.live_image, capture, False)
265+
if self.hwnd:
266+
capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked())
267+
set_ui_image(self.live_image, capture, False)
270268

271269
except AttributeError:
272270
pass
@@ -376,7 +374,7 @@ def __start_image_function(self):
376374
self.start_image_label.setText(f"{START_IMAGE_TEXT}: started")
377375
send_command(self, "start")
378376
# Email sent to [email protected]
379-
QtTest.QTest.qWait(1 / self.fps_limit_spinbox.value()) # type: ignore
377+
QtTest.QTest.qWait(int(1 / self.fps_limit_spinbox.value())) # type: ignore
380378
self.start_auto_splitter()
381379

382380
# update x, y, width, height when spinbox values are changed
@@ -554,10 +552,10 @@ def __auto_splitter(self):
554552
current_group: list[int] = []
555553
self.split_groups.append(current_group)
556554

557-
for i, image in enumerate(self.split_images):
555+
for i, image in enumerate(self.split_images_and_loop_number):
558556
current_group.append(i)
559557

560-
if not image.check_flag(DUMMY_FLAG) and i < len(self.split_images) - 1:
558+
if not image[0].check_flag(DUMMY_FLAG) and i < len(self.split_images_and_loop_number) - 1:
561559
current_group = []
562560
self.split_groups.append(current_group)
563561
self.gui_changes_on_start()
@@ -654,16 +652,16 @@ def __auto_splitter(self):
654652
if not self.split_image.check_flag(DUMMY_FLAG):
655653
# If it's a delayed split, check if the delay has passed
656654
# Otherwise calculate the split time for the key press
657-
if self.split_delay > 0 and not self.waiting_for_split_delay:
658-
split_time = int(round(time() * 1000) + self.split_delay)
655+
split_delay = self.split_image.delay / 1000
656+
if split_delay > 0 and not self.waiting_for_split_delay:
657+
split_time = round(time() + split_delay * 1000)
659658
self.waiting_for_split_delay = True
660659
self.undo_split_button.setEnabled(False)
661660
self.skip_split_button.setEnabled(False)
662661
self.current_split_image_file_label.setText(" ")
663662

664663
# check for reset while delayed and display a counter of the remaining split delay time
665664
delay_start_time = time()
666-
split_delay = self.split_delay / 1000
667665
while time() - delay_start_time < split_delay:
668666
delay_time_left = round(split_delay - (time() - delay_start_time), 1)
669667
self.current_split_image.setText(f"Delayed Split: {delay_time_left} sec remaining")
@@ -711,8 +709,6 @@ def __auto_splitter(self):
711709
# Also updates the current split image text, counting down the time until the next split image
712710
pause_time = self.split_image.get_pause_time(self)
713711
if pause_time > 0:
714-
self.current_split_image_file_label.setText(" ")
715-
self.image_loop_label.setText("Image Loop: -")
716712
pause_start_time = time()
717713
while time() - pause_start_time < pause_time:
718714
pause_time_left = round(pause_time - (time() - pause_start_time), 1)
@@ -788,6 +784,17 @@ def __get_capture_for_comparison(self):
788784
Grab capture region and resize for comparison
789785
"""
790786
capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked())
787+
788+
# This most likely means we lost capture (ie the captured window was closed, crashed, etc.)
789+
if capture is None:
790+
# Try to recover by using the window name
791+
self.live_image.setText("Trying to recover window...")
792+
# https://github.com/kaluluosi/pywin32-stubs/issues/7
793+
hwnd = win32gui.FindWindow(None, self.window_text) # type: ignore
794+
# Don't fallback to desktop
795+
if hwnd:
796+
self.hwnd = hwnd
797+
capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked())
791798
return None if capture is None else cv2.resize(capture, COMPARISON_RESIZE, interpolation=cv2.INTER_NEAREST)
792799

793800
def __reset_if_should(self, capture: Optional[cv2.ndarray]):
@@ -872,7 +879,7 @@ def exit_program():
872879

873880
if warning is QMessageBox.StandardButton.Yes:
874881
# TODO: Don't close if user cancelled the save
875-
self.save_settings_as()
882+
settings.save_settings_as(self)
876883
exit_program()
877884
if warning is QMessageBox.StandardButton.No:
878885
exit_program()
@@ -902,7 +909,6 @@ def main():
902909
if FROZEN:
903910
error_messages.exception_traceback(message, exception)
904911
else:
905-
print(message)
906912
traceback.print_exception(type(exception), exception, exception.__traceback__)
907913
sys.exit(1)
908914

src/AutoSplitImage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class AutoSplitImage():
3636
mask: Optional[cv2.ndarray] = None
3737
# This value is internal, check for mask instead
3838
_has_transparency: bool
39-
# These values should be overriden by defaults if null, use getters instead
39+
# These values should be overriden by Defaults if None. Use getters instead
4040
__pause_time: Optional[float] = None
4141
__similarity_threshold: Optional[float] = None
4242

@@ -64,7 +64,7 @@ def __init__(self, path: str):
6464
self.flags = flags_from_filename(self.filename)
6565
self.loops = loop_from_filename(self.filename)
6666
self.delay = delay_from_filename(self.filename)
67-
self._pause_time = pause_from_filename(self.filename)
67+
self.__pause_time = pause_from_filename(self.filename)
6868
self.__similarity_threshold = threshold_from_filename(self.filename)
6969
self.__read_image_bytes(path)
7070

src/capture_windows.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import ctypes
55
import ctypes.wintypes
6-
import platform
76
from dataclasses import dataclass
87
from PyQt6 import QtCore, QtGui
98
from PyQt6.QtWidgets import QLabel
@@ -13,26 +12,25 @@
1312
import win32con
1413
import win32ui
1514
import pywintypes
16-
from packaging import version
1715
from win32 import win32gui
1816
from win32typing import PyCBitmap, PyCDC
1917

2018
# This is an undocumented nFlag value for PrintWindow
2119
PW_RENDERFULLCONTENT = 0x00000002
22-
accelerated_windows: dict[int, bool] = {}
23-
is_windows_11 = version.parse(platform.version()) >= version.parse("10.0.22000")
2420

2521

26-
# ctypes.wintypes.RECT has c_long which doesn't have math operators implemented
2722
@dataclass
2823
class Rect(ctypes.wintypes.RECT):
24+
"""
25+
Overrides `ctypes.wintypes.RECT` to replace c_long with int for math operators
26+
"""
2927
left: int = -1 # type: ignore
3028
top: int = -1 # type: ignore
3129
right: int = -1 # type: ignore
3230
bottom: int = -1 # type: ignore
3331

3432

35-
def capture_region(hwnd: int, selection: Rect, force_print_window: bool):
33+
def capture_region(hwnd: int, selection: Rect, print_window: bool):
3634
"""
3735
Captures an image of the region for a window matching the given
3836
parameters of the bounding box
@@ -42,26 +40,6 @@ def capture_region(hwnd: int, selection: Rect, force_print_window: bool):
4240
@return: The image of the region in the window in BGRA format
4341
"""
4442

45-
# Windows 11 has some jank, and we're not ready to fully investigate it
46-
# for now let's ensure it works at the cost of performance
47-
is_accelerated_window = force_print_window or is_windows_11 or accelerated_windows.get(hwnd)
48-
49-
# The window type is not yet known, let's find out!
50-
if is_accelerated_window is None:
51-
# We need to get the image at least once to check if it's full black
52-
image = __get_capture_image(hwnd, selection, False)
53-
# TODO check for first non-black pixel, no need to iterate through the whole image
54-
is_accelerated_window = not np.count_nonzero(image)
55-
accelerated_windows[hwnd] = is_accelerated_window
56-
if is_accelerated_window:
57-
image = __get_capture_image(hwnd, selection, True)
58-
else:
59-
image = __get_capture_image(hwnd, selection, is_accelerated_window)
60-
61-
return None if image.size == 0 else image
62-
63-
64-
def __get_capture_image(hwnd: int, selection: Rect, print_window: bool = False):
6543
width: int = selection.right - selection.left
6644
height: int = selection.bottom - selection.top
6745
# If the window closes while it's being manipulated, it could cause a crash
@@ -82,7 +60,7 @@ def __get_capture_image(hwnd: int, selection: Rect, print_window: bool = False):
8260
# https://github.com/kaluluosi/pywin32-stubs/issues/5
8361
# pylint: disable=no-member
8462
except (win32ui.error, pywintypes.error): # type: ignore
85-
return np.array([0, 0, 0, 1], dtype="uint8")
63+
return None
8664

8765
image = np.frombuffer(cast(bytes, bitmap.GetBitmapBits(True)), dtype="uint8")
8866
image.shape = (height, width, 4)
@@ -101,7 +79,9 @@ def __get_capture_image(hwnd: int, selection: Rect, print_window: bool = False):
10179

10280
def set_ui_image(qlabel: QLabel, image: Optional[cv2.ndarray], transparency: bool):
10381
if image is None:
104-
qlabel.clear()
82+
# Clear current pixmap if image is None. But don't clear text
83+
if not qlabel.text():
84+
qlabel.clear()
10585
else:
10686
if transparency:
10787
color_code = cv2.COLOR_BGRA2RGBA

0 commit comments

Comments
 (0)