Global keyboard hook

Mac OSX specific forum
User avatar
Shardik
Addict
Addict
Posts: 2060
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Global keyboard hook

Post by Shardik »

spacebuddy wrote:Is it possible to create a global keyboard hook for a mac application?
The simplest solution is to use the HotKey API functions of Carbon. Unfortunately there doesn't exist a comparably simple and efficient Cocoa API solution. Most Cocoa examples simply wrap the Carbon API functions. Therefore I have done the same in the following example which I tested successfully on Mac 10.6.8 (Snow Leopard) and 10.8.2 (Mountain Lion) with Cocoa x86 and x64 and subsystem Carbon in both ASCII and Unicode mode.

The example code starts an invisible window and waits for the hot key combination Shift+Control+Space. When detected it makes the window visible. You may define several different hot keys which are handled in the HotKeyHandler() callback. The 4 byte signature of a hot key is user-defined and may be changed at will.

Code: Select all

EnableExplicit

#kEventClassKeyboard = $6B657962     ; 'keyb'
#kEventHotKeyPressed = 5
#kEventParamDirectObject = $2D2D2D2D ; '----'
#typeEventHotKeyID = $686B6964       ; 'hkid'

#controlKeyBit = 12
#controlKey = 1 << #controlKeyBit
#shiftKeyBit = 9
#shiftKey = 1 << #shiftKeyBit

ImportC ""
  GetApplicationEventTarget()
  RegisterEventHotKey(HotKeyCode.L, HotKeyModifiers.L, HotKeyID.Q, EventTargetRef.I, OptionBits.L, *EventHotKeyRef)
  UnregisterEventHotKey(EventHotKeyRef.I)
EndImport

Structure EventTypeSpec
  EventClass.L
  EventKind.L
EndStructure

Structure EventHotKeyID
  Signature.L
  ID.L
EndStructure

ProcedureC HotKeyHandler(NextHandlerRef.I, EventRef.I, *UserData)
  Protected HotKey.Q
  If GetEventParameter_(EventRef, #kEventParamDirectObject, #typeEventHotKeyID, 0, SizeOf(EventHotKeyID), 0, @HotKey) = 0
    If PeekL(HotKey) = $68746B31 ; HotKey\Signature = 'htk1' ?
      If PeekL(HotKey + 4) = 1   ; HotKey\ID = 1 ?
        HideWindow(0, #False)
      EndIf
    EndIf
  EndIf
EndProcedure

Define HotKey.EventHotKeyID
Define HotKeyRef.I

Dim EventTypes.EventTypeSpec(0)

EventTypes(0)\EventClass = #kEventClassKeyboard
EventTypes(0)\EventKind = #kEventHotKeyPressed

HotKey\Signature = $68746B31 ; 'htk1'
HotKey\ID = 1

OpenWindow(0, 270, 100, 220, 70, "Activated by HotKey", #PB_Window_SystemMenu | #PB_Window_Invisible)

; ----- Install EventHandler to react to pressing of hot key(s)
If InstallEventHandler_(GetApplicationEventTarget(), @HotKeyHandler(), 1, @EventTypes(), 0, 0) = 0
  ; ----- Register hot key (Control+Shift+Space)
  RegisterEventHotKey(49, #controlKey | #shiftKey, @HotKey, GetApplicationEventTarget(), 0, @HotKeyRef)
EndIf

Repeat
Until WaitWindowEvent() = #PB_Event_CloseWindow

; ----- You do not need to unregister a hot key when your application
;       terminates; the system takes care of that for you. You can use this
;       function if the user changes a hot key for something in your
;       application - you would unregister the previous key and register your
;       new key.
UnregisterEventHotKey(HotKeyRef)
spacebuddy
Enthusiast
Enthusiast
Posts: 361
Joined: Thu Jul 02, 2009 5:42 am

Re: Global keyboard hook

Post by spacebuddy »

Thanks Shardik, it works perfectly :D :D
nikoniko
User
User
Posts: 20
Joined: Fri Nov 11, 2011 7:58 am

Re: Global keyboard hook

Post by nikoniko »

I found JNativeHook java library http://code.google.com/p/jnativehook/
Is it really set global hook under MacOSX?
User avatar
Shardik
Addict
Addict
Posts: 2060
Joined: Thu Apr 21, 2005 2:38 pm
Location: Germany

Re: Global keyboard hook

Post by Shardik »

nikoniko wrote:I found JNativeHook java library http://code.google.com/p/jnativehook/
Thank you for your interesting link! I downloaded the sourcecode of JNativeHook and took a look into /JNativeHook/src/native/osx/NativeThread.c. They used the API functions CGEventMaskBit() to setup the event mask to listen for and CGEventTapCreate() (just like wilbert had proposed).
nikoniko wrote:Is it really set global hook under MacOSX?
Yes, JNativeHook sets up a global hook, just like my PureBasic example which is much simpler by using higher level Carbon API functions to reduce the lines of code you have to use... :wink:
User avatar
Andre
PureBasic Team
PureBasic Team
Posts: 2139
Joined: Fri Apr 25, 2003 6:14 pm
Location: Germany (Saxony, Deutscheinsiedel)
Contact:

Re: Global keyboard hook

Post by Andre »

@shardik: your example works well on MacOS 10.5.8 too, thank you! :D
Bye,
...André
(PureBasicTeam::Docs & Support - PureArea.net | Order:: PureBasic | PureVisionXP)
nikoniko
User
User
Posts: 20
Joined: Fri Nov 11, 2011 7:58 am

Re: Global keyboard hook

Post by nikoniko »

Shardik wrote:Yes, JNativeHook sets up a global hook, just like my PureBasic example which is much simpler by using higher level Carbon API functions to reduce the lines of code you have to use... :wink:
Your example is simple but doesn't track all keystrokes (ok, most of them).

It is useful for advanced tool to capture ctrl, shift, etc keys.
nikoniko
User
User
Posts: 20
Joined: Fri Nov 11, 2011 7:58 am

Re: Global keyboard hook

Post by nikoniko »

Seems it isn't hard for macosx guru, but not for me yet.

Add 2 links (use this topic as bookmark)

Log keydowns and mouse buttons
http://forums.appleinsider.com/t/7872/k ... r-mac-os-x

Receiving, Filtering, and Modifying Mouse Events
http://www.osxbook.com/book/bonus/chapter2/altermouse/
And key events
http://www.osxbook.com/book/bonus/chapter2/alterkeys/
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3942
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Global keyboard hook

Post by wilbert »

Windows (x64)
Raspberry Pi OS (Arm64)
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3942
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Global keyboard hook

Post by wilbert »

Code: Select all

EnableExplicit

#KeyDownMask      = 1 << 10
#KeyUpMask        = 1 << 11
#FlagsChangedMask = 1 << 12

#kCGKeyboardEventKeycode = 9

ImportC ""
  CGEventTapCreate(tap, place, options, eventsOfInterest.q, callback, refcon)
  CGEventGetIntegerValueField.q(event, field)
  CGEventKeyboardGetUnicodeString(event, maxStringLength, *actualStringLength, *unicodeString)
EndImport

Define eventTap

ProcedureC eventTapFunction(proxy, type, event, refcon)
  Protected keyCode = CGEventGetIntegerValueField(event, #kCGKeyboardEventKeycode)
  Protected unicodeBuffer.q, bufferLen.i = 4
  CGEventKeyboardGetUnicodeString(event, bufferLen, @bufferLen, @unicodeBuffer)
  Protected text.s = PeekS(@unicodeBuffer, bufferLen, #PB_Unicode)
  
  SetGadgetText(0, "keyCode : " + Str(keyCode) + #LF$ + "String : " + text)
EndProcedure


If OpenWindow(0, 0, 0, 220, 70, "keyCode tap", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  StickyWindow(0, 1)
  StringGadget(0, 10, 10, 200, 50, "")
  
  eventTap = CGEventTapCreate(0, 0, 1, #KeyDownMask | #FlagsChangedMask, @eventTapFunction(), 0)
  If eventTap
    CocoaMessage(0, CocoaMessage(0, 0, "NSRunLoop currentRunLoop"), "addPort:", eventTap, "forMode:$", @"kCFRunLoopDefaultMode")
  EndIf

  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
Windows (x64)
Raspberry Pi OS (Arm64)
nikoniko
User
User
Posts: 20
Joined: Fri Nov 11, 2011 7:58 am

Re: Global keyboard hook

Post by nikoniko »

Wilbert +100

While I am writing reply about my attempts to change your code for all processes and flags keys you are posting fully working code for me.

Thanks, thanks, thanks.
wilbert
PureBasic Expert
PureBasic Expert
Posts: 3942
Joined: Sun Aug 08, 2004 5:21 am
Location: Netherlands

Re: Global keyboard hook

Post by wilbert »

Here's a modification also showing the keyboard related flags

Code: Select all

EnableExplicit

#KeyDownMask      = 1 << 10
#KeyUpMask        = 1 << 11
#FlagsChangedMask = 1 << 12

#kCGKeyboardEventKeycode = 9

ImportC ""
  CGEventTapCreate(tap, place, options, eventsOfInterest.q, callback, refcon)
  CGEventGetFlags.q(event)
  CGEventGetIntegerValueField.q(event, field)
  CGEventKeyboardGetUnicodeString(event, maxStringLength, *actualStringLength, *unicodeString)
EndImport

Define eventTap

ProcedureC eventTapFunction(proxy, type, event, refcon)
  Protected keyCode = CGEventGetIntegerValueField(event, #kCGKeyboardEventKeycode)
  Protected keyFlags = CGEventGetFlags(event) >> 16 & 255
  Protected unicodeBuffer.q, bufferLen.i = 4
  CGEventKeyboardGetUnicodeString(event, bufferLen, @bufferLen, @unicodeBuffer)
  Protected text.s = PeekS(@unicodeBuffer, bufferLen, #PB_Unicode)
  
  SetGadgetText(0, "keyCode : " + Str(keyCode) + #LF$ + "keyFlags : " + RSet(Bin(keyFlags), 8, "0") + #LF$ + "string : " + text)
EndProcedure


If OpenWindow(0, 0, 0, 220, 90, "keyCode tap", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  StickyWindow(0, 1)
  StringGadget(0, 10, 10, 200, 70, "")
  
  eventTap = CGEventTapCreate(0, 0, 1, #KeyDownMask | #FlagsChangedMask, @eventTapFunction(), 0)
  If eventTap
    CocoaMessage(0, CocoaMessage(0, 0, "NSRunLoop currentRunLoop"), "addPort:", eventTap, "forMode:$", @"kCFRunLoopDefaultMode")
  EndIf

  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
And a practical example of responding to the key combination [fn] + [esc]

Code: Select all

EnableExplicit

#KeyDownMask      = 1 << 10
#FlagsChangedMask = 1 << 12

#kCGKeyboardEventKeycode = 9

ImportC ""
  CGEventTapCreate(tap, place, options, eventsOfInterest.q, callback, refcon)
  CGEventGetFlags.q(event)
  CGEventGetIntegerValueField.q(event, field)
EndImport

Define eventTap


Global SpeechSynthesizer = CocoaMessage(0, CocoaMessage(0, 0, "NSSpeechSynthesizer alloc"), "initWithVoice:", #nil)

ProcedureC eventTapFunction(proxy, type, event, refcon)
  Protected keyCode = CGEventGetIntegerValueField(event, #kCGKeyboardEventKeycode)
  Protected keyFlags = CGEventGetFlags(event) >> 16 & 255
  
  If keyCode = 53 And keyFlags & $80
    ; [fn] + [esc] pressed
    CocoaMessage(0, SpeechSynthesizer, "startSpeakingString:$", @"fn and esc pressed")
  EndIf
  
EndProcedure


If OpenWindow(0, 0, 0, 200, 30, "key tap", #PB_Window_SystemMenu | #PB_Window_Minimize | #PB_Window_NoActivate)
  
  eventTap = CGEventTapCreate(0, 0, 1, #KeyDownMask | #FlagsChangedMask, @eventTapFunction(), 0)
  If eventTap
    CocoaMessage(0, CocoaMessage(0, 0, "NSRunLoop currentRunLoop"), "addPort:", eventTap, "forMode:$", @"kCFRunLoopDefaultMode")
  EndIf

  Repeat : Until WaitWindowEvent() = #PB_Event_CloseWindow
EndIf
Windows (x64)
Raspberry Pi OS (Arm64)
User avatar
mk-soft
Always Here
Always Here
Posts: 6253
Joined: Fri May 12, 2006 6:51 pm
Location: Germany

Re: Global keyboard hook

Post by mk-soft »

Thanks for Hotkey example :D

here the complete HotKeyModifiers

Code: Select all

#cmdKeyBit = 8
#shiftKeyBit = 9
#alphaLockBit = 10
#optionKeyBit = 11
#controlKeyBit = 12
#rightShiftKeyBit = 13
#rightOptionKeyBit = 14
#rightControlKeyBit = 15
#cmdKey = 1 << #cmdKeyBit
#shiftKey = 1 << #shiftKeyBit
#alphaLock = 1 << #alphaLockBit
#optionKey = 1 << #optionKeyBit
#controlKey = 1 << #controlKeyBit
#rightShiftKey = 1 << #rightShiftKeyBit
#rightOptionKey = 1 << #rightOptionKeyBit
#rightControlKey = 1 << #rightControlKeyBit
GT :wink:
My Projects ThreadToGUI / OOP-BaseClass / EventDesigner V3
PB v3.30 / v5.75 - OS Mac Mini OSX 10.xx - VM Window Pro / Linux Ubuntu
Downloads on my Webspace / OneDrive
User avatar
deseven
Enthusiast
Enthusiast
Posts: 367
Joined: Wed Jan 12, 2011 3:48 pm
Location: Serbia
Contact:

Re: Global keyboard hook

Post by deseven »

Thanks Shardik, wilbert and mk-soft! I wrote a module for that based on your code:
http://forums.purebasic.com/english/vie ... 14&t=64532
NY152
User
User
Posts: 29
Joined: Sun May 14, 2006 12:33 am

Re: Global keyboard hook

Post by NY152 »

CocoaMessage() is not functon ... etc

EDIT:

CocoaMessage is a MACOX function sorry ...

Windows equivalent exists ?
.:NY152:.
User avatar
deseven
Enthusiast
Enthusiast
Posts: 367
Joined: Wed Jan 12, 2011 3:48 pm
Location: Serbia
Contact:

Re: Global keyboard hook

Post by deseven »

well, yes
Post Reply