diff --git a/src/hotkeys.py b/src/hotkeys.py index eca9a822..ab1ff145 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -1,6 +1,7 @@ import keyboard import threading + # do all of these after you click "set hotkey" but before you type the hotkey. def beforeSettingHotkey(self): self.startautosplitterButton.setEnabled(False) @@ -26,10 +27,71 @@ def afterSettingHotkey(self): self.setundosplithotkeyButton.setEnabled(True) self.setpausehotkeyButton.setEnabled(True) - return -#--------------------HOTKEYS-------------------------- -#Going to comment on one func, and others will be similar. +def is_digit(key): + try: + key_as_num = int(key) + return key_as_num >= 0 and key_as_num <= 9 + except Exception: + return False + + +def __validate_keypad(expected_key, keyboard_event): + # Prevent "num delete", "num dot" and "delete" from triggering each other + # as well as "dot" and "num dot" + if keyboard_event.scan_code == 83 or keyboard_event.scan_code == 52: + if expected_key == keyboard_event.name: + return True + else: + # TODO: "delete" won't work with "num delete" if localized in non-english + return False + # Prevent "action" from triggering "numpad" hotkeys + if is_digit(keyboard_event.name[-1]): + # Prevent "regular number" from activating "numpad" hotkeys + if expected_key.startswith("num "): + return keyboard_event.is_keypad + # Prevent "numpad" from activating "regular number" hotkeys + else: + return not keyboard_event.is_keypad + else: + # Prevent "num action" keys from triggering "regular number" and "numpad" hotkeys. + # Still allow the same key that might be localized differently on keypad vs non-keypad + return not is_digit(expected_key[-1]) + + +# 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 +# +# 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't. +# +# 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, key_name, action): + if keyboard_event.event_type == keyboard.KEY_DOWN and __validate_keypad(key_name, keyboard_event): + action() + + +def __get_key_name(keyboard_event): + return "num " + keyboard_event.name \ + if keyboard_event.is_keypad and is_digit(keyboard_event.name) \ + else keyboard_event.name + + +def __is_key_already_set(self, key_name): + return key_name == self.splitLineEdit.text() \ + or key_name == self.resetLineEdit.text() \ + or key_name == self.skipsplitLineEdit.text() \ + or key_name == self.undosplitLineEdit.text() \ + or key_name == self.pausehotkeyLineEdit.text() + + +# --------------------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 setSplitHotkey(self): self.setsplithotkeyButton.setText('Press a key..') @@ -38,48 +100,34 @@ def setSplitHotkey(self): # new thread points to callback. this thread is needed or GUI will freeze # while the program waits for user input on the hotkey - def callback(): + def callback(hotkey): # try to remove the previously set hotkey if there is one. try: - keyboard.remove_hotkey(self.split_hotkey) - except AttributeError: - pass - #this error was coming up when loading the program and - #the lineEdit area was empty (no hotkey set), then you - #set one, reload the setting once back to blank works, - #but if you click reload settings again, it errors - #we can just have it pass, but don't want to throw in - #generic exception here in case another one of these - #pops up somewhere. - except KeyError: + keyboard.unhook_key(hotkey) + # KeyError was coming up when loading the program and + # the lineEdit area was empty (no hotkey set), then you + # set one, reload the setting once back to blank works, + # but if you click reload settings again, it errors + # we can just have it pass, but don't want to throw in + # generic exception here in case another one of these + # pops up somewhere. + except (AttributeError, KeyError): pass # wait until user presses the hotkey, then keyboard module reads the input - self.split_key = keyboard.read_hotkey(False) - - # If the key the user presses is equal to itself or another hotkey already set, - # this causes issues. so here, it catches that, and will make no changes to the hotkey. + key_name = __get_key_name(keyboard.read_event(True)) try: - if self.split_key == self.splitLineEdit.text() \ - or self.split_key == self.resetLineEdit.text() \ - or self.split_key == self.skipsplitLineEdit.text() \ - or self.split_key == self.undosplitLineEdit.text() \ - or self.split_key == self.pausehotkeyLineEdit.text(): - self.split_hotkey = keyboard.add_hotkey(self.old_split_key, self.startAutoSplitter) - self.afterSettingHotkeySignal.emit() - return - except AttributeError: - self.afterSettingHotkeySignal.emit() - return + # If the key the user presses is equal to itself or another hotkey already set, + # this causes issues. so here, it catches that, and will make no changes to the hotkey. - # keyboard module allows you to hit multiple keys for a hotkey. they are joined - # together by +. If user hits two keys at the same time, make no changes to the - # hotkey. A try and except is needed if a hotkey hasn't been set yet. I'm not - # allowing for these multiple-key hotkeys because it can cause crashes, and - # not many people are going to really use or need this. - try: - if '+' in self.split_key: - self.split_hotkey = keyboard.add_hotkey(self.old_split_key, self.startAutoSplitter) + # or + + # keyboard module allows you to hit multiple keys for a hotkey. they are joined + # together by +. If user hits two keys at the same time, make no changes to the + # hotkey. A try and except is needed if a hotkey hasn't been set yet. I'm not + # allowing for these multiple-key hotkeys because it can cause crashes, and + # not many people are going to really use or need this. + if __is_key_already_set(self, key_name) or '+' in key_name: self.afterSettingHotkeySignal.emit() return except AttributeError: @@ -88,192 +136,131 @@ def callback(): # add the key as the hotkey, set the text into the LineEdit, set it as old_xxx_key, # then emite a signal to re-enable some buttons and change some text in GUI. - self.split_hotkey = keyboard.add_hotkey(self.split_key, self.startAutoSplitter) - self.splitLineEdit.setText(self.split_key) - self.old_split_key = self.split_key + + # We need to inspect the event to know if it comes from numpad because of _canonial_names. + # See: https://github.com/boppreh/keyboard/issues/161#issuecomment-386825737 + # The best way to achieve this is make our own hotkey handling on top of hook + # See: https://github.com/boppreh/keyboard/issues/216#issuecomment-431999553 + self.split_hotkey = keyboard.hook_key(key_name, lambda e: _hotkey_action(e, key_name, self.startAutoSplitter)) + self.splitLineEdit.setText(key_name) + self.split_key = key_name self.afterSettingHotkeySignal.emit() - return - t = threading.Thread(target=callback) + t = threading.Thread(target=callback, args=(self.split_hotkey,)) t.start() - return + def setResetHotkey(self): self.setresethotkeyButton.setText('Press a key..') self.beforeSettingHotkey() - def callback(): + def callback(hotkey): try: - keyboard.remove_hotkey(self.reset_hotkey) - except AttributeError: + keyboard.unhook_key(hotkey) + except (AttributeError, KeyError): pass - except KeyError: - pass - self.reset_key = keyboard.read_hotkey(False) - try: - if self.reset_key == self.splitLineEdit.text() \ - or self.reset_key == self.resetLineEdit.text() \ - or self.reset_key == self.skipsplitLineEdit.text() \ - or self.reset_key == self.undosplitLineEdit.text() \ - or self.reset_key == self.pausehotkeyLineEdit.text(): - self.reset_hotkey = keyboard.add_hotkey(self.old_reset_key, self.startReset) - self.afterSettingHotkeySignal.emit() - return - except AttributeError: - self.afterSettingHotkeySignal.emit() - return + + key_name = __get_key_name(keyboard.read_event(True)) + try: - if '+' in self.reset_key: - self.reset_hotkey = keyboard.add_hotkey(self.old_reset_key, self.startReset) + if __is_key_already_set(self, key_name) or '+' in key_name: self.afterSettingHotkeySignal.emit() return except AttributeError: self.afterSettingHotkeySignal.emit() return - self.reset_hotkey = keyboard.add_hotkey(self.reset_key, self.startReset) - self.resetLineEdit.setText(self.reset_key) - self.old_reset_key = self.reset_key + + self.reset_hotkey = keyboard.hook_key(key_name, lambda e: _hotkey_action(e, key_name, self.startReset)) + self.resetLineEdit.setText(key_name) + self.reset_key = key_name self.afterSettingHotkeySignal.emit() - return - t = threading.Thread(target=callback) + t = threading.Thread(target=callback, args=(self.reset_hotkey,)) t.start() - return + def setSkipSplitHotkey(self): self.setskipsplithotkeyButton.setText('Press a key..') self.beforeSettingHotkey() - def callback(): + def callback(hotkey): try: - keyboard.remove_hotkey(self.skip_split_hotkey) - except AttributeError: - pass - except KeyError: + keyboard.unhook_key(hotkey) + except (AttributeError, KeyError): pass - self.skip_split_key = keyboard.read_hotkey(False) + key_name = __get_key_name(keyboard.read_event(True)) try: - if self.skip_split_key == self.splitLineEdit.text() \ - or self.skip_split_key == self.resetLineEdit.text() \ - or self.skip_split_key == self.skipsplitLineEdit.text() \ - or self.skip_split_key == self.undosplitLineEdit.text() \ - or self.skip_split_key == self.pausehotkeyLineEdit.text(): - self.skip_split_hotkey = keyboard.add_hotkey(self.old_skip_split_key, self.startSkipSplit) + if __is_key_already_set(self, key_name) or '+' in key_name: self.afterSettingHotkeySignal.emit() return except AttributeError: self.afterSettingHotkeySignal.emit() return - try: - if '+' in self.skip_split_key: - self.skip_split_hotkey = keyboard.add_hotkey(self.old_skip_split_key, self.startSkipSplit) - self.afterSettingHotkeySignal.emit() - return - except AttributeError: - self.afterSettingHotkeySignal.emit() - return - - self.skip_split_hotkey = keyboard.add_hotkey(self.skip_split_key, self.startSkipSplit) - self.skipsplitLineEdit.setText(self.skip_split_key) - self.old_skip_split_key = self.skip_split_key + self.skip_split_hotkey = keyboard.hook_key(key_name, lambda e: _hotkey_action(e, key_name, self.startSkipSplit)) + self.skipsplitLineEdit.setText(key_name) + self.skip_split_key = key_name self.afterSettingHotkeySignal.emit() - return - t = threading.Thread(target=callback) + t = threading.Thread(target=callback, args=(self.skip_split_hotkey,)) t.start() - return + def setUndoSplitHotkey(self): self.setundosplithotkeyButton.setText('Press a key..') self.beforeSettingHotkey() - def callback(): + def callback(hotkey): try: - keyboard.remove_hotkey(self.undo_split_hotkey) - except AttributeError: - pass - except KeyError: + keyboard.unhook_key(hotkey) + except (AttributeError, KeyError): pass - self.undo_split_key = keyboard.read_hotkey(False) + key_name = __get_key_name(keyboard.read_event(True)) try: - if self.undo_split_key == self.splitLineEdit.text() \ - or self.undo_split_key == self.resetLineEdit.text() \ - or self.undo_split_key == self.skipsplitLineEdit.text() \ - or self.undo_split_key == self.undosplitLineEdit.text() \ - or self.undo_split_key == self.pausehotkeyLineEdit.text(): - self.undo_split_hotkey = keyboard.add_hotkey(self.old_undo_split_key, self.startUndoSplit) + if __is_key_already_set(self, key_name) or '+' in key_name: self.afterSettingHotkeySignal.emit() return except AttributeError: self.afterSettingHotkeySignal.emit() return - try: - if '+' in self.undo_split_key: - self.undo_split_hotkey = keyboard.add_hotkey(self.old_undo_split_key, self.startUndoSplit) - self.afterSettingHotkeySignal.emit() - return - except AttributeError: - self.afterSettingHotkeySignal.emit() - return - - self.undo_split_hotkey = keyboard.add_hotkey(self.undo_split_key, self.startUndoSplit) - self.undosplitLineEdit.setText(self.undo_split_key) - self.old_undo_split_key = self.undo_split_key + self.undo_split_hotkey = keyboard.hook_key(key_name, lambda e: _hotkey_action(e, key_name, self.startUndoSplit)) + self.undosplitLineEdit.setText(key_name) + self.undo_split_key = key_name self.afterSettingHotkeySignal.emit() - return - t = threading.Thread(target=callback) + t = threading.Thread(target=callback, args=(self.undo_split_hotkey,)) t.start() - return + def setPauseHotkey(self): self.setpausehotkeyButton.setText('Press a key..') self.beforeSettingHotkey() - def callback(): + def callback(hotkey): try: - keyboard.remove_hotkey(self.pause_hotkey) - except AttributeError: - pass - except KeyError: + keyboard.unhook_key(hotkey) + except (AttributeError, KeyError): pass - self.pause_key = keyboard.read_hotkey(False) - - try: - if self.pause_key == self.splitLineEdit.text() \ - or self.pause_key == self.resetLineEdit.text() \ - or self.pause_key == self.skipsplitLineEdit.text() \ - or self.pause_key == self.undosplitLineEdit.text() \ - or self.pause_key == self.pausehotkeyLineEdit.text(): - self.pause_hotkey = keyboard.add_hotkey(self.old_pause_key, self.startPause) - self.afterSettingHotkeySignal.emit() - return - except AttributeError: - self.afterSettingHotkeySignal.emit() - return + key_name = __get_key_name(keyboard.read_event(True)) try: - if '+' in self.pause_key: - self.pause_hotkey = keyboard.add_hotkey(self.old_pause_key, self.startPause) + if __is_key_already_set(self, key_name) or '+' in key_name: self.afterSettingHotkeySignal.emit() return except AttributeError: self.afterSettingHotkeySignal.emit() return - self.pause_hotkey = keyboard.add_hotkey(self.pause_key, self.startPause) - self.pausehotkeyLineEdit.setText(self.pause_key) - self.old_pause_key = self.pause_key + self.pause_hotkey = keyboard.hook_key(key_name, lambda e: _hotkey_action(e, key_name, self.startPause)) + self.pausehotkeyLineEdit.setText(key_name) + self.pause_key = key_name self.afterSettingHotkeySignal.emit() - return - t = threading.Thread(target=callback) + t = threading.Thread(target=callback, args=(self.pause_hotkey,)) t.start() - return \ No newline at end of file diff --git a/src/settings_file.py b/src/settings_file.py index 3304858c..0fa98da8 100644 --- a/src/settings_file.py +++ b/src/settings_file.py @@ -2,7 +2,10 @@ import win32gui import pickle import glob +import logging from PyQt4 import QtGui +from hotkeys import _hotkey_action + def getSaveSettingsValues(self): # get values to be able to save settings @@ -106,7 +109,14 @@ def saveSettingsAs(self): def loadSettings(self): - if self.load_settings_on_open == True: + # hotkeys need to be initialized to be passed as thread arguments in hotkeys.py + self.split_hotkey = "" + self.reset_hotkey = "" + self.skip_split_hotkey = "" + self.undo_split_hotkey = "" + self.pause_hotkey = "" + + if self.load_settings_on_open: self.settings_files = glob.glob("*.pkl") if len(self.settings_files) < 1: self.noSettingsFileOnOpenError() @@ -166,102 +176,69 @@ def loadSettings(self): self.hwnd = win32gui.FindWindow(None, self.hwnd_title) # set custom checkbox's accordingly - if self.custom_pause_times_setting == 1: - self.custompausetimesCheckBox.setChecked(True) - else: - self.custompausetimesCheckBox.setChecked(False) - - if self.custom_thresholds_setting == 1: - self.customthresholdsCheckBox.setChecked(True) - else: - self.customthresholdsCheckBox.setChecked(False) - - if self.group_dummy_splits_undo_skip_setting == 1: - self.groupDummySplitsCheckBox.setChecked(True) - else: - self.groupDummySplitsCheckBox.setChecked(False) - - if self.loop_setting == 1: - self.loopCheckBox.setChecked(True) - else: - self.loopCheckBox.setChecked(False) - - if self.auto_start_on_reset_setting == 1: - self.autostartonresetCheckBox.setChecked(True) - else: - self.autostartonresetCheckBox.setChecked(False) + self.custompausetimesCheckBox.setChecked(self.custom_pause_times_setting == 1) + self.customthresholdsCheckBox.setChecked(self.custom_thresholds_setting == 1) + self.groupDummySplitsCheckBox.setChecked(self.group_dummy_splits_undo_skip_setting == 1) + self.loopCheckBox.setChecked(self.loop_setting == 1) + self.autostartonresetCheckBox.setChecked(self.auto_start_on_reset_setting == 1) + # TODO: Reuse code from hotkeys rather than duplicating here # try to set hotkeys from when user last closed the window try: - try: - keyboard.remove_hotkey(self.split_hotkey) - except AttributeError: - pass - if self.is_auto_controlled == False: - self.splitLineEdit.setText(str(self.split_key)) - self.split_hotkey = keyboard.add_hotkey(str(self.split_key), self.startAutoSplitter) - self.old_split_key = self.split_key + keyboard.unhook_key(self.split_hotkey) # pass if the key is an empty string (hotkey was never set) - except ValueError: + except (AttributeError, KeyError): pass - except KeyError: + try: + if self.is_auto_controlled == False: + self.splitLineEdit.setText(self.split_key) + self.split_hotkey = keyboard.hook_key(str(self.split_key), lambda e: _hotkey_action(e, self.split_key, self.startAutoSplitter)) + except (ValueError, KeyError): pass try: - try: - keyboard.remove_hotkey(self.reset_hotkey) - except AttributeError: - pass - self.resetLineEdit.setText(str(self.reset_key)) - self.reset_hotkey = keyboard.add_hotkey(str(self.reset_key), self.startReset) - self.old_reset_key = self.reset_key - except ValueError: + keyboard.unhook_key(self.reset_hotkey) + except (AttributeError, KeyError): pass - except KeyError: + try: + self.resetLineEdit.setText(self.reset_key) + self.reset_hotkey = keyboard.hook_key(self.reset_key, lambda e: _hotkey_action(e, self.reset_key, self.startReset)) + except (ValueError, KeyError): pass try: - try: - keyboard.remove_hotkey(self.skip_split_hotkey) - except AttributeError: - pass - self.skipsplitLineEdit.setText(str(self.skip_split_key)) - self.skip_split_hotkey = keyboard.add_hotkey(str(self.skip_split_key), self.startSkipSplit) - self.old_skip_split_key = self.skip_split_key - except ValueError: + keyboard.unhook_key(self.skip_split_hotkey) + except (AttributeError, KeyError): pass - except KeyError: + try: + self.skipsplitLineEdit.setText(self.skip_split_key) + self.skip_split_hotkey = keyboard.hook_key(self.skip_split_key, lambda e: _hotkey_action(e, self.skip_split_key, self.startSkipSplit)) + except (ValueError, KeyError): pass try: - try: - keyboard.remove_hotkey(self.undo_split_hotkey) - except AttributeError: - pass - self.undosplitLineEdit.setText(str(self.undo_split_key)) - self.undo_split_hotkey = keyboard.add_hotkey(str(self.undo_split_key), self.startUndoSplit) - self.old_undo_split_key = self.undo_split_key - except ValueError: + keyboard.unhook_key(self.undo_split_hotkey) + except (AttributeError, KeyError): pass - except KeyError: + try: + self.undosplitLineEdit.setText(self.undo_split_key) + self.undo_split_hotkey = keyboard.hook_key(self.undo_split_key, lambda e: _hotkey_action(e, self.undo_split_key, self.startUndoSplit)) + except (ValueError, KeyError): pass try: - try: - keyboard.remove_hotkey(self.pause_hotkey) - except AttributeError: - pass - self.pausehotkeyLineEdit.setText(str(self.pause_key)) - self.pause_hotkey = keyboard.add_hotkey(str(self.pause_key), self.startPause) - self.old_pause_key = self.pause_key - except ValueError: + keyboard.unhook_key(self.pause_hotkey) + except (AttributeError, KeyError): pass - except KeyError: + try: + self.pausehotkeyLineEdit.setText(self.pause_key) + self.pause_hotkey = keyboard.hook_key(self.pause_key, lambda e: _hotkey_action(e, self.pause_key, self.startPause)) + except (ValueError, KeyError): pass self.last_successfully_loaded_settings_file_path = self.load_settings_file_path self.checkLiveImage() except Exception: + logging.error(logging.traceback.format_exc()) self.invalidSettingsError() - pass