From 8d06e773d8bd28b23e9bede01e21effdaf3f0e3a Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sat, 15 Jun 2024 21:50:16 -0400 Subject: [PATCH 1/7] Change from using set() to dict() so we retain insertion order --- UI/KeySelectDialog.py | 56 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index e38c8415..eb5c5f5d 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -60,9 +60,9 @@ def __init__(self, button): desc = f"Press the key you want bound to {self.Desc}\n(Right-click a key button to clear it.)" # is this ugly? - self.ModSlot = set() + self.ModSlot = {} self.KeySlot = None - self.PressedKeys = set() + self.PressedKeys = {} self.SetKeymap(); sizer = wx.BoxSizer(wx.VERTICAL); @@ -135,9 +135,9 @@ def handleJS(self, event): for button in range(0, self.controller.GetNumberButtons()): button_keyname = "JOY" + str(button+1) if self.controller.GetButtonState(button): - self.PressedKeys.add(self.Keymap[button_keyname]) + self.PressedKeys[self.Keymap[button_keyname]] = None else: - self.PressedKeys.discard(self.Keymap[button_keyname]) + self.PressedKeys.pop(self.Keymap[button_keyname], None) self.controller.SetCurrentAxisPercents() # don't let wee jiggles at the center trigger this. @@ -149,11 +149,11 @@ def handleJS(self, event): # this means they have to hold the axis while clicking "OK" which # is less than great but we can't detect intent in software. for axis_code in self.controller.GetAllAxisCodes(): - self.PressedKeys.discard(self.Keymap[axis_code]) + self.PressedKeys.pop(self.Keymap[axis_code], None) current_axis = self.controller.GetCurrentAxis() if current_axis: - self.PressedKeys.add(self.Keymap[current_axis]) + self.PressedKeys[self.Keymap[current_axis]] = None else: # Unknown joystick event. These fire quite a bit. @@ -173,10 +173,10 @@ def handleCharHook(self, event): rkc = event.GetRawKeyCode() if SeparateLR and rkc in modRawKeyCodes: - self.PressedKeys.add(modRawKeyCodes[rkc]) + self.PressedKeys[modRawKeyCodes[rkc]] = None else: self.clearMouseKeys() - self.PressedKeys.add(self.Keymap[event.GetKeyCode()]) + self.PressedKeys[self.Keymap[event.GetKeyCode()]] = None self.buildBind() @@ -186,22 +186,22 @@ def handleKeyUp(self, event): rkc = event.GetRawKeyCode() if SeparateLR and rkc in modRawKeyCodes: - self.PressedKeys.discard(modRawKeyCodes[rkc]) + self.PressedKeys.pop(modRawKeyCodes[rkc], None) else: - self.PressedKeys.discard(self.Keymap[event.GetKeyCode()]) + self.PressedKeys.pop(self.Keymap[event.GetKeyCode()], None) self.buildBind() def handleMouse(self, event): self.clearMouseKeys() if event.GetEventType() == wx.wxEVT_MOUSEWHEEL: - self.PressedKeys.add("MOUSEWHEEL") + self.PressedKeys["MOUSEWHEEL"] = None elif (event.LeftIsDown() and event.RightIsDown()): - self.PressedKeys.add("MOUSECHORD") + self.PressedKeys["MOUSECHORD"] = None elif (event.ButtonDClick()): - self.PressedKeys.add("DCLICK" + str(event.GetButton())) + self.PressedKeys["DCLICK" + str(event.GetButton())] = None else: - self.PressedKeys.add("BUTTON" + str(event.GetButton())) + self.PressedKeys["BUTTON" + str(event.GetButton())] = None self.buildBind() @@ -209,9 +209,9 @@ def clearMouseKeys(self): pressedkeys = list(self.PressedKeys) for key in pressedkeys: if re.match("DCLICK", key) or re.match("BUTTON", key): - self.PressedKeys.discard(key) - self.PressedKeys.discard("MOUSEWHEEL") - self.PressedKeys.discard("MOUSECHORD") + self.PressedKeys.pop(key, None) + self.PressedKeys.pop("MOUSEWHEEL", None) + self.PressedKeys.pop("MOUSECHORD", None) def buildBind(self): @@ -220,13 +220,13 @@ def buildBind(self): # If we're completely off the keyboard/etc, clear our current state, # but return so we don't update the binding to nothing if not self.PressedKeys: - self.ModSlot = set() + self.ModSlot = {} self.KeySlot = None return for key in self.PressedKeys: if key in self.modKeys: - self.ModSlot.add(key) + self.ModSlot[key] = None else: self.KeySlot = key @@ -234,24 +234,24 @@ def buildBind(self): # Clear up that situation if ("SHIFT" in self.ModSlot and ("LSHIFT" in self.ModSlot or "RSHIFT" in self.ModSlot)): if SeparateLR: - self.ModSlot.discard("SHIFT") + self.ModSlot.pop("SHIFT", None) else: - self.ModSlot.discard("LSHIFT") - self.ModSlot.discard("RSHIFT") + self.ModSlot.pop("LSHIFT", None) + self.ModSlot.pop("RSHIFT", None) if ("CTRL" in self.ModSlot and ("LCTRL" in self.ModSlot or "RCTRL" in self.ModSlot)): if SeparateLR: - self.ModSlot.discard("CTRL") + self.ModSlot.pop("CTRL", None) else: - self.ModSlot.discard("LCTRL") - self.ModSlot.discard("RCTRL") + self.ModSlot.pop("LCTRL", None) + self.ModSlot.pop("RCTRL", None) if ("ALT" in self.ModSlot and ("LALT" in self.ModSlot or "RALT" in self.ModSlot)): if SeparateLR: - self.ModSlot.discard("ALT") + self.ModSlot.pop("ALT", None) else: - self.ModSlot.discard("LALT") - self.ModSlot.discard("RALT") + self.ModSlot.pop("LALT", None) + self.ModSlot.pop("RALT", None) # Finally, build the bind string from what we have left over totalModKeys = "+".join([ key for key in sorted(self.ModSlot, reverse=True) if key]) From 979e79d58040e5953f29f2714d7d5082bd10d641 Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 01:08:56 -0400 Subject: [PATCH 2/7] New logic for keybinder dialog. Works, has had zero controller testing --- Help/Manual.html | 5 +- UI/KeySelectDialog.py | 161 ++++++++++++++++++++++-------------------- UI/PrefsDialog.py | 2 +- 3 files changed, 88 insertions(+), 80 deletions(-) diff --git a/Help/Manual.html b/Help/Manual.html index c33612d8..61dcf4ee 100644 --- a/Help/Manual.html +++ b/Help/Manual.html @@ -52,8 +52,9 @@

Preferences

Various of the keybind systems generated by BindControl, most especially the Speed on Demand binds, load other bindfiles with every keystroke. Depending on CPU usage and server lag, it's theoretically possible to get binds stuck in a strange state, leading to behavior like powers not turning off or movement continuing after a key is released. This happens less in the 2020s than it did when the game was new, but just in case, the Binds Reset Key will reset all BindControl binds to their initially-loaded state and stop all movement.

Bind left and right modifier keys separately

-

While typically keybinds with modifier keys are of the form SHIFT+R, for instance, City of Heroes supports separate binds for the left and right modifier keys, ie, making LSHIFT+R a different keybind than RSHIFT+R. This historically can work a little strangely, or not at all, inside City of Heroes, so the feature is considered experimental and is off by default.

- +

City of Heroes supports separate binds for the left and right modifier keys (SHIFT, ALT, and CTRL) when they are used alone or in the "right-hand" position with another modifier key, eg, just "LSHIFT" or "ALT+RCTRL."

+

Binding "LCTRL+R" does not work, and doing so manually in-game will simply bind "CTRL+R" -- this is how the game is implemented and is out of BindControl's influence. The left versus right modifier keys are only treated separately on the "right-hand" side of a bind.

+

This is a fiddly scheme but BindControl attempts to support it, so this preference is here, and sets up the Key Binder dialog to honor left versus right modifier keys in the "right-hand" position of a keybind.

It can also be toggled on or off when binding a key, if you want to mix and match your side-specific and side-agnostic keybinds. This is not super recommended, but you do you.

Set all binds to default before reapplying

diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index eb5c5f5d..b83e6577 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -45,6 +45,7 @@ def __init__(self, button): self.Binding = button.Key self.modKeys: List[str] = [] # gets set every ShowModal() call + self.dualKeys = ['SHIFT','CTRL','ALT'] # keys which might be mod and might be trigger wx.Dialog.__init__(self, button.Parent, -1, self.Desc, style = wx.WANTS_CHARS|wx.DEFAULT_DIALOG_STYLE) @@ -60,9 +61,9 @@ def __init__(self, button): desc = f"Press the key you want bound to {self.Desc}\n(Right-click a key button to clear it.)" # is this ugly? - self.ModSlot = {} + self.ModSlot = None self.KeySlot = None - self.PressedKeys = {} + self.PressedKeys = dict() self.SetKeymap(); sizer = wx.BoxSizer(wx.VERTICAL); @@ -129,31 +130,27 @@ def ShowBind(self): self.kbBind.SetPage('
' + self.Binding + '
') def handleJS(self, event): - # knock wood, this seems to be working fairly well now. - if event.ButtonDown() or event.IsMove() or event.IsZMove(): - # iterate joystick buttons and add those that are pressed - for button in range(0, self.controller.GetNumberButtons()): - button_keyname = "JOY" + str(button+1) - if self.controller.GetButtonState(button): - self.PressedKeys[self.Keymap[button_keyname]] = None - else: - self.PressedKeys.pop(self.Keymap[button_keyname], None) + if event.ButtonDown(): + button_keyname = "JOY" + str(event.GetButtonChange()+1) + if button_keyname in self.modKeys: + self.ModSlot = self.Keymap[button_keyname] + else: + self.KeySlot = self.Keymap[button_keyname] + elif event.IsMove() or event.IsZMove(): self.controller.SetCurrentAxisPercents() # don't let wee jiggles at the center trigger this. # this is "no axis is > 50% in some direction" and "POV is centered" if self.controller.StickIsNearCenter() and self.controller.GetPOVPosition() > 60000: return - # iterate axes and discard() all; then add the current one - # this means they have to hold the axis while clicking "OK" which - # is less than great but we can't detect intent in software. - for axis_code in self.controller.GetAllAxisCodes(): - self.PressedKeys.pop(self.Keymap[axis_code], None) - current_axis = self.controller.GetCurrentAxis() if current_axis: - self.PressedKeys[self.Keymap[current_axis]] = None + payload = self.Keymap[current_axis] + if payload in self.modKeys: + self.ModSlot = payload + else: + self.KeySlot = payload else: # Unknown joystick event. These fire quite a bit. @@ -171,15 +168,65 @@ def handleCharHook(self, event): self.EndModal(wx.CANCEL) return - rkc = event.GetRawKeyCode() - if SeparateLR and rkc in modRawKeyCodes: - self.PressedKeys[modRawKeyCodes[rkc]] = None + payload = self.Keymap[event.GetKeyCode()] + + if not self.PressedKeys: + # If we have no pressed keys, ie, we're starting over, clear state and start + rkc = event.GetRawKeyCode() + if SeparateLR and rkc in modRawKeyCodes: + payload = modRawKeyCodes[rkc] + self.KeySlot = payload + self.ModSlot = '' else: - self.clearMouseKeys() - self.PressedKeys[self.Keymap[event.GetKeyCode()]] = None + # we have something held down + if payload in self.dualKeys: + # we're handling one of the magical "dual keys" + # this is tangly. We want to put dual keys in the Right Place, + # which is hard to DWIM algorithmically. It's easy to get something + # stuck in ModSlot and never pop it out. + if self.ModSlot: + # if we have a ModKey already... + # if we are not the existing ModSlot, use us as the target key. + # ie, we already had "SHIFT" and we hit "CTRL" + if self.ModSlot != payload: + rkc = event.GetRawKeyCode() + if SeparateLR and rkc in modRawKeyCodes: + payload = modRawKeyCodes[rkc] + self.KeySlot = payload + # else we have a modslot and we are already it, we're done + else: + # we don't have a modkey, but we have something held down, + # so it must be in keyslot. + if self.KeySlot in self.dualKeys: + # If it's a dualkey, let's bump it to the modslot + # and use us as the keyslot. + self.ModSlot = self.NormalizeDualKeys(self.KeySlot) + self.KeySlot = payload + else: + # it's not a dualkey, so let's be its mod + self.ModSlot = payload + + # else this is a "normal" key and we can just DTRT + else: + # if we have LSHIFT etc in the keyslot, move it to the modslot. + # This is probably going to be the path most taken, as people will hit + # "SHIFT" then "R" or whatever, and we'll initially assign the SHIFT + # to the KeySlot + if self.KeySlot in self.modKeys: + self.ModSlot = self.NormalizeDualKeys(str(self.KeySlot)) + + self.KeySlot = payload + + self.PressedKeys[payload] = None self.buildBind() + def NormalizeDualKeys(self, name): + return { 'LSHIFT' : 'SHIFT' , 'RSHIFT' : 'SHIFT' , 'SHIFT' : 'SHIFT' , + 'LCTRL' : 'CTRL' , 'RCTRL' : 'CTRL' , 'CTRL' : 'CTRL' , + 'LALT' : 'ALT' , 'RALT' : 'ALT' , 'ALT' : 'ALT' , + }[name] + def handleKeyUp(self, event): # Key-up: clear keys that were released SeparateLR = self.SeparateLRChooser.Value @@ -193,69 +240,29 @@ def handleKeyUp(self, event): self.buildBind() def handleMouse(self, event): - self.clearMouseKeys() + # if nothing's pressed, clear everything + if not self.PressedKeys: + self.ModSlot = '' + self.KeySlot = '' + + # if we have just a modkey, move it over + if self.KeySlot in self.modKeys: + self.ModSlot = self.NormalizeDualKeys(self.KeySlot) + if event.GetEventType() == wx.wxEVT_MOUSEWHEEL: - self.PressedKeys["MOUSEWHEEL"] = None + self.KeySlot = "MOUSEWHEEL" elif (event.LeftIsDown() and event.RightIsDown()): - self.PressedKeys["MOUSECHORD"] = None + self.KeySlot = "MOUSECHORD" elif (event.ButtonDClick()): - self.PressedKeys["DCLICK" + str(event.GetButton())] = None + self.KeySlot = "DCLICK" + str(event.GetButton()) else: - self.PressedKeys["BUTTON" + str(event.GetButton())] = None + self.KeySlot = "BUTTON" + str(event.GetButton()) self.buildBind() - def clearMouseKeys(self): - pressedkeys = list(self.PressedKeys) - for key in pressedkeys: - if re.match("DCLICK", key) or re.match("BUTTON", key): - self.PressedKeys.pop(key, None) - self.PressedKeys.pop("MOUSEWHEEL", None) - self.PressedKeys.pop("MOUSECHORD", None) - - def buildBind(self): - SeparateLR = self.SeparateLRChooser.Value - - # If we're completely off the keyboard/etc, clear our current state, - # but return so we don't update the binding to nothing - if not self.PressedKeys: - self.ModSlot = {} - self.KeySlot = None - return - - for key in self.PressedKeys: - if key in self.modKeys: - self.ModSlot[key] = None - else: - self.KeySlot = key - - # If we switched SeparateLR midstream, we might get LSHIFT and SHIFT both in there. - # Clear up that situation - if ("SHIFT" in self.ModSlot and ("LSHIFT" in self.ModSlot or "RSHIFT" in self.ModSlot)): - if SeparateLR: - self.ModSlot.pop("SHIFT", None) - else: - self.ModSlot.pop("LSHIFT", None) - self.ModSlot.pop("RSHIFT", None) - - if ("CTRL" in self.ModSlot and ("LCTRL" in self.ModSlot or "RCTRL" in self.ModSlot)): - if SeparateLR: - self.ModSlot.pop("CTRL", None) - else: - self.ModSlot.pop("LCTRL", None) - self.ModSlot.pop("RCTRL", None) - - if ("ALT" in self.ModSlot and ("LALT" in self.ModSlot or "RALT" in self.ModSlot)): - if SeparateLR: - self.ModSlot.pop("ALT", None) - else: - self.ModSlot.pop("LALT", None) - self.ModSlot.pop("RALT", None) - # Finally, build the bind string from what we have left over - totalModKeys = "+".join([ key for key in sorted(self.ModSlot, reverse=True) if key]) - self.Binding = "+".join([ key for key in [totalModKeys, self.KeySlot] if key]) + self.Binding = "+".join([ key for key in [self.ModSlot, self.KeySlot] if key]) self.ShowBind() diff --git a/UI/PrefsDialog.py b/UI/PrefsDialog.py index 748a614e..3f6fc3d5 100644 --- a/UI/PrefsDialog.py +++ b/UI/PrefsDialog.py @@ -47,7 +47,7 @@ def __init__(self, parent): generalSizer.Add( splitKeyLabel, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT, 6 ) self.UseSplitModKeys = wx.CheckBox(generalPanel) self.UseSplitModKeys.SetValue(config.ReadBool('UseSplitModKeys')) - self.UseSplitModKeys.SetToolTip("By default, BindControl will bind modifier keys, eg CTRL and SHIFT, without regard to which side of the keyboard they're on. Check this if you'd like to bind the left-side modifier keys separately from the right-side ones.") + self.UseSplitModKeys.SetToolTip("This allows the left and right modifier keys to be bound separately if on the \"right-hand\" side of a bind. Check the Manual for more information.") generalSizer.Add( self.UseSplitModKeys, 1, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 6 ) setattr(splitKeyLabel, 'CB', self.UseSplitModKeys) From c6f9c1bb5d6bad1645e2e7a75002cb5d25d0d35a Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 01:23:39 -0400 Subject: [PATCH 3/7] Stop having invalid SHIFT+CTRL+ binds as defaults --- Page/InspirationPopper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Page/InspirationPopper.py b/Page/InspirationPopper.py index 136b5e8f..9c52754d 100644 --- a/Page/InspirationPopper.py +++ b/Page/InspirationPopper.py @@ -44,14 +44,14 @@ def __init__(self, parent): 'SingleBreakFreeKey' : "SHIFT+E", 'SingleResistDamageKey' : "SHIFT+SPACE", 'SingleResurrectionKey' : "SHIFT+TILDE", - 'SingleRevAccuracyKey' : "SHIFT+CTRL+A", - 'SingleRevHealthKey' : "SHIFT+CTRL+S", - 'SingleRevDamageKey' : "SHIFT+CTRL+D", - 'SingleRevEnduranceKey' : "SHIFT+CTRL+Q", - 'SingleRevDefenseKey' : "SHIFT+CTRL+W", - 'SingleRevBreakFreeKey' : "SHIFT+CTRL+E", - 'SingleRevResistDamageKey' : "SHIFT+CTRL+SPACE", - 'SingleRevResurrectionKey' : "SHIFT+CTRL+TILDE", + 'SingleRevAccuracyKey' : "", + 'SingleRevHealthKey' : "", + 'SingleRevDamageKey' : "", + 'SingleRevEnduranceKey' : "", + 'SingleRevDefenseKey' : "", + 'SingleRevBreakFreeKey' : "", + 'SingleRevResistDamageKey' : "", + 'SingleRevResurrectionKey' : "", }) def BuildPage(self): From 78d1d6abff6f5b4feeffe91dfd58092822e98b9d Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 01:59:44 -0400 Subject: [PATCH 4/7] Controller mostly working now. Have to keep your mod pressed, like LTrigger or whatever, or it clears the dialog. This might just be the way of things for now. --- UI/KeySelectDialog.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index b83e6577..440d6a54 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -130,12 +130,21 @@ def ShowBind(self): self.kbBind.SetPage('
' + self.Binding + '
') def handleJS(self, event): + if not self.PressedKeys: + self.ModSlot = '' + self.KeySlot = '' + if event.ButtonDown(): - button_keyname = "JOY" + str(event.GetButtonChange()+1) + button_keyname = self.Keymap["JOY" + str(event.GetButtonOrdinal()+1)] + self.PressedKeys[button_keyname] = None if button_keyname in self.modKeys: - self.ModSlot = self.Keymap[button_keyname] + self.ModSlot = button_keyname else: - self.KeySlot = self.Keymap[button_keyname] + self.KeySlot = button_keyname + + elif event.ButtonUp(): + button_keyname = self.Keymap["JOY" + str(event.GetButtonOrdinal()+1)] + self.PressedKeys.pop(button_keyname, '') elif event.IsMove() or event.IsZMove(): self.controller.SetCurrentAxisPercents() @@ -144,9 +153,18 @@ def handleJS(self, event): if self.controller.StickIsNearCenter() and self.controller.GetPOVPosition() > 60000: return + # clear all axes from PressedKeys + for axis in [ + "JOYSTICK1_UP" , "JOYSTICK1_DOWN" , "JOYSTICK1_LEFT" , "JOYSTICK1_RIGHT" , + "RTrigger" , "LTrigger" , + "JOYSTICK3_UP" , "JOYSTICK3_DOWN" , "JOYSTICK3_LEFT" , "JOYSTICK3_RIGHT" , + "JOYPAD_UP" , "JOYPAD_DOWN" , "JOYPAD_LEFT" , "JOYPAD_RIGHT" , + ]:self.PressedKeys.pop(axis, '') + current_axis = self.controller.GetCurrentAxis() if current_axis: payload = self.Keymap[current_axis] + self.PressedKeys[payload] = None if payload in self.modKeys: self.ModSlot = payload else: From 68640fbea8b4e8d0fe5cdc1c7a1f483bec8a744b Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 13:29:28 -0400 Subject: [PATCH 5/7] Polish up controller behavior with new KeySelect dialog logic --- UI/KeySelectDialog.py | 86 +++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index 440d6a54..aef2277c 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -44,6 +44,20 @@ def __init__(self, button): self.Button = button self.Binding = button.Key + # prepopulate ModSlot and KeySlot so ShowBind doesn't clear everything + # on the first event it sees, usually a spurious joystick event + bindsplit = re.split(r'\+', self.Binding) + if len(bindsplit) == 2: + self.ModSlot = bindsplit[0] + self.KeySlot = bindsplit[1] + else: + self.ModSlot = '' + self.KeySlot = bindsplit[0] + + self.PressedKeys = set() + + self.SetKeymap(); + self.modKeys: List[str] = [] # gets set every ShowModal() call self.dualKeys = ['SHIFT','CTRL','ALT'] # keys which might be mod and might be trigger @@ -61,15 +75,10 @@ def __init__(self, button): desc = f"Press the key you want bound to {self.Desc}\n(Right-click a key button to clear it.)" # is this ugly? - self.ModSlot = None - self.KeySlot = None - self.PressedKeys = dict() - self.SetKeymap(); - sizer = wx.BoxSizer(wx.VERTICAL); self.kbDesc = wx.StaticText ( self, -1, desc, style = wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) - self.kbBind = wx.html.HtmlWindow( self, -1, size=(360,60), style=wx.html.HW_SCROLLBAR_NEVER) + self.kbBind = wx.html.HtmlWindow( self, -1, size=(450,60), style=wx.html.HW_SCROLLBAR_NEVER) self.kbBind.SetHTMLBackgroundColour( wx.WHITE ) self.kbErr = wx.StaticText ( self, -1, " ", style = wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) @@ -130,13 +139,20 @@ def ShowBind(self): self.kbBind.SetPage('
' + self.Binding + '
') def handleJS(self, event): - if not self.PressedKeys: - self.ModSlot = '' - self.KeySlot = '' + # we don't want to do this clearing logic with unknown events, + # hence this tangly "if" + if ( + (not event.ButtonDown()) and (not event.ButtonUp()) and + (not event.IsMove()) and (not event.IsZMove()) and + (not self.controller.GetCurrentAxis()) + ): + if not self.PressedKeys: + self.ModSlot = '' + self.KeySlot = '' if event.ButtonDown(): button_keyname = self.Keymap["JOY" + str(event.GetButtonOrdinal()+1)] - self.PressedKeys[button_keyname] = None + self.PressedKeys.add(button_keyname) if button_keyname in self.modKeys: self.ModSlot = button_keyname else: @@ -144,7 +160,7 @@ def handleJS(self, event): elif event.ButtonUp(): button_keyname = self.Keymap["JOY" + str(event.GetButtonOrdinal()+1)] - self.PressedKeys.pop(button_keyname, '') + self.PressedKeys.discard(button_keyname) elif event.IsMove() or event.IsZMove(): self.controller.SetCurrentAxisPercents() @@ -159,12 +175,12 @@ def handleJS(self, event): "RTrigger" , "LTrigger" , "JOYSTICK3_UP" , "JOYSTICK3_DOWN" , "JOYSTICK3_LEFT" , "JOYSTICK3_RIGHT" , "JOYPAD_UP" , "JOYPAD_DOWN" , "JOYPAD_LEFT" , "JOYPAD_RIGHT" , - ]:self.PressedKeys.pop(axis, '') + ]:self.PressedKeys.discard(axis) current_axis = self.controller.GetCurrentAxis() if current_axis: payload = self.Keymap[current_axis] - self.PressedKeys[payload] = None + self.PressedKeys.add(payload) if payload in self.modKeys: self.ModSlot = payload else: @@ -178,7 +194,6 @@ def handleJS(self, event): def handleCharHook(self, event): # Key down - SeparateLR = self.SeparateLRChooser.Value # close the dialog on ESCAPE if (isinstance(event, wx.KeyEvent)): @@ -186,39 +201,40 @@ def handleCharHook(self, event): self.EndModal(wx.CANCEL) return + # fish out the payload name -- upgrade "SHIFT" to "LSHIFT" etc if appropriate + SeparateLR = self.SeparateLRChooser.Value payload = self.Keymap[event.GetKeyCode()] + rkc = event.GetRawKeyCode() + if SeparateLR and rkc in modRawKeyCodes: + payload = modRawKeyCodes[rkc] if not self.PressedKeys: # If we have no pressed keys, ie, we're starting over, clear state and start - rkc = event.GetRawKeyCode() - if SeparateLR and rkc in modRawKeyCodes: - payload = modRawKeyCodes[rkc] self.KeySlot = payload self.ModSlot = '' else: # we have something held down if payload in self.dualKeys: # we're handling one of the magical "dual keys" - # this is tangly. We want to put dual keys in the Right Place, - # which is hard to DWIM algorithmically. It's easy to get something - # stuck in ModSlot and never pop it out. if self.ModSlot: # if we have a ModKey already... - # if we are not the existing ModSlot, use us as the target key. - # ie, we already had "SHIFT" and we hit "CTRL" - if self.ModSlot != payload: - rkc = event.GetRawKeyCode() - if SeparateLR and rkc in modRawKeyCodes: - payload = modRawKeyCodes[rkc] + if self.KeySlot in self.dualKeys: + # If we have a dualkey in the keyslot, bump it to the ModSlot + # and use us as the keyslot. + self.ModSlot = self.NormalizeDualKeyName(self.KeySlot) + self.KeySlot = payload + elif self.ModSlot != self.NormalizeDualKeyName(payload): + # if we are not the existing ModSlot, use us as the target key. + # ie, we already had "SHIFT" and we hit "CTRL" self.KeySlot = payload # else we have a modslot and we are already it, we're done else: # we don't have a modkey, but we have something held down, # so it must be in keyslot. if self.KeySlot in self.dualKeys: - # If it's a dualkey, let's bump it to the modslot - # and use us as the keyslot. - self.ModSlot = self.NormalizeDualKeys(self.KeySlot) + # If it's a dualkey in KeySlot, let's bump it to ModSlot + # and use this as KeySlot. + self.ModSlot = self.NormalizeDualKeyName(self.KeySlot) self.KeySlot = payload else: # it's not a dualkey, so let's be its mod @@ -231,15 +247,15 @@ def handleCharHook(self, event): # "SHIFT" then "R" or whatever, and we'll initially assign the SHIFT # to the KeySlot if self.KeySlot in self.modKeys: - self.ModSlot = self.NormalizeDualKeys(str(self.KeySlot)) + self.ModSlot = self.NormalizeDualKeyName(self.KeySlot) self.KeySlot = payload - self.PressedKeys[payload] = None + self.PressedKeys.add(payload) self.buildBind() - def NormalizeDualKeys(self, name): + def NormalizeDualKeyName(self, name): return { 'LSHIFT' : 'SHIFT' , 'RSHIFT' : 'SHIFT' , 'SHIFT' : 'SHIFT' , 'LCTRL' : 'CTRL' , 'RCTRL' : 'CTRL' , 'CTRL' : 'CTRL' , 'LALT' : 'ALT' , 'RALT' : 'ALT' , 'ALT' : 'ALT' , @@ -251,9 +267,9 @@ def handleKeyUp(self, event): rkc = event.GetRawKeyCode() if SeparateLR and rkc in modRawKeyCodes: - self.PressedKeys.pop(modRawKeyCodes[rkc], None) + self.PressedKeys.discard(modRawKeyCodes[rkc]) else: - self.PressedKeys.pop(self.Keymap[event.GetKeyCode()], None) + self.PressedKeys.discard(self.Keymap[event.GetKeyCode()]) self.buildBind() @@ -265,7 +281,7 @@ def handleMouse(self, event): # if we have just a modkey, move it over if self.KeySlot in self.modKeys: - self.ModSlot = self.NormalizeDualKeys(self.KeySlot) + self.ModSlot = self.NormalizeDualKeyName(self.KeySlot) if event.GetEventType() == wx.wxEVT_MOUSEWHEEL: self.KeySlot = "MOUSEWHEEL" From 9bb11468ef3279852c3935dfe12aa88d2e909ee8 Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 14:20:00 -0400 Subject: [PATCH 6/7] Fall back to old RawKeyFlag scheme for left vs right. --- UI/KeySelectDialog.py | 84 +++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index aef2277c..f3737837 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -13,25 +13,26 @@ from bcController import bcController KeyChanged, EVT_KEY_CHANGED = wx.lib.newevent.NewEvent() -# Platform-specific keycodes for telling left from right -modRawKeyCodes = {} + +# Platform-specific keyevent flags for telling left from right +modKeyFlags = {} if platform.system() == 'Windows': - modRawKeyCodes = { - 160: 'LSHIFT', 161: 'RSHIFT', - 162: 'LCTRL' , 163: 'RCTRL' , - 164: 'LAlT' , 165: 'RALT' , + modKeyFlags = { + 'RSHIFT': 0x40000, + 'RCTRL' : 0x1000000, + 'RALT' : 0x1000000, } elif platform.system() == 'Linux': - modRawKeyCodes = { - 65505: 'LSHIFT', 65506: 'RSHIFT', - 65507: 'LCTRL' , 65508: 'RCTRL' , - 65513: 'LALT' , 65514: 'RALT' , + modKeyFlags = { + 'RSHIFT': 0x08, + 'LCTRL' : 0x04, + 'RALT' : 0x08, } elif platform.system() == 'Darwin': - modRawKeyCodes = { - 56: 'LSHIFT', 60: 'RSHIFT', - 59: 'LCTRL' , 62: 'RCTRL' , - 58: 'LALT' , 61: 'RALT' , + modKeyFlags = { + 'LSHIFT': 0x02, + 'LCTRL' : 0x2000, + 'LALT' : 0x20, } class KeySelectDialog(wx.Dialog): @@ -40,7 +41,7 @@ def __init__(self, button): if button.CtlLabel: self.Desc = button.CtlLabel.GetLabel() else: - self.Desc = UI.Labels.get(button.CtlName, 'this button') + self.Desc = UI.Labels.get(button.CtlName, 'this keybind') self.Button = button self.Binding = button.Key @@ -89,7 +90,7 @@ def __init__(self, button): sizer.Add( self.kbErr , 1, wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5); sizer.AddSpacer(15) - if(modRawKeyCodes): + if(modKeyFlags): self.SeparateLRChooser = wx.CheckBox( self, -1, "Bind left/right mod keys separately") self.SeparateLRChooser.SetToolTip("This allows you to bind specifically left or right side mod keys for this bind. This will not change the global preference.") sizer.Add( self.SeparateLRChooser, 0, wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) @@ -201,12 +202,7 @@ def handleCharHook(self, event): self.EndModal(wx.CANCEL) return - # fish out the payload name -- upgrade "SHIFT" to "LSHIFT" etc if appropriate - SeparateLR = self.SeparateLRChooser.Value - payload = self.Keymap[event.GetKeyCode()] - rkc = event.GetRawKeyCode() - if SeparateLR and rkc in modRawKeyCodes: - payload = modRawKeyCodes[rkc] + payload = self.GetEventPayload(event) if not self.PressedKeys: # If we have no pressed keys, ie, we're starting over, clear state and start @@ -217,6 +213,9 @@ def handleCharHook(self, event): if payload in self.dualKeys: # we're handling one of the magical "dual keys" if self.ModSlot: + # TODO -- this logic isn't right and we still get "ALT+ALT" if + # we're holding down SHIFT+ALT and hit "ALT" again. + # if we have a ModKey already... if self.KeySlot in self.dualKeys: # If we have a dualkey in the keyslot, bump it to the ModSlot @@ -255,6 +254,36 @@ def handleCharHook(self, event): self.buildBind() + def GetEventPayload(self, event): + # fish out the payload name -- upgrade "SHIFT" to "LSHIFT" etc if appropriate + SeparateLR = self.SeparateLRChooser.Value + payload = self.Keymap[event.GetKeyCode()] + + if SeparateLR and modKeyFlags: + rawFlags = event.GetRawKeyFlags() + system = platform.system() + if payload == "SHIFT": + if system == 'Darwin': + payload = "LSHIFT" if (rawFlags & modKeyFlags['LSHIFT']) else "RSHIFT" + else: + payload = "RSHIFT" if (rawFlags & modKeyFlags['RSHIFT']) else "LSHIFT" + + if payload == "CTRL": + if system == 'Darwin': + payload = "LCTRL" if (rawFlags & modKeyFlags['LCTRL']) else "RCTRL" + elif system == 'Linux': + payload = "LCTRL" if (rawFlags & modKeyFlags['LCTRL']) else "RCTRL" + else: + payload = "RCTRL" if (rawFlags & modKeyFlags['RCTRL']) else "LCTRL" + + if payload == "ALT": + if system == 'Darwin': + payload = "LALT" if (rawFlags & modKeyFlags['LALT']) else "RALT" + else: + payload = "RALT" if (rawFlags & modKeyFlags['RALT']) else "LALT" + + return payload + def NormalizeDualKeyName(self, name): return { 'LSHIFT' : 'SHIFT' , 'RSHIFT' : 'SHIFT' , 'SHIFT' : 'SHIFT' , 'LCTRL' : 'CTRL' , 'RCTRL' : 'CTRL' , 'CTRL' : 'CTRL' , @@ -262,15 +291,7 @@ def NormalizeDualKeyName(self, name): }[name] def handleKeyUp(self, event): - # Key-up: clear keys that were released - SeparateLR = self.SeparateLRChooser.Value - rkc = event.GetRawKeyCode() - - if SeparateLR and rkc in modRawKeyCodes: - self.PressedKeys.discard(modRawKeyCodes[rkc]) - else: - self.PressedKeys.discard(self.Keymap[event.GetKeyCode()]) - + self.PressedKeys.discard(self.GetEventPayload(event)) self.buildBind() def handleMouse(self, event): @@ -330,6 +351,7 @@ def SetKeymap(self): wx.WXK_SHIFT: 'SHIFT', wx.WXK_ALT: 'ALT', wx.WXK_CONTROL: 'CTRL', # TODO - wx.WXK_RAW_CONTROL for Mac, instead? + wx.WXK_WINDOWS_LEFT: '', # TODO - is this OK? keeps it from blowing up but... wx.WXK_SPACE : 'SPACE', wx.WXK_UP : 'UP', wx.WXK_DOWN : 'DOWN', From 3f35ea185e9e92f77d2f4eaceae4f444cbe04610 Mon Sep 17 00:00:00 2001 From: Russell Pickett Date: Sun, 16 Jun 2024 14:31:26 -0400 Subject: [PATCH 7/7] A little more fiddling to alleviate "SHIFT+SHIFT" happening --- UI/KeySelectDialog.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/UI/KeySelectDialog.py b/UI/KeySelectDialog.py index f3737837..7f80b01d 100644 --- a/UI/KeySelectDialog.py +++ b/UI/KeySelectDialog.py @@ -213,13 +213,16 @@ def handleCharHook(self, event): if payload in self.dualKeys: # we're handling one of the magical "dual keys" if self.ModSlot: - # TODO -- this logic isn't right and we still get "ALT+ALT" if - # we're holding down SHIFT+ALT and hit "ALT" again. - - # if we have a ModKey already... - if self.KeySlot in self.dualKeys: - # If we have a dualkey in the keyslot, bump it to the ModSlot - # and use us as the keyslot. + # if we have a ModKey already + if (self.KeySlot in self.dualKeys and + (self.NormalizeDualKeyName(self.KeySlot) != self.NormalizeDualKeyName(payload)) + ): + # If we have a dualkey in the keyslot, and it's not us + # (ie, prevent "ALT+ALT"), bump it to the ModSlot and + # use us as the KeySlot. + # + # TODO there's still a weird case where if we are holding down SHIFT + # and keep tapping ALT, while LR is true, we get "ALT+LALT" self.ModSlot = self.NormalizeDualKeyName(self.KeySlot) self.KeySlot = payload elif self.ModSlot != self.NormalizeDualKeyName(payload):