Firstly apologies for creating a GitHub issue for this but I don't really know where else to get help!
I have implemented a script that allows "Mouse Button 5" to act as though I'm holding down Command in World of Warcraft. The simple way of trapping the mouseOtherDown and mouseOtherUp events and making them "post" keyUp and keyDown events for the command key did not work, so instead I have had to use those events to set a "wowCmdMod" variable to true or false and then add a 3rd event hook that check keypresses and if they key is 1 through 6 and the mouse "wowCmdMod" is true then it changes the key press to CMD-1 through CMD-6, which is then picked up in WoW.
This is the only thing that I could get to work since WoW does not recognise just applying the modifier on mouseOtherDown / Up. BUT it causes terrible lag in the game. When you turn things become a slideshow.
Is there any other way to achieve this without the keyboard hook? I'll try and paste my code below.
wowCmdMod = false
function mouseDown(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
-- Set modifer for later use since WoW doesn't see the
-- modifiers if we try to directly inject them.
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
print(hs.window.focusedWindow():application():bundleID().." Button 5 down : CMD")
wowCmdMod = true
return true
end
end
end
function mouseUp(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
-- Unset wowModifier
print(hs.window.focusedWindow():application():bundleID().." Button 5 up : CMD")
wowCmdMod = false
return true
end
end
return false
end
function keyDown(eventObj)
if (hs.window.focusedWindow() ~= nil) and (hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft") then
kc = eventObj:getKeyCode()
-- Examine keys 1 to 6 and see if we want to inject a modifier
if kc > 17 and kc < 24 then
print(hs.window.focusedWindow():application():bundleID()..": "..kc)
if wowCmdMod == true then
newEvents = { hs.eventtap.event.newKeyEvent({'cmd'}, kc, true),
hs.eventtap.event.newKeyEvent({'cmd'}, kc, false):post() }
return true, newEvents
end
end
end
end
MU = hs.eventtap.new({hs.eventtap.event.types.otherMouseUp}, mouseUp):start()
MD = hs.eventtap.new({hs.eventtap.event.types.otherMouseDown}, mouseDown):start()
KD = hs.eventtap.new({hs.eventtap.event.types.keyDown}, keyDown):start()
If I comment out the start of the keyDown eventtap then the lag goes away.
Unfortunately, I don't think there is anything we can do about it... By capturing keyDown messages, all key strokes are monitored by Hammerspoon, and this can cause a noticeable delay, which WOW is sensing and reducing its framerate for.
I tried modifying some of the internals to hs.eventtap to see if we could get around this (there are a couple of different sources we can specify when we inject events and we've chosen the one that provides the most consistent experience), but this didn't help and in fact broke some of the existing use cases we do regularly test against. It boils down to the fact that outside of Apple's Sticky Keys accessibility feature, Command, Option, Shift, and Control are not toggle-able -- they are just modifiers to other events that must be sent in sequence.
I'd be curious if anyone knows of a way to programmatically enable/disable the features of Sticky Keys, but outside of this possibility, I'm at a loss for a better approach.
That said, you might be able to make it bearable by only enabling the key watcher when you actually need it -- i.e. add KD:start() in MD when button == 4 and KD:stop() in MU when button == 4. That way it's only slowing things down when the mouse button is actually pressed.
Something like this:
~~~lua
function mouseDown(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
-- Set modifer for later use since WoW doesn't see the
-- modifiers if we try to directly inject them.
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
print(hs.window.focusedWindow():application():bundleID().." Button 5 down : CMD")
KD:start()
return true
end
end
end
function mouseUp(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
-- Unset wowModifier
print(hs.window.focusedWindow():application():bundleID().." Button 5 up : CMD")
KD:stop()
return true
end
end
return false
end
function keyDown(eventObj)
if (hs.window.focusedWindow() ~= nil) and (hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft") then
kc = eventObj:getKeyCode()
-- Examine keys 1 to 6 and see if we want to inject a modifier
if kc > 17 and kc < 24 then
print(hs.window.focusedWindow():application():bundleID()..": "..kc)
newEvents = { hs.eventtap.event.newKeyEvent({'cmd'}, kc, true),
hs.eventtap.event.newKeyEvent({'cmd'}, kc, false):post() }
return true, newEvents
end
end
end
KD = hs.eventtap.new({hs.eventtap.event.types.keyDown}, keyDown)
MU = hs.eventtap.new({hs.eventtap.event.types.otherMouseUp}, mouseUp):start()
MD = hs.eventtap.new({hs.eventtap.event.types.otherMouseDown}, mouseDown):start()
~~~
@asmagill Good catch - thank you! I've just given that a quick test and it does indeed solve the issue, or at least mitigate the damage as it were. It doesn't technically make the problem go away but with us humans only having two hands you can't really run / move, turn, cast a spell and apply the modifier all at the same time so in reality you don't really see the issue.
I can also simplify the logic in keyDown, firstly by removing the test for the active window, since the key watcher will only be started if the window is WoW and also as per your example, there is no need for the 'wowCmdMod' flag either.
It turns out I also have a similar handler running in another code snippet to catch CMD-Left and CMD-Right for Terminal. Two keyDown watchers wasn't helping at all, but I should be able to apply a similar principal in only starting the terminal keyDown watcher when the terminal window has focus.
I'll leave this for a couple of days in case anyone else has any other input but then close it. This largely boils down to me needing to get into the proper mindset of how Hammerspoon works.
Thanks again for the input.
Closing since the solution worked for me and there has been no further input.
_--snip--_
[...] It boils down to the fact that outside of Apple's Sticky Keys accessibility feature, Command, Option, Shift, and Control are not toggle-able -- they are just modifiers to other events that must be sent in sequence.I'd be curious if anyone knows of a way to programmatically enable/disable the features of Sticky Keys, but outside of this possibility, I'm at a loss for a better approach.
You could do this with AppleScript (thanks to dxlr8r [1]):
tell application "System Events" to key down command
I tried it with middle mouse button and it was working fine. I am not sure if it is fast enough for a game though.
A quick note: if you are going to test this, don't forget to add "key up command" after it. I didn't do it at first, and had to logout to reset this command down state. :sweat_smile:
[1] https://github.com/pqrs-org/Karabiner-Elements/issues/2524
When I was testing this, only mouse down event's callback was fired if you do "normal" mouse clicks. For mouse up event's callback to fire, I had to hold down the mouse button for a few milliseconds before releasing it. Is this a bug?
Most helpful comment
Something like this:
~~~lua
function mouseDown(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
-- Set modifer for later use since WoW doesn't see the
-- modifiers if we try to directly inject them.
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
print(hs.window.focusedWindow():application():bundleID().." Button 5 down : CMD")
KD:start()
return true
end
end
end
function mouseUp(eventObj)
if hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft" then
if eventObj:getProperty(hs.eventtap.event.properties.mouseEventButtonNumber) == 4 then
-- Unset wowModifier
print(hs.window.focusedWindow():application():bundleID().." Button 5 up : CMD")
KD:stop()
return true
end
end
return false
end
function keyDown(eventObj)
if (hs.window.focusedWindow() ~= nil) and (hs.window.focusedWindow():application():bundleID() == "com.blizzard.worldofwarcraft") then
kc = eventObj:getKeyCode()
-- Examine keys 1 to 6 and see if we want to inject a modifier
if kc > 17 and kc < 24 then
print(hs.window.focusedWindow():application():bundleID()..": "..kc)
newEvents = { hs.eventtap.event.newKeyEvent({'cmd'}, kc, true),
hs.eventtap.event.newKeyEvent({'cmd'}, kc, false):post() }
return true, newEvents
end
end
end
KD = hs.eventtap.new({hs.eventtap.event.types.keyDown}, keyDown)
MU = hs.eventtap.new({hs.eventtap.event.types.otherMouseUp}, mouseUp):start()
MD = hs.eventtap.new({hs.eventtap.event.types.otherMouseDown}, mouseDown):start()
~~~