How do I get Java to output to where my focus is? (example: open text document)

84 views Asked by At

I used jnativehook.keyboard.NativeKeyListener to listen to keys typed in an open text document. No problem here. After a short permutation (think Caesar cipher), I want to send the output back to that open text document. How do I do this? This seems to be surprisingly complicated...

1

There are 1 answers

8
JayC667 On

Actually, opposed to what the commenters to your post say, there IS a Java way to send key events, using the Robot.

  1. Robot bot = new Robot(); (java.awt.Robot)

  2. If you have NativeKeyEvent pKeyEvent as in org.jnativehook.keyboard.NativeKeyEvent then you can send that key back with

         bot.keyPress(pKeyEvent.getRawCode());
         actionSleep(); // sleep for as long as you need, normal is ~20-60 ms
         bot.keyRelease(pKeyEvent.getRawCode());
    
  3. If you have the Java Swing/AWT KeyEvent (KeyEvent pKeyEvent as in java.awt.event.KeyEvent), you can use it like this:

         bot.keyPress(pKeyEvent.getKeyCode());
         actionSleep(); // sleep
         bot.keyRelease(pKeyEvent.getKeyCode());
    
  4. If you want to send keys via code, you can use this (for pressing and releasing key 'a'):

         bot.keyPress(KeyEvent.VK_A);
         actionSleep(); // sleep
         bot.keyRelease(KeyEvent.VK_A);
    

The Robot also has some additional functionality, like screen capture / screenshot, screen pixel reading, control mouse.

Example app:

package stackoverflow.keylistenercaesar;

import java.awt.AWTException;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.github.kwhat.jnativehook.GlobalScreen;
import com.github.kwhat.jnativehook.NativeHookException;
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent;
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener;

import jc.lib.lang.thread.JcUThread;

/**
 * Written with jnativehook_2.2.0<br>
 * <br>
 * <b>Important!</b> jnativehook_2.1.0 does NOT work correctly under windows, because it does not generate addNativeKeyListener->nativeKeyTyped events!
 *
 * @author jc
 * @since 2023-09-23
 *
 */
public class KeyListenerCaesarChiffre {

    static public final int KEY_PRESS_DURATION_MS = 1;



    static private StringBuilder        sRecordedChars          = new StringBuilder();
    static private ArrayList<Integer>   sRecordedRawKeyCodes    = new ArrayList<>();

    static private Robot sRobot;

    static private volatile boolean sRecordKeys;



    static public void main(final String[] args) throws NativeHookException {
        // disable the stupid logger
        final Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName());
        logger.setLevel(Level.WARNING);
        logger.setUseParentHandlers(false);

        // set up key hook listening
        GlobalScreen.registerNativeHook();
        GlobalScreen.addNativeKeyListener(new NativeKeyListener() {
            @Override public void nativeKeyPressed(final NativeKeyEvent pKeyEvent) {
                //              System.out.println("nativeKeyPressed(c" + pKeyEvent.getKeyCode() + " r" + pKeyEvent.getRawCode() + " c" + pKeyEvent.getKeyChar() + ")");
            }
            @Override public void nativeKeyReleased(final NativeKeyEvent pKeyEvent) {
                //              System.out.println("nativeKeyReleased(c" + pKeyEvent.getKeyCode() + " r" + pKeyEvent.getRawCode() + " c" + pKeyEvent.getKeyChar() + ")");
                try {
                    controlKeyReleased(pKeyEvent);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
            @Override public void nativeKeyTyped(final NativeKeyEvent pKeyEvent) {
                //              System.out.println("nativeKeyTyped(c" + pKeyEvent.getKeyCode() + " r" + pKeyEvent.getRawCode() + " c" + pKeyEvent.getKeyChar() + ")");
                try {
                    inputKeyTyped(pKeyEvent);
                } catch (final Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // print info
        System.out.println("Key Bindings");
        System.out.println("\tKey\tUse");
        System.out.println("\t------\t------");
        System.out.println("\tF2\tStart Listening");
        System.out.println("\tF3\tEnd Listening");
        System.out.println("\tF4\tExit app");
        System.out.println("\tEnter\tTrigger caesar conversion + output");
    }

    /**
     * This method will be called each time any key is released. So we have to filter carefully
     */
    static protected void controlKeyReleased(final NativeKeyEvent pKeyEvent) throws NativeHookException {
        switch (pKeyEvent.getKeyCode()) {
            case NativeKeyEvent.VC_F2: {
                System.out.println("Start listening...");
                sRecordKeys = true;
                break;
            }
            case NativeKeyEvent.VC_F3: {
                System.out.println("Stop listening...");
                sRecordKeys = false;
                break;
            }
            case NativeKeyEvent.VC_F4: {
                System.out.println("Shutting down...");
                GlobalScreen.unregisterNativeHook();
                break;
            }
            case NativeKeyEvent.VC_ENTER: {
                if (sRecordKeys) runCasesarAndOutput();
                break;
            }
            default: // ignore the rest
        }
    }

    /**
     * This method will only get triggered for valid input chars. So no control codes like F1, F2 etc will arrive here
     */
    static protected void inputKeyTyped(final NativeKeyEvent pKeyEvent) {
        if (!sRecordKeys) return;

        //      System.out.println("KeyListenerCaesarChiffre.inputKeyTypedx(" + pKeyEvent.getKeyCode() + " r" + pKeyEvent.getRawCode() + " c" + pKeyEvent.getKeyChar() + ")");
        switch (pKeyEvent.getKeyChar()) {
            default: {
                //              System.out.println("Key:\t" + pKeyEvent.getKeyCode() + " / " + pKeyEvent.getRawCode() + " => " + pKeyEvent.getKeyChar());
                sRecordedChars.append(pKeyEvent.getKeyChar());
                sRecordedRawKeyCodes.add(Integer.valueOf(pKeyEvent.getRawCode()));
                System.out.println("\tlen:" + sRecordedChars.length() + "\ttext:" + sRecordedChars.toString());
            }
        }
    }



    /**
     * This will need to run in its own thread.
     * If we do not, we would run in the key event listener thread, and all keys simulated here would only be processed AFTER this method is done,
     * so it would record its own inputs.
     */
    static private void runCasesarAndOutput() {
        new Thread(() -> {
            try {
                runCasesarAndOutput_();
            } catch (final AWTException e) {
                e.printStackTrace();
            }
        }).start();
    }
    static private void runCasesarAndOutput_() throws AWTException {
        // wait until enter key event is processed
        sleep(50);

        try {
            // suspend key listening temporaily or we will feed back into our own input
            sRecordKeys = false;

            // send output. Do NOT process the enter key, thus size-1
            for (int i = 0; i < sRecordedRawKeyCodes.size() - 1; i++) {
                final int c = sRecordedRawKeyCodes.get(i).intValue();
                typeKey(c + 1); // this is a really bad implementation of caesar
                // especially because this will fall out of the keymapping boundaries a lot!
                // some normal chars might get converted into control chars, like the arrow keys, for example
                // also, letters do not wrap back up (z does not wrap back up to a in case of +1)
                // also, you need to take additional care of special control keys like backspace the handle the ensuing "text" properly!
            }

            // send enter to finish line
            typeKey(KeyEvent.VK_ENTER);

        } finally {
            sRecordKeys = true;
        }

        // clear intercepted key chars
        sRecordedChars.setLength(0);
        sRecordedRawKeyCodes.clear();
    }



    static public void typeKey(final int pKeyCode) throws AWTException {
        if (sRobot == null) sRobot = new Robot();
        try {
            sRobot.keyPress(pKeyCode);
            sleep(KEY_PRESS_DURATION_MS);
            sRobot.keyRelease(pKeyCode);
            //          sleep(KEY_PRESS_DURATION_MS);

        } catch (final java.lang.IllegalArgumentException e) {
            System.err.println("Key code [" + pKeyCode + "] is invalid!");
            e.printStackTrace();
        }
    }
    static public void beep(final int pCount) {
        final Toolkit dtk = Toolkit.getDefaultToolkit();
        for (int i = 0; i < pCount; i++) {
            dtk.beep();
            JcUThread.sleep(300);
        }
    }
    static public void sleep(final int pMS) {
        try {
            Thread.sleep(pMS);
        } catch (final InterruptedException e) { /* */ }
    }



}

There's lots of downsides to this implementation and the problem per se:

  • Needs jnativehook_2.2.0 as earlier versions do NOT work reliably under Windows
  • Needs a lot of special care when handling control keys like backspace, arrows etc.
  • Caesar chiper will be a pain in the ass to implement properly
  • More details in the comments

Usage:

  • Run the app
  • Head into your text editor
  • Press F2 to start recording the keys
  • When you press Enter, the keys will be caesar-transformed and then sent to the doc as keystrokes
  • Press F3 to halt recording
  • Or press F4 to shut down the app