diff --git a/.flake8 b/.flake8 index 65910611..2715e740 100644 --- a/.flake8 +++ b/.flake8 @@ -12,7 +12,7 @@ per-file-ignores= __init__.pyi:Q000 ; PyQt methods ignore-names=closeEvent,paintEvent,keyPressEvent,mousePressEvent,mouseMoveEvent,mouseReleaseEvent -; McCabe max-complexity is also taken care of by Pylint and doesn't fail teh build there +; McCabe max-complexity is also taken care of by Pylint and doesn't fail the build there ; So this is the hard limit max-complexity=32 inline-quotes=" diff --git a/README.md b/README.md index 2e43e50d..4c9e24c4 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,6 @@ This program can be used to automatically start, split, and reset your preferred - {d} dummy split image. When matched, it moves to the next image without hitting your split hotkey. - {b} split when similarity goes below the threshold rather than above. When a split image filename has this flag, the split image similarity will go above the threshold, do nothing, and then split the next time the similarity goes below the threshold. - {p} pause flag. When a split image filename has this flag, it will hit your pause hotkey rather than your split hokey. - - A pause flag and a dummy flag `{pd}` cannot be used together - Filename examples: - `001_SplitName_(0.9)_[10].png` is a split image with a threshold of 0.9 and a pause time of 10 seconds. - `002_SplitName_(0.9)_[10]_{d}.png` is the second split image with a threshold of 0.9, pause time of 10, and is a dummy split. @@ -156,9 +155,9 @@ Given these splits: 1 dummy, 2 normal, 3 dummy, 4 dummy, 5 normal, 6 normal. In this situation you would have only 3 splits in LiveSplit/wsplit (even though there are 6 split images, only 3 are "real" splits). This basically results in 3 groups of splits: 1st split is images 1 and 2. 2nd split is images 3, 4 and 5. 3rd split is image 6. - If you are in the 1st or 2nd image and press the skip key, it will end up on the 3rd image -- If you are in the 3rd, 4th or 5th image and press the undo key, it will end up on the 1st image +- If you are in the 3rd, 4th or 5th image and press the undo key, it will end up on the 2nd image - If you are in the 3rd, 4th or 5th image and press the skip key, it will end up on the 6th image -- If you are in the 6th image and press the undo key, it will end up on the 3rd image +- If you are in the 6th image and press the undo key, it will end up on the 5th image ### Loop Split Images diff --git a/pyproject.toml b/pyproject.toml index 8001ad5f..3f71a85f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,11 @@ aggressive = 3 [tool.pyright] pythonPlatform = "Windows" typeCheckingMode = "strict" +# Extra strict +reportPropertyTypeMismatch=true +reportUninitializedInstanceVariable=true +reportCallInDefaultInitializer=true +reportImplicitStringConcatenation=true ignore = [ # Auto generated "src/gen/", diff --git a/res/design.ui b/res/design.ui index 80334881..ea0037ef 100644 --- a/res/design.ui +++ b/res/design.ui @@ -1,7 +1,7 @@ - main_window - + MainWindow + 0 @@ -37,7 +37,7 @@ AutoSplit - + :/resources/icon.ico:/resources/icon.ico @@ -93,6 +93,9 @@ + + false + 650 @@ -108,7 +111,10 @@ Reset - + + + false + 650 @@ -124,7 +130,10 @@ Undo - + + + false + 712 @@ -196,6 +205,9 @@ + + Qt::AlignCenter + @@ -212,6 +224,9 @@ + + Qt::AlignCenter + @@ -265,7 +280,7 @@ - 9999 + @@ -332,7 +347,7 @@ - Image Filename + - Qt::AlignCenter @@ -507,7 +522,7 @@ - Window Name + - Qt::AlignCenter @@ -516,8 +531,8 @@ - 450 - 309 + 451 + 313 67 20 @@ -796,8 +811,8 @@ - 450 - 345 + 449 + 344 98 16 @@ -809,8 +824,8 @@ - 551 - 345 + 550 + 344 98 16 @@ -822,17 +837,20 @@ - 519 - 309 + 520 + 313 131 20 - x/x + N/A + + false + 449 @@ -852,6 +870,9 @@ + + false + 744 @@ -874,8 +895,8 @@ select_region_button start_auto_splitter_button reset_button - undo_button - skip_button + undo_split_button + skip_split_button check_fps_button fps_label current_image_label @@ -1027,7 +1048,7 @@ height_spinbox - + diff --git a/res/settings.ui b/res/settings.ui index 22aa4a35..ee4dfd38 100644 --- a/res/settings.ui +++ b/res/settings.ui @@ -1,13 +1,13 @@ - dialog_settings - + DialogSettings + 0 0 289 - 570 + 540 @@ -19,13 +19,13 @@ 289 - 570 + 540 289 - 570 + 540 @@ -40,19 +40,6 @@ false - - - - 127 - 538 - 156 - 24 - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - @@ -93,7 +80,7 @@ false - + 138 @@ -102,23 +89,17 @@ 22 - - - - - 0 + + ArrowCursor - 30.000000000000000 + 20 - 5000.000000000000000 - - - 1.000000000000000 + 240 - 60.000000000000000 + 60 @@ -244,7 +225,7 @@ Default Pause Time (sec): - + 167 @@ -288,7 +269,7 @@ Default Similarity Threshold: - + 167 @@ -332,7 +313,7 @@ false - + 6 @@ -351,10 +332,10 @@ teset - <html><head/><body><p>Custom image settings and flags are set in the <br></br> image file name. These will override the default <br></br> values. View the <a href="https://github.com/Toufool/Auto-Split#readme"><span style=" text-decoration: underline; color:#0000ff;">README</span></a> for full details on all <br></br> available custom image settings</p></body></html> + <html><head/><body><p>Custom image settings and flags are set in the <br></br> image file name. These will override the default <br></br> values. View the <a href="https://github.com/Toufool/Auto-Split#readme"><span style=" text-decoration: underline; color:#0000ff;">README</span></a> for full details on all <br></br> available custom image settings.</p></body></html> - + 6 @@ -373,7 +354,7 @@ Default Delay Time (ms): - + 167 @@ -382,26 +363,14 @@ 22 + + ArrowCursor + After an image is matched, this is the amount of time in millseconds that will be delayed before splitting. - - - - - 0 - - - 0.000000000000000 - - 36000000.000000000000000 - - - 1.000000000000000 - - - 0.000000000000000 + 999999999 @@ -430,7 +399,7 @@ 180 - 133 + 130 81 21 @@ -443,10 +412,13 @@ + + true + 76 - 28 + 30 94 20 @@ -455,7 +427,7 @@ - false + true @@ -481,7 +453,7 @@ 6 - 31 + 32 71 16 @@ -494,7 +466,7 @@ 76 - 53 + 55 94 20 @@ -505,9 +477,6 @@ - - true - true @@ -532,7 +501,7 @@ 6 - 55 + 57 41 16 @@ -545,7 +514,7 @@ 180 - 53 + 55 81 21 @@ -590,7 +559,7 @@ 76 - 133 + 130 94 20 @@ -622,7 +591,7 @@ 180 - 106 + 105 81 21 @@ -638,7 +607,7 @@ 6 - 108 + 107 61 16 @@ -651,7 +620,7 @@ 76 - 106 + 105 94 20 @@ -672,46 +641,13 @@ skip_split_input pause_input default_comparison_method - default_similarity_threshold_double_spinbox - default_pause_time_double_spinbox + default_similarity_threshold_spinbox + default_pause_time_spinbox loop_splits_checkbox fps_limit_spinbox live_capture_region_checkbox force_print_window_checkbox - - - save_cancel_dialog_button_box - accepted() - dialog_settings - accept() - - - 194 - 541 - - - 140 - 282 - - - - - save_cancel_dialog_button_box - rejected() - dialog_settings - reject() - - - 194 - 541 - - - 140 - 282 - - - - + diff --git a/scripts/compile_resources.bat b/scripts/compile_resources.bat index 3c1398ff..d5ef6cf0 100644 --- a/scripts/compile_resources.bat +++ b/scripts/compile_resources.bat @@ -2,5 +2,6 @@ cd "%~dp0.." md .\src\gen pyuic6 ".\res\about.ui" -o ".\src\gen\about.py" pyuic6 ".\res\design.ui" -o ".\src\gen\design.py" +pyuic6 ".\res\settings.ui" -o ".\src\gen\settings.py" pyuic6 ".\res\update_checker.ui" -o ".\src\gen\update_checker.py" pyside6-rcc ".\res\resources.qrc" -o ".\src\gen\resources_rc.py" diff --git a/scripts/designer.bat b/scripts/designer.bat index 3b7af65e..d3b61064 100644 --- a/scripts/designer.bat +++ b/scripts/designer.bat @@ -11,4 +11,9 @@ IF NOT DEFINED PYTHONPATH ( SET PYTHONPATH=!pythonFiles[0]! ) -START "Qt Designer" "%PYTHONPATH:~0,-11%\Lib\site-packages\qt6_applications\Qt\bin\designer.exe" "%~d0%~p0..\res\design.ui" "%~d0%~p0..\res\about.ui" "%~d0%~p0..\res\update_checker.ui" +START "Qt Designer" "%PYTHONPATH:~0,-11%\Lib\site-packages\qt6_applications\Qt\bin\designer.exe"^ + "%~d0%~p0..\res\design.ui"^ + "%~d0%~p0..\res\about.ui"^ + "%~d0%~p0..\res\settings.ui"^ + "%~d0%~p0..\res\update_checker.ui" + diff --git a/src/AutoControlledWorker.py b/src/AutoControlledWorker.py index 0c252d44..36b231d9 100644 --- a/src/AutoControlledWorker.py +++ b/src/AutoControlledWorker.py @@ -20,6 +20,8 @@ def run(self): except RuntimeError: self.autosplit.show_error_signal.emit(error_messages.stdin_lost) break + except EOFError: + continue # TODO: "AutoSplit Integration" needs to call this and wait instead of outright killing the app. # For now this can only used in a Development environment if line == "kill": diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 97031748..91d8b88f 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -28,18 +28,18 @@ import error_messages import settings_file as settings from AutoControlledWorker import AutoControlledWorker -from capture_windows import capture_region, Rect, set_ui_image -from gen import about, design, update_checker -from hotkeys import send_command, after_setting_hotkey, set_split_hotkey, set_reset_hotkey, set_skip_split_hotkey, \ - set_undo_split_hotkey, set_pause_hotkey -from menu_bar import open_about, VERSION, view_help, check_for_updates, open_update_checker +from capture_windows import capture_region, set_ui_image +from gen import about, design, settings as settings_ui, update_checker +from hotkeys import send_command, after_setting_hotkey +from menu_bar import get_default_settings_from_ui, open_about, VERSION, open_settings, view_help, check_for_updates, \ + open_update_checker from screen_region import select_region, select_window, align_region, validate_before_parsing from settings_file import FROZEN from split_parser import BELOW_FLAG, DUMMY_FLAG, PAUSE_FLAG, parse_and_validate_images -CREATE_NEW_ISSUE_MESSAGE = "Please create a New Issue at " \ - "github.com/Toufool/Auto-Split/issues, describe what happened, and copy & paste the error message below" -START_IMAGE_TEXT = "Start Image" +CREATE_NEW_ISSUE_MESSAGE = ( + "Please create a New Issue at " + + "github.com/Toufool/Auto-Split/issues, describe what happened, and copy & paste the error message below") START_AUTO_SPLITTER_TEXT = "Start Auto Splitter" CHECK_FPS_ITERATIONS = 10 @@ -47,14 +47,14 @@ os.environ["REQUESTS_CA_BUNDLE"] = certifi.where() -def make_excepthook(main_window: AutoSplit): +def make_excepthook(autosplit: AutoSplit): def excepthook(exception_type: type[BaseException], exception: BaseException, _traceback: Optional[TracebackType]): # Catch Keyboard Interrupts for a clean close if exception_type is KeyboardInterrupt or isinstance(exception, KeyboardInterrupt): sys.exit(0) - main_window.show_error_signal.emit(lambda: error_messages.exception_traceback( + autosplit.show_error_signal.emit(lambda: error_messages.exception_traceback( "AutoSplit encountered an unhandled exception and will try to recover, " - f"however, there is no guarantee everything will work properly. {CREATE_NEW_ISSUE_MESSAGE}", + + f"however, there is no guarantee it will keep working properly. {CREATE_NEW_ISSUE_MESSAGE}", exception)) return excepthook @@ -82,9 +82,10 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): timer_start_image = QtCore.QTimer() # Widgets - AboutWidget: about.Ui_AboutAutoSplitWidget - UpdateCheckerWidget: update_checker.Ui_UpdateChecker - CheckForUpdatesThread: QtCore.QThread + AboutWidget: Optional[about.Ui_AboutAutoSplitWidget] = None + UpdateCheckerWidget: Optional[update_checker.Ui_UpdateChecker] = None + CheckForUpdatesThread: Optional[QtCore.QThread] = None + SettingsWidget: Optional[settings_ui.Ui_DialogSettings] = None # hotkeys need to be initialized to be passed as thread arguments in hotkeys.py # and for type safety in both hotkeys.py and settings_file.py @@ -95,13 +96,10 @@ class AutoSplit(QMainWindow, design.Ui_MainWindow): pause_hotkey: Optional[Callable[[], None]] = None # Initialize a few attributes - split_image_directory = "" hwnd = 0 """Window Handle used for Capture Region""" - window_text = "" - selection = Rect() last_saved_settings: list[Union[str, float, int, bool]] = [] - live_image_function_on_open = True + similarity = 0.0 split_image_number = 0 split_images_and_loop_number: list[tuple[AutoSplitImage, int]] = [] split_groups: list[list[int]] = [] @@ -130,34 +128,32 @@ def __init__(self, parent: Optional[QWidget] = None): # Setup global error handling self.show_error_signal.connect(lambda errorMessageBox: errorMessageBox()) - # Whithin LiveSplit excepthook needs to use main_window's signals to show errors + # Whithin LiveSplit excepthook needs to use MainWindow's signals to show errors sys.excepthook = make_excepthook(self) self.setupUi(self) + # Get default values defined in SettingsDialog + self.settings_dict = get_default_settings_from_ui(self) settings.load_check_for_updates_on_open(self) - # close all processes when closing window self.action_view_help.triggered.connect(view_help) self.action_about.triggered.connect(lambda: open_about(self)) self.action_check_for_updates.triggered.connect(lambda: check_for_updates(self)) - self.action_save_settings.triggered.connect(lambda: settings.save_settings(self)) - self.action_save_settings_as.triggered.connect(lambda: settings.save_settings_as(self)) - self.action_load_settings.triggered.connect(lambda: settings.load_settings(self)) + self.action_settings.triggered.connect(lambda: open_settings(self)) + self.action_save_profile.triggered.connect(lambda: settings.save_settings(self)) + self.action_save_profile_as.triggered.connect(lambda: settings.save_settings_as(self)) + self.action_load_profile.triggered.connect(lambda: settings.load_settings(self)) + + if self.SettingsWidget: + self.SettingsWidget.split_input.setEnabled(False) + self.SettingsWidget.reset_input.setEnabled(False) + self.SettingsWidget.skip_split_input.setEnabled(False) + self.SettingsWidget.undo_split_input.setEnabled(False) + self.SettingsWidget.pause_input.setEnabled(False) if self.is_auto_controlled: - self.set_split_hotkey_button.setEnabled(False) - self.set_reset_hotkey_button.setEnabled(False) - self.set_skip_split_hotkey_button.setEnabled(False) - self.set_undo_split_hotkey_button.setEnabled(False) - self.set_pause_hotkey_button.setEnabled(False) self.start_auto_splitter_button.setEnabled(False) - self.split_input.setEnabled(False) - self.reset_input.setEnabled(False) - self.skip_split_input.setEnabled(False) - self.undo_split_input.setEnabled(False) - self.pause_input.setEnabled(False) - self.timer_global_hotkeys_label.setText("Hotkeys Inactive - Use LiveSplit Hotkeys") # Send version and process ID to stdout print(f"{VERSION}\n{os.getpid()}", flush=True) @@ -183,14 +179,9 @@ def __init__(self, parent: Optional[QWidget] = None): self.undo_split_button.clicked.connect(self.__undo_split) self.next_image_button.clicked.connect(lambda: self.__skip_split(True)) self.previous_image_button.clicked.connect(lambda: self.__undo_split(True)) - self.set_split_hotkey_button.clicked.connect(lambda: set_split_hotkey(self)) - self.set_reset_hotkey_button.clicked.connect(lambda: set_reset_hotkey(self)) - self.set_skip_split_hotkey_button.clicked.connect(lambda: set_skip_split_hotkey(self)) - self.set_undo_split_hotkey_button.clicked.connect(lambda: set_undo_split_hotkey(self)) - self.set_pause_hotkey_button.clicked.connect(lambda: set_pause_hotkey(self)) self.align_region_button.clicked.connect(lambda: align_region(self)) self.select_window_button.clicked.connect(lambda: select_window(self)) - self.start_image_reload_button.clicked.connect(lambda: self.load_start_image(True, True)) + self.reload_start_image_button.clicked.connect(lambda: self.load_start_image(True, True)) self.action_check_for_updates_on_open.changed.connect(lambda: settings.set_check_for_updates_on_open( self, self.action_check_for_updates_on_open.isChecked()) @@ -213,7 +204,7 @@ def __init__(self, parent: Optional[QWidget] = None): self.pause_signal.connect(self.pause) # live image checkbox - self.live_image_checkbox.clicked.connect(self.check_live_image) + self.timer_live_image.start(int(1000 / 60)) self.timer_live_image.timeout.connect(self.__live_image_function) # Automatic timer start @@ -236,49 +227,37 @@ def __browse(self): new_split_image_directory = QFileDialog.getExistingDirectory( self, "Select Split Image Directory", - os.path.join(self.split_image_directory or settings.auto_split_directory, "..")) + os.path.join(self.settings_dict["split_image_directory"] or settings.auto_split_directory, "..")) # If the user doesn't select a folder, it defaults to "". if new_split_image_directory: # set the split image folder line to the directory text - self.split_image_directory = new_split_image_directory + self.settings_dict["split_image_directory"] = new_split_image_directory self.split_image_folder_input.setText(f"{new_split_image_directory}/") self.load_start_image() - def check_live_image(self): - if self.live_image_checkbox.isChecked(): - self.timer_live_image.start(int(1000 / 60)) - else: - self.timer_live_image.stop() - self.__live_image_function() - def __live_image_function(self): - try: - self.capture_region_window_label.setText(self.window_text) - if not self.window_text: - self.timer_live_image.stop() - self.live_image.clear() - if self.live_image_function_on_open: - self.live_image_function_on_open = False - return - # Set live image in UI - if self.hwnd: - capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked()) - set_ui_image(self.live_image, capture, False) - - except AttributeError: - pass + self.capture_region_window_label.setText(self.settings_dict["captured_window_title"]) + if not (self.settings_dict["live_capture_region"] and self.settings_dict["captured_window_title"]): + self.live_image.clear() + return + # Set live image in UI + if self.hwnd: + capture = capture_region(self.hwnd, + self.settings_dict["capture_region"], + self.settings_dict["force_print_window"]) + set_ui_image(self.live_image, capture, False) def load_start_image(self, started_by_button: bool = False, wait_for_delay: bool = True): self.timer_start_image.stop() - self.current_split_image_file_label.setText(" ") - self.start_image_label.setText(f"{START_IMAGE_TEXT}: not found") + self.current_image_file_label.setText("-") + self.start_image_status_value_label.setText("not found") QApplication.processEvents() if not self.is_auto_controlled \ - and (not self.split_input.text() - or not self.reset_input.text() - or not self.pause_input.text()): + and (not self.settings_dict["split_hotkey"] + or not self.settings_dict["reset_hotkey"] + or not self.settings_dict["pause_hotkey"]): error_messages.load_start_image() return @@ -295,17 +274,17 @@ def load_start_image(self, started_by_button: bool = False, wait_for_delay: bool start_pause_time = self.start_image.get_pause_time(self) if not wait_for_delay and start_pause_time > 0: self.check_start_image_timestamp = time() + start_pause_time - self.start_image_label.setText(f"{START_IMAGE_TEXT}: paused") - self.highest_similarity_label.setText(" ") - self.current_similarity_threshold_number_label.setText(" ") + self.start_image_status_value_label.setText("paused") + self.table_current_image_highest_label.setText("-") + self.table_current_image_threshold_label.setText("-") else: self.check_start_image_timestamp = 0.0 - self.start_image_label.setText(f"{START_IMAGE_TEXT}: ready") + self.start_image_status_value_label.setText("ready") self.__update_split_image(self.start_image) self.highest_similarity = 0.0 self.start_image_split_below_threshold = False - self.timer_start_image.start(int(1000 / self.fps_limit_spinbox.value())) + self.timer_start_image.start(int(1000 / self.settings_dict["fps_limit"])) QApplication.processEvents() @@ -313,35 +292,31 @@ def __start_image_function(self): if self.start_image is None \ or not self.start_image \ or time() < self.check_start_image_timestamp \ - or (not self.split_input.text() and not self.is_auto_controlled): - pause_time_left = f"{self.check_start_image_timestamp - time():.1f}" + or (not self.settings_dict["split_hotkey"] and not self.is_auto_controlled): + pause_time_left = self.check_start_image_timestamp - time() self.current_split_image.setText( - f"None\n (Paused before loading {START_IMAGE_TEXT}).\n {pause_time_left} sec remaining") + f"None\n (Paused before loading Start Image).\n {seconds_remaining_text(pause_time_left)}") return if self.check_start_image_timestamp > 0: self.check_start_image_timestamp = 0.0 - self.start_image_label.setText(f"{START_IMAGE_TEXT}: ready") + self.start_image_status_value_label.setText("ready") self.__update_split_image(self.start_image) capture = self.__get_capture_for_comparison() start_image_threshold = self.start_image.get_similarity_threshold(self) start_image_similarity = self.start_image.compare_with_capture(self, capture) - self.current_similarity_threshold_number_label.setText(f"{start_image_threshold:.2f}") + self.table_current_image_threshold_label.setText(f"{start_image_threshold:.2f}") # Show live similarity if the checkbox is checked - self.live_similarity_label.setText(str(start_image_similarity)[:4] - if self.show_live_similarity_checkbox.isChecked() - else " ") + self.table_current_image_live_label.setText(str(start_image_similarity)[:4]) # If the similarity becomes higher than highest similarity, set it as such. if start_image_similarity > self.highest_similarity: self.highest_similarity = start_image_similarity # Show live highest similarity if the checkbox is checked - self.highest_similarity_label.setText(str(self.highest_similarity)[:4] - if self.show_highest_similarity_checkbox.isChecked() - else " ") + self.table_current_image_highest_label.setText(str(self.highest_similarity)[:4]) # If the {b} flag is set, let similarity go above threshold first, then split on similarity below threshold # Otherwise just split when similarity goes above threshold @@ -361,46 +336,34 @@ def __start_image_function(self): # delay start image if needed if self.start_image.delay > 0: - self.start_image_label.setText(f"{START_IMAGE_TEXT}: delaying start...") + self.start_image_status_value_label.setText("delaying start...") delay_start_time = time() start_delay = self.start_image.delay / 1000 while time() - delay_start_time < start_delay: - delay_time_left = round(start_delay - (time() - delay_start_time), 1) + delay_time_left = start_delay - (time() - delay_start_time) self.current_split_image.setText( - f"Delayed Before Starting:\n {delay_time_left} sec remaining") + f"Delayed Before Starting:\n {seconds_remaining_text(delay_time_left)}") # Email sent to pyqt@riverbankcomputing.com QtTest.QTest.qWait(1) # type: ignore - self.start_image_label.setText(f"{START_IMAGE_TEXT}: started") + self.start_image_status_value_label.setText("started") send_command(self, "start") # Email sent to pyqt@riverbankcomputing.com - QtTest.QTest.qWait(int(1 / self.fps_limit_spinbox.value())) # type: ignore + QtTest.QTest.qWait(int(1 / self.settings_dict["fps_limit"])) # type: ignore self.start_auto_splitter() # update x, y, width, height when spinbox values are changed def __update_x(self): - try: - self.selection.left = self.x_spinbox.value() - self.selection.right = self.selection.left + self.width_spinbox.value() - self.check_live_image() - except AttributeError: - pass + self.settings_dict["capture_region"].x = self.x_spinbox.value() def __update_y(self): - try: - self.selection.top = self.y_spinbox.value() - self.selection.bottom = self.selection.top + self.height_spinbox.value() - self.check_live_image() - except AttributeError: - pass + self.settings_dict["capture_region"].y = self.y_spinbox.value() def __update_width(self): - self.selection.right = self.selection.left + self.width_spinbox.value() - self.check_live_image() + self.settings_dict["capture_region"].width = self.width_spinbox.value() def __update_height(self): - self.selection.bottom = self.selection.top + self.height_spinbox.value() - self.check_live_image() + self.settings_dict["capture_region"].height = self.height_spinbox.value() def __take_screenshot(self): if not validate_before_parsing(self, check_empty_directory=False): @@ -411,13 +374,17 @@ def __take_screenshot(self): # which is a problem, but I doubt anyone will get to 1000 split images... screenshot_index = 1 while True: - screenshot_path = os.path.join(self.split_image_directory, f"{screenshot_index:03}_SplitImage.png") + screenshot_path = os.path.join( + self.settings_dict["split_image_directory"], + f"{screenshot_index:03}_SplitImage.png") if not os.path.exists(screenshot_path): break screenshot_index += 1 # Grab screenshot of capture region - capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked()) + capture = capture_region(self.hwnd, + self.settings_dict["capture_region"], + self.settings_dict["force_print_window"]) if capture is None: error_messages.region() return @@ -427,7 +394,7 @@ def __take_screenshot(self): os.startfile(screenshot_path) def __check_fps(self): - self.fps_value_label.setText(" ") + self.fps_value_label.clear() if not (validate_before_parsing(self) and parse_and_validate_images(self)): return @@ -444,9 +411,7 @@ def __check_fps(self): while count < CHECK_FPS_ITERATIONS: capture = self.__get_capture_for_comparison() _ = image.compare_with_capture(self, capture) - set_ui_image(self.current_split_image, image.bytes, True) count += 1 - self.current_split_image.clear() # calculate FPS t1 = time() @@ -470,9 +435,9 @@ def __undo_split(self, navigate_image_only: bool = False): return if not navigate_image_only: - for i, group in enumerate(self.split_groups): + for i, group in enumerate(self.split_groups,): if i > 0 and self.split_image_number in group: - self.split_image_number = self.split_groups[i - 1][0] + self.split_image_number = self.split_groups[i - 1][-1] break else: self.split_image_number -= 1 @@ -521,15 +486,15 @@ def start_auto_splitter(self): or (not self.start_auto_splitter_button.isEnabled() and not self.is_auto_controlled): return - start_label: str = self.start_image_label.text() + start_label: str = self.start_image_status_value_label.text() if start_label.endswith("ready") or start_label.endswith("paused"): - self.start_image_label.setText(f"{START_IMAGE_TEXT}: not ready") + self.start_image_status_value_label.setText("not ready") self.start_auto_splitter_signal.emit() def __check_for_reset(self): if self.start_auto_splitter_button.text() == START_AUTO_SPLITTER_TEXT: - if self.auto_start_on_reset_checkbox.isChecked(): + if self.settings_dict["loop_splits"]: self.start_auto_splitter_signal.emit() else: self.gui_changes_on_reset() @@ -537,7 +502,7 @@ def __check_for_reset(self): return False def __auto_splitter(self): - if not self.split_input.text() and not self.is_auto_controlled: + if not self.settings_dict["split_hotkey"] and not self.is_auto_controlled: self.gui_changes_on_reset() error_messages.split_hotkey() return @@ -606,20 +571,14 @@ def __auto_splitter(self): self.similarity = self.split_image.compare_with_capture(self, capture) # show live similarity if the checkbox is checked - self.live_similarity_label.setText( - str(self.similarity)[:4] - if self.show_live_similarity_checkbox.isChecked() - else " ") + self.table_current_image_live_label.setText(str(self.similarity)[:4]) # if the similarity becomes higher than highest similarity, set it as such. if self.similarity > self.highest_similarity: self.highest_similarity = self.similarity # show live highest similarity if the checkbox is checked - self.highest_similarity_label.setText( - str(self.highest_similarity)[:4] - if self.show_highest_similarity_checkbox.isChecked() - else " ") + self.table_current_image_highest_label.setText(str(self.highest_similarity)[:4]) # If its the last split image and last loop number, disable the next image button # If its the first split image, disable the undo split and previous image buttons @@ -645,7 +604,7 @@ def __auto_splitter(self): break # limit the number of time the comparison runs to reduce cpu usage - frame_interval: float = 1 / self.fps_limit_spinbox.value() + frame_interval: float = 1 / self.settings_dict["fps_limit"] # Email sent to pyqt@riverbankcomputing.com QtTest.QTest.qWait(int(frame_interval - (time() - start) % frame_interval)) # type: ignore QApplication.processEvents() @@ -663,13 +622,13 @@ def __auto_splitter(self): self.waiting_for_split_delay = True self.undo_split_button.setEnabled(False) self.skip_split_button.setEnabled(False) - self.current_split_image_file_label.setText(" ") + self.current_image_file_label.clear() # check for reset while delayed and display a counter of the remaining split delay time delay_start_time = time() while time() - delay_start_time < split_delay: - delay_time_left = round(split_delay - (time() - delay_start_time), 1) - self.current_split_image.setText(f"Delayed Split: {delay_time_left} sec remaining") + delay_time_left = split_delay - (time() - delay_start_time) + self.current_split_image.setText(f"Delayed Split: {seconds_remaining_text(delay_time_left)}") if self.__check_for_reset(): return @@ -687,7 +646,7 @@ def __auto_splitter(self): # if loop check box is checked and its the last split, go to first split. # else go to the next split image. - if self.loop_checkbox.isChecked() and self.split_image_number == number_of_split_images - 1: + if self.settings_dict["loop_splits"] and self.split_image_number == number_of_split_images - 1: self.split_image_number = 0 else: self.split_image_number += 1 @@ -716,8 +675,8 @@ def __auto_splitter(self): if pause_time > 0: pause_start_time = time() while time() - pause_start_time < pause_time: - pause_time_left = round(pause_time - (time() - pause_start_time), 1) - self.current_split_image.setText(f"None (Paused). {pause_time_left} sec remaining") + pause_time_left = pause_time - (time() - pause_start_time) + self.current_split_image.setText(f"None (Paused). {seconds_remaining_text(pause_time_left)}") if self.__check_for_reset(): return @@ -742,46 +701,49 @@ def gui_changes_on_start(self): self.timer_start_image.stop() self.start_auto_splitter_button.setText("Running...") self.browse_button.setEnabled(False) - self.start_image_reload_button.setEnabled(False) + self.reload_start_image_button.setEnabled(False) self.previous_image_button.setEnabled(True) self.next_image_button.setEnabled(True) + if self.SettingsWidget: + self.SettingsWidget.set_split_hotkey_button.setEnabled(False) + self.SettingsWidget.set_reset_hotkey_button.setEnabled(False) + self.SettingsWidget.set_skip_split_hotkey_button.setEnabled(False) + self.SettingsWidget.set_undo_split_hotkey_button.setEnabled(False) + self.SettingsWidget.set_pause_hotkey_button.setEnabled(False) + if not self.is_auto_controlled: self.start_auto_splitter_button.setEnabled(False) self.reset_button.setEnabled(True) self.undo_split_button.setEnabled(True) self.skip_split_button.setEnabled(True) - self.set_split_hotkey_button.setEnabled(False) - self.set_reset_hotkey_button.setEnabled(False) - self.set_skip_split_hotkey_button.setEnabled(False) - self.set_undo_split_hotkey_button.setEnabled(False) - self.set_pause_hotkey_button.setEnabled(False) QApplication.processEvents() def gui_changes_on_reset(self): self.start_auto_splitter_button.setText(START_AUTO_SPLITTER_TEXT) - self.image_loop_label.setText("Image Loop: -") - self.current_split_image.setText(" ") - self.current_split_image_file_label.setText(" ") - self.live_similarity_label.setText(" ") - self.highest_similarity_label.setText(" ") - self.current_similarity_threshold_number_label.setText(" ") + self.image_loop_value_label.setText("N/A") + self.current_split_image.clear() + self.current_image_file_label.clear() + self.table_current_image_live_label.setText("-") + self.table_current_image_highest_label.setText("-") + self.table_current_image_threshold_label.setText("-") self.browse_button.setEnabled(True) - self.start_image_reload_button.setEnabled(True) + self.reload_start_image_button.setEnabled(True) self.previous_image_button.setEnabled(False) self.next_image_button.setEnabled(False) + if self.SettingsWidget: + self.SettingsWidget.set_split_hotkey_button.setEnabled(True) + self.SettingsWidget.set_reset_hotkey_button.setEnabled(True) + self.SettingsWidget.set_skip_split_hotkey_button.setEnabled(True) + self.SettingsWidget.set_undo_split_hotkey_button.setEnabled(True) + self.SettingsWidget.set_pause_hotkey_button.setEnabled(True) if not self.is_auto_controlled: self.start_auto_splitter_button.setEnabled(True) self.reset_button.setEnabled(False) self.undo_split_button.setEnabled(False) self.skip_split_button.setEnabled(False) - self.set_split_hotkey_button.setEnabled(True) - self.set_reset_hotkey_button.setEnabled(True) - self.set_skip_split_hotkey_button.setEnabled(True) - self.set_undo_split_hotkey_button.setEnabled(True) - self.set_pause_hotkey_button.setEnabled(True) QApplication.processEvents() self.load_start_image(False, False) @@ -790,18 +752,22 @@ def __get_capture_for_comparison(self): """ Grab capture region and resize for comparison """ - capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked()) + capture = capture_region(self.hwnd, + self.settings_dict["capture_region"], + self.settings_dict["force_print_window"]) # This most likely means we lost capture (ie the captured window was closed, crashed, etc.) if capture is None: # Try to recover by using the window name self.live_image.setText("Trying to recover window...") # https://github.com/kaluluosi/pywin32-stubs/issues/7 - hwnd = win32gui.FindWindow(None, self.window_text) # type: ignore + hwnd = win32gui.FindWindow(None, self.settings_dict["captured_window_title"]) # type: ignore # Don't fallback to desktop if hwnd: self.hwnd = hwnd - capture = capture_region(self.hwnd, self.selection, self.force_print_window_checkbox.isChecked()) + capture = capture_region(self.hwnd, + self.settings_dict["capture_region"], + self.settings_dict["force_print_window"]) return None if capture is None else cv2.resize(capture, COMPARISON_RESIZE, interpolation=cv2.INTER_NEAREST) def __reset_if_should(self, capture: Optional[cv2.ndarray]): @@ -833,15 +799,15 @@ def __update_split_image(self, specific_image: Optional[AutoSplitImage] = None): if self.split_image.bytes is not None: set_ui_image(self.current_split_image, self.split_image.bytes, True) - self.current_split_image_file_label.setText(self.split_image.filename) - self.current_similarity_threshold_number_label.setText(f"{self.split_image.get_similarity_threshold(self):.2f}") + self.current_image_file_label.setText(self.split_image.filename) + self.table_current_image_threshold_label.setText(f"{self.split_image.get_similarity_threshold(self):.2f}") # Set Image Loop # if specific_image and specific_image.image_type == ImageType.START: - self.image_loop_label.setText("Image Loop: N/A") + self.image_loop_value_label.setText("N/A") else: loop_tuple = self.split_images_and_loop_number[self.split_image_number] - self.image_loop_label.setText(f"Image Loop: {loop_tuple[1]}/{loop_tuple[0].loops}") + self.image_loop_value_label.setText(f"{loop_tuple[1]}/{loop_tuple[0].loops}") self.highest_similarity = 0.0 # need to set split below threshold to false each time an image updates. @@ -894,6 +860,10 @@ def exit_program(): exit_program() +def seconds_remaining_text(seconds: float): + return f"{seconds:.1f} second{'' if 0 < seconds <= 1 else 's'} remaining" + + def main(): # Call to QApplication outside the try-except so we can show error messages app = QApplication(sys.argv) diff --git a/src/AutoSplitImage.py b/src/AutoSplitImage.py index 126f2abe..9eeaf8a9 100644 --- a/src/AutoSplitImage.py +++ b/src/AutoSplitImage.py @@ -46,7 +46,7 @@ def get_pause_time(self, default: Union[AutoSplit, float]): """ default_value: float = default \ if isinstance(default, float) \ - else default.pause_spinbox.value() + else default.settings_dict["default_pause_time"] return default_value if self.__pause_time is None else self.__pause_time def get_similarity_threshold(self, default: Union[AutoSplit, float]): @@ -55,7 +55,7 @@ def get_similarity_threshold(self, default: Union[AutoSplit, float]): """ default_value: float = default \ if isinstance(default, float) \ - else default.similarity_threshold_spinbox.value() + else default.settings_dict["default_similarity_threshold"] return default_value if self.__similarity_threshold is None else self.__similarity_threshold def __init__(self, path: str): @@ -109,7 +109,7 @@ def compare_with_capture( """ comparison_method: int = comparison \ if isinstance(comparison, int) \ - else comparison.comparison_method_combobox.currentIndex() + else comparison.settings_dict["default_comparison_method"] if self.bytes is None or capture is None: return 0.0 diff --git a/src/capture_windows.py b/src/capture_windows.py index c991cc38..c6401efa 100644 --- a/src/capture_windows.py +++ b/src/capture_windows.py @@ -20,17 +20,15 @@ @dataclass -class Rect(ctypes.wintypes.RECT): - """ - Overrides `ctypes.wintypes.RECT` to replace c_long with int for math operators - """ - left: int = -1 # type: ignore - top: int = -1 # type: ignore - right: int = -1 # type: ignore - bottom: int = -1 # type: ignore +class Region(): + def __init__(self, x: int, y: int, width: int, height: int): + self.x = x + self.y = y + self.width = width + self.height = height -def capture_region(hwnd: int, selection: Rect, print_window: bool): +def capture_region(hwnd: int, selection: Region, print_window: bool): """ Captures an image of the region for a window matching the given parameters of the bounding box @@ -40,8 +38,6 @@ def capture_region(hwnd: int, selection: Rect, print_window: bool): @return: The image of the region in the window in BGRA format """ - width: int = selection.right - selection.left - height: int = selection.bottom - selection.top # If the window closes while it's being manipulated, it could cause a crash try: window_dc: int = win32gui.GetWindowDC(hwnd) @@ -54,16 +50,17 @@ def capture_region(hwnd: int, selection: Rect, print_window: bool): compatible_dc = cast(PyCDC, dc_object.CreateCompatibleDC()) bitmap: PyCBitmap = win32ui.CreateBitmap() - bitmap.CreateCompatibleBitmap(dc_object, width, height) + bitmap.CreateCompatibleBitmap(dc_object, selection.width, selection.height) compatible_dc.SelectObject(bitmap) - compatible_dc.BitBlt((0, 0), (width, height), dc_object, (selection.left, selection.top), win32con.SRCCOPY) + compatible_dc.BitBlt((0, 0), (selection.width, selection.height), dc_object, + (selection.x, selection.y), win32con.SRCCOPY) # https://github.com/kaluluosi/pywin32-stubs/issues/5 # pylint: disable=no-member except (win32ui.error, pywintypes.error): # type: ignore return None image = np.frombuffer(cast(bytes, bitmap.GetBitmapBits(True)), dtype="uint8") - image.shape = (height, width, 4) + image.shape = (selection.height, selection.width, 4) try: dc_object.DeleteDC() diff --git a/src/error_messages.py b/src/error_messages.py index 5d8bd382..cb437f36 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -31,12 +31,12 @@ def split_image_directory_empty(): def image_type(image: str): set_text_message(f'"{image}" is not a valid image file, does not exist, ' - "or the full image file path contains a special character.") + + "or the full image file path contains a special character.") def region(): set_text_message("No region is selected or the Capture Region window is not open. " - "Select a region or load settings while the Capture Region window is open.") + + "Select a region or load settings while the Capture Region window is open.") def split_hotkey(): @@ -45,7 +45,7 @@ def split_hotkey(): def pause_hotkey(): set_text_message("Your split image folder contains an image filename with a pause flag {p}, " - "but no pause hotkey is set.") + + "but no pause hotkey is set.") def align_region_image_type(): @@ -82,7 +82,7 @@ def no_settings_file_on_open(): def too_many_settings_files_on_open(): set_text_message("Too many settings files found. " - "Only one can be loaded on open if placed in the same folder as AutoSplit.exe") + + "Only one can be loaded on open if placed in the same folder as AutoSplit.exe") def check_for_updates(): @@ -91,7 +91,7 @@ def check_for_updates(): def load_start_image(): set_text_message("Start Image found, but cannot be loaded unless Start, Reset, and Pause hotkeys are set. " - "Please set these hotkeys, and then click the Reload Start Image button.") + + "Please set these hotkeys, and then click the Reload Start Image button.") def stdin_lost(): diff --git a/src/hotkeys.py b/src/hotkeys.py index 80701127..9fcdf23c 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -1,14 +1,15 @@ from __future__ import annotations from typing import Literal, Optional, TYPE_CHECKING, Union from collections.abc import Callable + if TYPE_CHECKING: from AutoSplit import AutoSplit import threading -from keyboard._keyboard_event import KeyboardEvent, KEY_DOWN + import keyboard # https://github.com/boppreh/keyboard/issues/505 import pyautogui # https://github.com/asweigart/pyautogui/issues/645 -# While not usually recommended, we don'thread manipulate the mouse, and we don'thread want the extra delay +# While not usually recommended, we don't manipulate the mouse, and we don't want the extra delay pyautogui.FAILSAFE = False SET_HOTKEY_TEXT = "Set Hotkey" @@ -18,27 +19,29 @@ # do all of these after you click "Set Hotkey" but before you type the hotkey. def before_setting_hotkey(autosplit: AutoSplit): autosplit.start_auto_splitter_button.setEnabled(False) - autosplit.set_split_hotkey_button.setEnabled(False) - autosplit.set_reset_hotkey_button.setEnabled(False) - autosplit.set_skip_split_hotkey_button.setEnabled(False) - autosplit.set_undo_split_hotkey_button.setEnabled(False) - autosplit.set_pause_hotkey_button.setEnabled(False) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_split_hotkey_button.setEnabled(False) + autosplit.SettingsWidget.set_reset_hotkey_button.setEnabled(False) + autosplit.SettingsWidget.set_skip_split_hotkey_button.setEnabled(False) + autosplit.SettingsWidget.set_undo_split_hotkey_button.setEnabled(False) + autosplit.SettingsWidget.set_pause_hotkey_button.setEnabled(False) # do all of these things after you set a hotkey. a signal connects to this because # changing GUI stuff in the hotkey thread was causing problems def after_setting_hotkey(autosplit: AutoSplit): - autosplit.set_split_hotkey_button.setText(SET_HOTKEY_TEXT) - autosplit.set_reset_hotkey_button.setText(SET_HOTKEY_TEXT) - autosplit.set_skip_split_hotkey_button.setText(SET_HOTKEY_TEXT) - autosplit.set_undo_split_hotkey_button.setText(SET_HOTKEY_TEXT) - autosplit.set_pause_hotkey_button.setText(SET_HOTKEY_TEXT) autosplit.start_auto_splitter_button.setEnabled(True) - autosplit.set_split_hotkey_button.setEnabled(True) - autosplit.set_reset_hotkey_button.setEnabled(True) - autosplit.set_skip_split_hotkey_button.setEnabled(True) - autosplit.set_undo_split_hotkey_button.setEnabled(True) - autosplit.set_pause_hotkey_button.setEnabled(True) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_split_hotkey_button.setText(SET_HOTKEY_TEXT) + autosplit.SettingsWidget.set_reset_hotkey_button.setText(SET_HOTKEY_TEXT) + autosplit.SettingsWidget.set_skip_split_hotkey_button.setText(SET_HOTKEY_TEXT) + autosplit.SettingsWidget.set_undo_split_hotkey_button.setText(SET_HOTKEY_TEXT) + autosplit.SettingsWidget.set_pause_hotkey_button.setText(SET_HOTKEY_TEXT) + autosplit.SettingsWidget.set_split_hotkey_button.setEnabled(True) + autosplit.SettingsWidget.set_reset_hotkey_button.setEnabled(True) + autosplit.SettingsWidget.set_skip_split_hotkey_button.setEnabled(True) + autosplit.SettingsWidget.set_undo_split_hotkey_button.setEnabled(True) + autosplit.SettingsWidget.set_pause_hotkey_button.setEnabled(True) def is_digit(key: Optional[str]): @@ -57,15 +60,15 @@ def send_command(autosplit: AutoSplit, command: Commands): if autosplit.is_auto_controlled: print(command, flush=True) elif command in {"split", "start"}: - _send_hotkey(autosplit.split_input.text()) + _send_hotkey(autosplit.settings_dict["split_hotkey"]) elif command == "pause": - _send_hotkey(autosplit.pause_input.text()) + _send_hotkey(autosplit.settings_dict["pause_hotkey"]) elif command == "reset": - _send_hotkey(autosplit.reset_input.text()) + _send_hotkey(autosplit.settings_dict["reset_hotkey"]) elif command == "skip": - _send_hotkey(autosplit.skip_split_input.text()) + _send_hotkey(autosplit.settings_dict["skip_split_hotkey"]) elif command == "undo": - _send_hotkey(autosplit.undo_split_input.text()) + _send_hotkey(autosplit.settings_dict["undo_split_hotkey"]) else: raise KeyError(f"'{command}' is not a valid LiveSplit.AutoSplitIntegration command") @@ -97,11 +100,11 @@ def _send_hotkey(key_or_scan_code: Union[int, str]): pyautogui.hotkey(key_or_scan_code.replace(" ", "")) -def __validate_keypad(expected_key: str, keyboard_event: KeyboardEvent) -> bool: +def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) -> bool: # Prevent "(keypad)delete", "(keypad)./decimal" and "del" from triggering each other # as well as "." and "(keypad)./decimal" if keyboard_event.scan_code in {83, 52}: - # TODO: "del" won'thread work with "(keypad)delete" if localized in non-english (ie: "suppr" in french) + # TODO: "del" won't work with "(keypad)delete" if localized in non-english (ie: "suppr" in french) return expected_key == keyboard_event.name # Prevent "action keys" from triggering "keypad keys" if keyboard_event.name and is_digit(keyboard_event.name[-1]): @@ -122,34 +125,35 @@ def __validate_keypad(expected_key: str, keyboard_event: KeyboardEvent) -> bool: # We're doing the check here instead of saving the key code because it'll # cause issues with save files and the non-keypad shared keys are localized -# while the keypad ones aren'thread. +# while the keypad ones aren't. -# Since we reuse the key string we set to send to LiveSplit, we can'thread use fake names like "num home". +# 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. -def _hotkey_action(keyboard_event: KeyboardEvent, key_name: str, action: Callable[[], None]): - if keyboard_event.event_type == KEY_DOWN and __validate_keypad(key_name, keyboard_event): +def _hotkey_action(keyboard_event: keyboard.KeyboardEvent, key_name: str, action: Callable[[], None]): + if keyboard_event.event_type == keyboard.KEY_DOWN and __validate_keypad(key_name, keyboard_event): action() -def __get_key_name(keyboard_event: KeyboardEvent): +def __get_key_name(keyboard_event: keyboard.KeyboardEvent): return f"num {keyboard_event.name}" \ if keyboard_event.is_keypad and is_digit(keyboard_event.name) \ else str(keyboard_event.name) def __is_key_already_set(autosplit: AutoSplit, key_name: str): - return key_name in (autosplit.split_input.text(), - autosplit.reset_input.text(), - autosplit.skip_split_input.text(), - autosplit.undo_split_input.text(), - autosplit.pause_input.text()) + return key_name in (autosplit.settings_dict["split_hotkey"], + autosplit.settings_dict["reset_hotkey"], + autosplit.settings_dict["skip_split_hotkey"], + autosplit.settings_dict["undo_split_hotkey"], + autosplit.settings_dict["pause_hotkey"]) # --------------------HOTKEYS-------------------------- # TODO: Refactor to de-duplicate all this code, including settings_file.py # Going to comment on one func, and others will be similar. def set_split_hotkey(autosplit: AutoSplit, preselected_key: str = ""): - autosplit.set_split_hotkey_button.setText(PRESS_A_KEY_TEXT) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_split_hotkey_button.setText(PRESS_A_KEY_TEXT) # disable some buttons before_setting_hotkey(autosplit) @@ -188,7 +192,9 @@ def callback(): autosplit.split_hotkey = keyboard.hook_key( key_name, lambda error: _hotkey_action(error, key_name, autosplit.start_auto_splitter)) - autosplit.split_input.setText(key_name) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.split_input.setText(key_name) + autosplit.settings_dict["split_hotkey"] = key_name autosplit.after_setting_hotkey_signal.emit() # try to remove the previously set hotkey if there is one. @@ -198,7 +204,8 @@ def callback(): def set_reset_hotkey(autosplit: AutoSplit, preselected_key: str = ""): - autosplit.set_reset_hotkey_button.setText(PRESS_A_KEY_TEXT) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_reset_hotkey_button.setText(PRESS_A_KEY_TEXT) before_setting_hotkey(autosplit) def callback(): @@ -215,7 +222,9 @@ def callback(): autosplit.reset_hotkey = keyboard.hook_key( key_name, lambda error: _hotkey_action(error, key_name, autosplit.reset_signal.emit)) - autosplit.reset_input.setText(key_name) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.reset_input.setText(key_name) + autosplit.settings_dict["reset_hotkey"] = key_name autosplit.after_setting_hotkey_signal.emit() _unhook(autosplit.reset_hotkey) @@ -224,7 +233,8 @@ def callback(): def set_skip_split_hotkey(autosplit: AutoSplit, preselected_key: str = ""): - autosplit.set_skip_split_hotkey_button.setText(PRESS_A_KEY_TEXT) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_skip_split_hotkey_button.setText(PRESS_A_KEY_TEXT) before_setting_hotkey(autosplit) def callback(): @@ -241,7 +251,9 @@ def callback(): autosplit.skip_split_hotkey = keyboard.hook_key( key_name, lambda error: _hotkey_action(error, key_name, autosplit.skip_split_signal.emit)) - autosplit.skip_split_input.setText(key_name) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.skip_split_input.setText(key_name) + autosplit.settings_dict["skip_split_hotkey"] = key_name autosplit.after_setting_hotkey_signal.emit() _unhook(autosplit.skip_split_hotkey) @@ -250,7 +262,8 @@ def callback(): def set_undo_split_hotkey(autosplit: AutoSplit, preselected_key: str = ""): - autosplit.set_undo_split_hotkey_button.setText(PRESS_A_KEY_TEXT) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_undo_split_hotkey_button.setText(PRESS_A_KEY_TEXT) before_setting_hotkey(autosplit) def callback(): @@ -267,7 +280,9 @@ def callback(): autosplit.undo_split_hotkey = keyboard.hook_key( key_name, lambda error: _hotkey_action(error, key_name, autosplit.undo_split_signal.emit)) - autosplit.undo_split_input.setText(key_name) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.undo_split_input.setText(key_name) + autosplit.settings_dict["undo_split_hotkey"] = key_name autosplit.after_setting_hotkey_signal.emit() _unhook(autosplit.undo_split_hotkey) @@ -276,7 +291,8 @@ def callback(): def set_pause_hotkey(autosplit: AutoSplit, preselected_key: str = ""): - autosplit.set_pause_hotkey_button.setText(PRESS_A_KEY_TEXT) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.set_pause_hotkey_button.setText(PRESS_A_KEY_TEXT) before_setting_hotkey(autosplit) def callback(): @@ -293,7 +309,9 @@ def callback(): autosplit.pause_hotkey = keyboard.hook_key( key_name, lambda error: _hotkey_action(error, key_name, autosplit.pause_signal.emit)) - autosplit.pause_input.setText(key_name) + if autosplit.SettingsWidget: + autosplit.SettingsWidget.pause_input.setText(key_name) + autosplit.settings_dict["pause_hotkey"] = key_name autosplit.after_setting_hotkey_signal.emit() _unhook(autosplit.pause_hotkey) diff --git a/src/menu_bar.py b/src/menu_bar.py index 66cb9c85..d90c1e8c 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -14,7 +15,9 @@ import error_messages import settings_file as settings -from gen import about, design, resources_rc, update_checker # noqa: F401 +from capture_windows import Region +from gen import about, design, resources_rc, settings as settings_ui, update_checker # noqa: F401 +from hotkeys import set_split_hotkey, set_reset_hotkey, set_skip_split_hotkey, set_undo_split_hotkey, set_pause_hotkey # AutoSplit Version number VERSION = "1.6.1" @@ -82,7 +85,7 @@ def __init__(self, autosplit: AutoSplit, check_on_open: bool): def run(self): try: response = requests.get("https://api.github.com/repos/Toufool/Auto-Split/releases/latest") - latest_version = response.json()["name"].split("v")[1] + latest_version = str(response.json()["name"]).split("v")[1] self.autosplit.update_checker_widget_signal.emit(latest_version, self.check_on_open) except (RequestException, KeyError, JSONDecodeError): if not self.check_on_open: @@ -92,3 +95,107 @@ def run(self): def check_for_updates(autosplit: AutoSplit, check_on_open: bool = False): autosplit.CheckForUpdatesThread = __CheckForUpdatesThread(autosplit, check_on_open) autosplit.CheckForUpdatesThread.start() + + +class __SettingsWidget(QtWidgets.QDialog, settings_ui.Ui_DialogSettings): + def __init__(self, autosplit: AutoSplit): + super().__init__() + self.setupUi(self) + self.autosplit = autosplit + + def set_value(key: str, value: Any): + autosplit.settings_dict[key] = value + +# region Set initial values + # Hotkeys + self.split_input.setText(autosplit.settings_dict["split_hotkey"]) + self.reset_input.setText(autosplit.settings_dict["reset_hotkey"]) + self.undo_split_input.setText(autosplit.settings_dict["undo_split_hotkey"]) + self.skip_split_input.setText(autosplit.settings_dict["skip_split_hotkey"]) + self.pause_input.setText(autosplit.settings_dict["pause_hotkey"]) + + # Capture Settings + self.fps_limit_spinbox.setValue(autosplit.settings_dict["fps_limit"]) + self.live_capture_region_checkbox.setChecked(autosplit.settings_dict["live_capture_region"]) + self.force_print_window_checkbox.setChecked(autosplit.settings_dict["force_print_window"]) + + # Image Settings + self.default_comparison_method.setCurrentIndex(autosplit.settings_dict["default_comparison_method"]) + self.default_similarity_threshold_spinbox.setValue(autosplit.settings_dict["default_similarity_threshold"]) + self.default_delay_time_spinbox.setValue(autosplit.settings_dict["default_delay_time"]) + self.default_pause_time_spinbox.setValue(autosplit.settings_dict["default_pause_time"]) + self.loop_splits_checkbox.setChecked(autosplit.settings_dict["loop_splits"]) +# endregion +# region Binding + # Hotkeys + self.set_split_hotkey_button.clicked.connect(lambda: set_split_hotkey(self.autosplit)) + self.set_reset_hotkey_button.clicked.connect(lambda: set_reset_hotkey(self.autosplit)) + self.set_skip_split_hotkey_button.clicked.connect(lambda: set_skip_split_hotkey(self.autosplit)) + self.set_undo_split_hotkey_button.clicked.connect(lambda: set_undo_split_hotkey(self.autosplit)) + self.set_pause_hotkey_button.clicked.connect(lambda: set_pause_hotkey(self.autosplit)) + + # Capture Settings + self.fps_limit_spinbox.valueChanged.connect(lambda: set_value( + "fps_limit", + self.fps_limit_spinbox.value())) + self.live_capture_region_checkbox.stateChanged.connect(lambda: set_value( + "live_capture_region", + self.live_capture_region_checkbox.isChecked())) + self.force_print_window_checkbox.stateChanged.connect(lambda: set_value( + "force_print_window", + self.force_print_window_checkbox.isChecked())) + + # Image Settings + self.default_comparison_method.currentIndexChanged.connect(lambda: set_value( + "default_comparison_method", + self.default_comparison_method.currentIndex())) + self.default_similarity_threshold_spinbox.valueChanged.connect(lambda: set_value( + "default_similarity_threshold", + self.default_similarity_threshold_spinbox.value())) + self.default_delay_time_spinbox.valueChanged.connect(lambda: set_value( + "default_delay_time", + self.default_delay_time_spinbox.value())) + self.default_pause_time_spinbox.valueChanged.connect(lambda: set_value( + "default_pause_time", + self.default_pause_time_spinbox.value())) + self.loop_splits_checkbox.stateChanged.connect(lambda: set_value( + "loop_splits", + self.loop_splits_checkbox.isChecked())) +# endregion + + self.show() + + +def open_settings(autosplit: AutoSplit): + autosplit.SettingsWidget = __SettingsWidget(autosplit) + + +def get_default_settings_from_ui(autosplit: AutoSplit): + temp_dialog = QtWidgets.QDialog() + default_settings_dialog = settings_ui.Ui_DialogSettings() + default_settings_dialog.setupUi(temp_dialog) + default_settings: settings.SettingsDict = { + "split_hotkey": default_settings_dialog.split_input.text(), + "reset_hotkey": default_settings_dialog.reset_input.text(), + "undo_split_hotkey": default_settings_dialog.undo_split_input.text(), + "skip_split_hotkey": default_settings_dialog.skip_split_input.text(), + "pause_hotkey": default_settings_dialog.pause_input.text(), + "fps_limit": default_settings_dialog.fps_limit_spinbox.value(), + "live_capture_region": default_settings_dialog.live_capture_region_checkbox.isChecked(), + "force_print_window": default_settings_dialog.force_print_window_checkbox.isChecked(), + "default_comparison_method": default_settings_dialog.default_comparison_method.currentIndex(), + "default_similarity_threshold": default_settings_dialog.default_similarity_threshold_spinbox.value(), + "default_delay_time": default_settings_dialog.default_delay_time_spinbox.value(), + "default_pause_time": default_settings_dialog.default_pause_time_spinbox.value(), + "loop_splits": default_settings_dialog.loop_splits_checkbox.isChecked(), + + "split_image_directory": autosplit.split_image_folder_input.text(), + "captured_window_title": "", + "capture_region": Region( + autosplit.x_spinbox.value(), + autosplit.y_spinbox.value(), + autosplit.width_spinbox.value(), + autosplit.height_spinbox.value()) + } + del temp_dialog + return default_settings diff --git a/src/screen_region.py b/src/screen_region.py index 03bbf85e..2342dd8d 100644 --- a/src/screen_region.py +++ b/src/screen_region.py @@ -45,7 +45,7 @@ def select_region(autosplit: AutoSplit): error_messages.region() return autosplit.hwnd = hwnd - autosplit.window_text = window_text + autosplit.settings_dict["captured_window_title"] = window_text offset_x, offset_y, *_ = win32gui.GetWindowRect(autosplit.hwnd) __set_region_values(autosplit, @@ -76,7 +76,7 @@ def select_window(autosplit: AutoSplit): error_messages.region() return autosplit.hwnd = hwnd - autosplit.window_text = window_text + autosplit.settings_dict["captured_window_title"] = window_text # Getting window bounds # On Windows there is a shadow around the windows that we need to account for @@ -134,8 +134,8 @@ def align_region(autosplit: AutoSplit): # subregion being searched for to align the image. capture = capture_windows.capture_region( autosplit.hwnd, - autosplit.selection, - autosplit.force_print_window_checkbox.isChecked()) + autosplit.settings_dict["capture_region"], + autosplit.settings_dict["force_print_window"]) if capture is None: error_messages.region() @@ -151,25 +151,23 @@ def align_region(autosplit: AutoSplit): # The new region can be defined by using the min_loc point and the best_height and best_width of the template. __set_region_values(autosplit, - left=autosplit.selection.left + best_loc[0], - top=autosplit.selection.top + best_loc[1], + left=autosplit.settings_dict["capture_region"].x + best_loc[0], + top=autosplit.settings_dict["capture_region"].y + best_loc[1], width=best_width, height=best_height) def __set_region_values(autosplit: AutoSplit, left: int, top: int, width: int, height: int): - autosplit.selection.left = left - autosplit.selection.top = top - autosplit.selection.right = left + width - autosplit.selection.bottom = top + height + autosplit.settings_dict["capture_region"].x = left + autosplit.settings_dict["capture_region"].y = top + autosplit.settings_dict["capture_region"].width = width + autosplit.settings_dict["capture_region"].height = height autosplit.x_spinbox.setValue(left) autosplit.y_spinbox.setValue(top) autosplit.width_spinbox.setValue(width) autosplit.height_spinbox.setValue(height) - autosplit.check_live_image() - def __test_alignment(capture: cv2.ndarray, template: cv2.ndarray): # Obtain the best matching point for the template within the @@ -213,11 +211,11 @@ def __test_alignment(capture: cv2.ndarray, template: cv2.ndarray): def validate_before_parsing(autosplit: AutoSplit, show_error: bool = True, check_empty_directory: bool = True): error = None - if not autosplit.split_image_directory: + if not autosplit.settings_dict["split_image_directory"]: error = error_messages.split_image_directory - elif not os.path.isdir(autosplit.split_image_directory): + elif not os.path.isdir(autosplit.settings_dict["split_image_directory"]): error = error_messages.split_image_directory_not_found - elif check_empty_directory and not os.listdir(autosplit.split_image_directory): + elif check_empty_directory and not os.listdir(autosplit.settings_dict["split_image_directory"]): error = error_messages.split_image_directory_empty elif autosplit.hwnd <= 0 or not win32gui.GetWindowText(autosplit.hwnd): error = error_messages.region diff --git a/src/settings_file.py b/src/settings_file.py index c0315c23..cc6fec44 100644 --- a/src/settings_file.py +++ b/src/settings_file.py @@ -1,5 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict + if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -12,6 +13,7 @@ from PyQt6 import QtCore, QtWidgets import error_messages +from capture_windows import Region from gen import design from hotkeys import set_pause_hotkey, set_reset_hotkey, set_skip_split_hotkey, set_split_hotkey, set_undo_split_hotkey @@ -21,6 +23,26 @@ auto_split_directory = os.path.dirname(sys.executable if FROZEN else os.path.abspath(__file__)) +class SettingsDict(TypedDict): + split_hotkey: str + reset_hotkey: str + undo_split_hotkey: str + skip_split_hotkey: str + pause_hotkey: str + fps_limit: int + live_capture_region: bool + force_print_window: bool + default_comparison_method: int + default_similarity_threshold: float + default_delay_time: int + default_pause_time: float + loop_splits: bool + + split_image_directory: str + captured_window_title: str + capture_region: Region + + class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module: str, name: str): @@ -29,27 +51,27 @@ def find_class(self, module: str, name: str): def get_save_settings_values(autosplit: AutoSplit): return [ - autosplit.split_image_directory, - autosplit.similarity_threshold_spinbox.value(), - autosplit.comparison_method_combobox.currentIndex(), - autosplit.pause_spinbox.value(), - int(autosplit.fps_limit_spinbox.value()), - autosplit.split_input.text(), - autosplit.reset_input.text(), - autosplit.skip_split_input.text(), - autosplit.undo_split_input.text(), - autosplit.pause_input.text(), - autosplit.x_spinbox.value(), - autosplit.y_spinbox.value(), - autosplit.width_spinbox.value(), - autosplit.height_spinbox.value(), - autosplit.window_text, + autosplit.settings_dict["split_image_directory"], + autosplit.settings_dict["default_similarity_threshold"], + autosplit.settings_dict["default_comparison_method"], + autosplit.settings_dict["default_pause_time"], + autosplit.settings_dict["fps_limit"], + autosplit.settings_dict["split_hotkey"], + autosplit.settings_dict["reset_hotkey"], + autosplit.settings_dict["skip_split_hotkey"], + autosplit.settings_dict["undo_split_hotkey"], + autosplit.settings_dict["pause_hotkey"], + autosplit.settings_dict["capture_region"].x, + autosplit.settings_dict["capture_region"].y, + autosplit.settings_dict["capture_region"].width, + autosplit.settings_dict["capture_region"].height, + autosplit.settings_dict["captured_window_title"], 0, 0, 1, - int(autosplit.loop_checkbox.isChecked()), - int(autosplit.auto_start_on_reset_checkbox.isChecked()), - autosplit.force_print_window_checkbox.isChecked()] + autosplit.settings_dict["loop_splits"], + 0, + autosplit.settings_dict["force_print_window"]] def have_settings_changed(autosplit: AutoSplit): @@ -123,12 +145,12 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str autosplit.show_error_signal.emit(error_messages.invalid_settings) return False - autosplit.split_image_directory = settings[0] + autosplit.settings_dict["split_image_directory"] = settings[0] autosplit.split_image_folder_input.setText(settings[0]) - autosplit.similarity_threshold_spinbox.setValue(settings[1]) - autosplit.comparison_method_combobox.setCurrentIndex(settings[2]) - autosplit.pause_spinbox.setValue(settings[3]) - autosplit.fps_limit_spinbox.setValue(settings[4]) + autosplit.settings_dict["default_similarity_threshold"] = settings[1] + autosplit.settings_dict["default_comparison_method"] = settings[2] + autosplit.settings_dict["default_pause_time"] = settings[3] + autosplit.settings_dict["fps_limit"] = settings[4] keyboard.unhook_all() if not autosplit.is_auto_controlled: set_split_hotkey(autosplit, settings[5]) @@ -140,20 +162,19 @@ def __load_settings_from_file(autosplit: AutoSplit, load_settings_file_path: str autosplit.y_spinbox.setValue(settings[11]) autosplit.width_spinbox.setValue(settings[12]) autosplit.height_spinbox.setValue(settings[13]) - autosplit.window_text = settings[14] - autosplit.loop_checkbox.setChecked(bool(settings[18])) - autosplit.auto_start_on_reset_checkbox.setChecked(bool(settings[19])) - autosplit.force_print_window_checkbox.setChecked(settings[20]) + autosplit.settings_dict["captured_window_title"] = settings[14] + autosplit.settings_dict["loop_splits"] = settings[18] + autosplit.settings_dict["force_print_window"] = settings[20] - if autosplit.window_text: + if autosplit.settings_dict["captured_window_title"]: # https://github.com/kaluluosi/pywin32-stubs/issues/7 - hwnd = win32gui.FindWindow(None, autosplit.window_text) # type: ignore + hwnd = win32gui.FindWindow(None, autosplit.settings_dict["captured_window_title"]) # type: ignore if hwnd: autosplit.hwnd = hwnd else: autosplit.live_image.setText("Reload settings after opening" - f'\n"{autosplit.window_text}"' - "\nto automatically load Live Capture") + + f'\n"{autosplit.settings_dict["captured_window_title"]}"' + + "\nto automatically load Capture Region") return True @@ -170,7 +191,6 @@ def load_settings( return autosplit.last_successfully_loaded_settings_file_path = load_settings_file_path - autosplit.check_live_image() autosplit.load_start_image() diff --git a/src/split_parser.py b/src/split_parser.py index 27862ed9..6e4a584f 100644 --- a/src/split_parser.py +++ b/src/split_parser.py @@ -153,9 +153,9 @@ def __pop_image_type(split_image: list[AutoSplitImage], image_type: ImageType): def parse_and_validate_images(autosplit: AutoSplit): # Get split images all_images = [ - AutoSplitImage(os.path.join(autosplit.split_image_directory, image_name)) + AutoSplitImage(os.path.join(autosplit.settings_dict["split_image_directory"], image_name)) for image_name - in os.listdir(autosplit.split_image_directory)] + in os.listdir(autosplit.settings_dict["split_image_directory"])] # Find non-split images and then remove them from the list autosplit.start_image = __pop_image_type(all_images, ImageType.START) @@ -171,7 +171,7 @@ def parse_and_validate_images(autosplit: AutoSplit): return False # error out if there is a {p} flag but no pause hotkey set and is not auto controlled. - if (not autosplit.pause_input.text() + if (not autosplit.settings_dict["pause_hotkey"] and image.check_flag(PAUSE_FLAG) and not autosplit.is_auto_controlled): autosplit.gui_changes_on_reset() @@ -181,7 +181,7 @@ def parse_and_validate_images(autosplit: AutoSplit): # Check that there's only one reset image if image.image_type == ImageType.RESET: # If there is no reset hotkey set but a reset image is present, and is not auto controlled, throw an error. - if not autosplit.reset_input.text() and not autosplit.is_auto_controlled: + if not autosplit.settings_dict["reset_hotkey"] and not autosplit.is_auto_controlled: autosplit.gui_changes_on_reset() error_messages.reset_hotkey() return False diff --git a/typings/keyboard/__init__.pyi b/typings/keyboard/__init__.pyi index 18f68af6..86bd06ec 100644 --- a/typings/keyboard/__init__.pyi +++ b/typings/keyboard/__init__.pyi @@ -3,6 +3,7 @@ This type stub file was generated by pyright. """ from __future__ import print_function as _print_function import typing +import collections.abc import re as _re import itertools as _itertools @@ -13,6 +14,7 @@ from threading import Lock as _Lock, Thread as _Thread from ._keyboard_event import KEY_DOWN, KEY_UP, KeyboardEvent from ._generic import GenericListener as _GenericListener from ._canonical_names import all_modifiers, normalize_name, sided_modifiers +__all__ = ["all_modifiers", "normalize_name", "sided_modifiers", "KEY_DOWN", "KEY_UP", "KeyboardEvent"] try: # Python2 @@ -103,12 +105,12 @@ key events. In this case `keyboard` will be unable to report events. - This program makes no attempt to hide itself, so don't use it for keyloggers or online gaming bots. Be responsible. """ -Callback = typing.Callable[[KeyboardEvent], None] +Callback = collections.abc.Callable[[KeyboardEvent], None] version: str -_is_str = typing.Callable[[typing.Any], bool] -_is_number = typing.Callable[[typing.Any], bool] -_is_list: typing.Callable[[typing.Any], bool] +_is_str = collections.abc.Callable[[typing.Any], bool] +_is_number = collections.abc.Callable[[typing.Any], bool] +_is_list: collections.abc.Callable[[typing.Any], bool] class _State: @@ -120,10 +122,6 @@ class _Event(_UninterruptibleEvent): ... -if _platform.system() == 'Windows': - ... -else: - ... _modifier_scan_codes: set @@ -197,14 +195,16 @@ class _KeyboardListener(_GenericListener): _listener: _KeyboardListener -def key_to_scan_codes(key: typing.Union[int, str, typing.List[typing.Union[int, str]]], error_if_missing: bool = ...) -> typing.List[int]: +def key_to_scan_codes(key: typing.Union[int, str, typing.List[typing.Union[int, str]]], + error_if_missing: bool = ...) -> typing.List[int]: """ Returns a list of scan codes associated with this key (name or scan code). """ ... -def parse_hotkey(hotkey) -> tuple[tuple[tuple[Unknown] | Unknown | tuple[()] | tuple[Unknown, ...]]] | tuple[tuple[tuple[Unknown] | Unknown | tuple[()] | tuple[Unknown, ...], ...]] | tuple[Unknown, ...]: +def parse_hotkey(hotkey) -> tuple[tuple[tuple[Unknown] | Unknown | tuple[()] | tuple[Unknown, ...]] + ] | tuple[tuple[tuple[Unknown] | Unknown | tuple[()] | tuple[Unknown, ...], ...]] | tuple[Unknown, ...]: """ Parses a user-provided hotkey into nested tuples representing the parsed structure, with the bottom values being lists of scan codes. @@ -274,10 +274,10 @@ def call_later(fn, args=..., delay=...) -> None: ... -_hooks: dict[typing.Callable, Unknown] +_hooks: dict[collections.abc.Callable, Unknown] -def hook(callback: Callback, suppress=..., on_remove=...) -> typing.Callable[[], None]: +def hook(callback: Callback, suppress=..., on_remove=...) -> collections.abc.Callable[[], None]: """ Installs a global listener on all available keyboards, invoking `callback` each time a key is pressed or released. @@ -296,21 +296,22 @@ def hook(callback: Callback, suppress=..., on_remove=...) -> typing.Callable[[], ... -def on_press(callback: Callback, suppress=...) -> typing.Callable[[], None]: +def on_press(callback: Callback, suppress=...) -> collections.abc.Callable[[], None]: """ Invokes `callback` for every KEY_DOWN event. For details see `hook`. """ ... -def on_release(callback: Callback, suppress=...) -> typing.Callable[[], None]: +def on_release(callback: Callback, suppress=...) -> collections.abc.Callable[[], None]: """ Invokes `callback` for every KEY_UP event. For details see `hook`. """ ... -def hook_key(key: typing.Union[int, str, typing.List[typing.Union[int, str]]], callback: Callback, suppress: bool = ...) -> typing.Callable[[], None]: +def hook_key(key: typing.Union[int, str, typing.List[typing.Union[int, str]]], + callback: Callback, suppress: bool = ...) -> collections.abc.Callable[[], None]: """ Hooks key up and key down events for a single key. Returns the event handler created. To remove a hooked key use `unhook_key(key)` or @@ -322,21 +323,21 @@ def hook_key(key: typing.Union[int, str, typing.List[typing.Union[int, str]]], c ... -def on_press_key(key, callback: Callback, suppress=...) -> typing.Callable[[], None]: +def on_press_key(key, callback: Callback, suppress=...) -> collections.abc.Callable[[], None]: """ Invokes `callback` for KEY_DOWN event related to the given key. For details see `hook`. """ ... -def on_release_key(key, callback: Callback, suppress=...) -> typing.Callable[[], None]: +def on_release_key(key, callback: Callback, suppress=...) -> collections.abc.Callable[[], None]: """ Invokes `callback` for KEY_UP event related to the given key. For details see `hook`. """ ... -def unhook(remove: typing.Callable[[], None]) -> None: +def unhook(remove: collections.abc.Callable[[], None]) -> None: """ Removes a previously added hook, either by callback or by the return value of `hook`. @@ -355,7 +356,7 @@ def unhook_all() -> None: ... -def block_key(key) -> typing.Callable[[], None]: +def block_key(key) -> collections.abc.Callable[[], None]: """ Suppresses all key events of the given key, regardless of modifiers. """ @@ -365,7 +366,7 @@ def block_key(key) -> typing.Callable[[], None]: unblock_key = unhook_key -def remap_key(src, dst) -> typing.Callable[[], None]: +def remap_key(src, dst) -> collections.abc.Callable[[], None]: """ Whenever the key `src` is pressed or released, regardless of modifiers, press or release the hotkey `dst` instead. @@ -388,7 +389,8 @@ def parse_hotkey_combinations(hotkey) -> tuple[tuple[tuple[Unknown, ...], ...], _hotkeys: dict -def add_hotkey(hotkey, callback: Callback, args=..., suppress=..., timeout=..., trigger_on_release=...) -> typing.Callable[[], None]: +def add_hotkey(hotkey, callback: collections.abc.Callable, args=..., suppress=..., timeout=..., + trigger_on_release=...) -> collections.abc.Callable[[], None]: """ Invokes a callback every time a hotkey is pressed. The hotkey must be in the format `ctrl+shift+a, s`. This would trigger when the user holds @@ -453,7 +455,7 @@ def unhook_all_hotkeys() -> None: unregister_all_hotkeys = remove_all_hotkeys = clear_all_hotkeys = unhook_all_hotkeys -def remap_hotkey(src, dst, suppress=..., trigger_on_release=...) -> typing.Callable[[], None]: +def remap_hotkey(src, dst, suppress=..., trigger_on_release=...) -> collections.abc.Callable[[], None]: """ Whenever the hotkey `src` is pressed, suppress it and send `dst` instead. @@ -589,10 +591,11 @@ def get_typed_strings(events, allow_backspace=...): ... -_recording: typing.Optional[tuple[Unknown | _queue.Queue[Unknown], typing.Callable[[], None]]] +_recording: typing.Optional[tuple[Unknown | _queue.Queue[Unknown], collections.abc.Callable[[], None]]] -def start_recording(recorded_events_queue=...) -> tuple[Unknown | _queue.Queue[Unknown], typing.Callable[[], None]]: +def start_recording(recorded_events_queue=...) -> tuple[Unknown + | _queue.Queue[Unknown], collections.abc.Callable[[], None]]: """ Starts recording all keyboard events into a global variable, or the given queue if any. Returns the queue of events and the hooked function. @@ -639,7 +642,13 @@ replay = play _word_listeners: dict -def add_word_listener(word, callback: Callback, triggers=..., match_suffix=..., timeout=...) -> typing.Callable[[], None]: +def add_word_listener( + word, + callback: Callback, + triggers=..., + match_suffix=..., + timeout=...) -> collections.abc.Callable[[], + None]: """ Invokes a callback every time a sequence of characters is typed (e.g. 'pet') and followed by a trigger key (e.g. space). Modifiers (e.g. alt, ctrl, @@ -676,7 +685,8 @@ def remove_word_listener(word_or_handler) -> None: ... -def add_abbreviation(source_text, replacement_text, match_suffix=..., timeout=...) -> typing.Callable[[], None]: +def add_abbreviation(source_text, replacement_text, match_suffix=..., + timeout=...) -> collections.abc.Callable[[], None]: """ Registers a hotkey that replaces one typed text with another. For example diff --git a/typings/keyboard/_canonical_names.pyi b/typings/keyboard/_canonical_names.pyi new file mode 100644 index 00000000..94cb7a64 --- /dev/null +++ b/typings/keyboard/_canonical_names.pyi @@ -0,0 +1,11 @@ +""" +This type stub file was generated by pyright. +""" + +canonical_names = ... +sided_modifiers = ... +all_modifiers = ... + + +def normalize_name(name: str) -> str: + ...