Getting global keystrokes in a Java or Scala Swing application

If you ever wanted to get access to global operating system keystrokes from a Java or Scala Swing application, you can do it with the jnativehook library. Here’s a short demo:

Here’s the full Scala source code for the demo. I put a few comments in the code to highlight the important areas:

package com.alvinalexander.mackeystrokes

import scala.swing._
import scala.swing.event.EditDone
import scala.swing.event.WindowActivated
import org.jnativehook.GlobalScreen
import org.jnativehook.NativeHookException
import org.jnativehook.keyboard.NativeKeyEvent
import org.jnativehook.keyboard.NativeKeyListener
import javax.swing.SwingUtilities

import java.awt.Graphics
import javax.swing.JPanel
import java.awt.Color

// (1) extend NativeKeyListener
object MacKeystrokes 
extends SimpleSwingApplication 
with NativeKeyListener 
{ 

  val charPanel = new CharPanel {
    setPreferredSize(new Dimension(400, 300))
  }
  
  def top = new MainFrame { 
    title = "Mac Keystrokes"
    peer.getContentPane.add(charPanel)
    peer.setLocationRelativeTo(null)
    peer.setBackground(Color.BLACK)
  }

  // (2) GlobalScreen.registerNativeHook
  try {
    GlobalScreen.registerNativeHook
  } catch {
    case e: NativeHookException =>
          System.err.println("There was a problem registering the native hook.")
          System.err.println(e.getMessage)
          System.exit(1)
  }


  // (3) implement nativeKeyPressed, nativeKeyReleased, and nativeKeyTyped
  def nativeKeyPressed(e: NativeKeyEvent) {
    val key = NativeKeyEvent.getKeyText(e.getKeyCode)
    println("Key Pressed: " + NativeKeyEvent.getKeyText(e.getKeyCode))
    SwingUtilities.invokeLater(new Runnable {
      def run {
        charPanel.setChar(key.toCharArray)
      }
    })
    if (e.getKeyCode == NativeKeyEvent.VK_ESCAPE) {
      GlobalScreen.unregisterNativeHook
    }
  }

  def nativeKeyReleased(e: NativeKeyEvent) {}
  def nativeKeyTyped(e: NativeKeyEvent) {}

  // (4) do this
  GlobalScreen.getInstance.addNativeKeyListener(this)

}

import scala.util.Random
import java.awt.Font

class CharPanel extends JPanel {

  private val colors = Array(Color.BLUE, Color.CYAN, Color.DARK_GRAY, 
                             Color.GREEN, Color.LIGHT_GRAY, Color.MAGENTA, 
                             Color.ORANGE, Color.RED, Color.YELLOW)
  private var chars = Array('a')
  private val r = new Random
  var i = 10
  
  def setChar(charArray: Array[Char]) { 
    this.chars = charArray
    i += 15
    repaint()
  }
 
  override def paintComponent(g: Graphics) {
    val font = new Font(Font.SANS_SERIF, Font.PLAIN, 60)
    val color = colors(r.nextInt(colors.length))
    g.setFont(font)
    g.setColor(color)
    g.clearRect(0, 0, 400, 300)
    g.drawChars(chars, 0, 1, r.nextInt(320), r.nextInt(220)+20)
  }

}

I needed to be able to do something like this for SARAH, so I dug around until I found this solution, which works great.

To get this to work, you have to enable “access for assistive devices”, as explained in this apple.com article.

Again, I did remarkably little here; the secret sauce is in the jnativehook library. See their examples and docs for more information.