Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started
All examples
RecipesInput110 lines · 1 files

Keyboard and focus

Tab follows the order you ADDED the fields, not where they ended up on screen. Build in reading order and the tab order is correct without being designed.

What this shows

Filling a form without touching the screen.

Every CrossFly app runs on the desktop, and on the desktop a form is filled with the keyboard. Tab walks the fields in the order they were added, which is why building a form in reading order is not only tidier — it is the tab order, for free. Build it out of order and the user tabs from the postcode back up to the surname.

Two style flags decide who can be reached. focusOnTouch means tapping this gives it the keyboard; focusOnKey means a keypress can move focus into it. A button that should be reachable by Tab needs the second, and a display-only panel wants neither — a Tab stop on something that does nothing is a Tab stop the user has to press through every time.

The other keyboard is the one you bring with you: NumericKeypad is an on-screen pad for a device that has no keys at all, or for a value that is only ever digits — a warehouse terminal, a scale, a till.

Run it on your machine

Every example is a plain Window, so the launcher only needs its name.

gradlew lwjgl3:run --args="classname dev.cf.recipe.R27Focus"
Editing it

Java is compiled ahead of time, so the browser cannot rebuild an edit on its own — the code you see here was turned into JavaScript by TeaVM before it was published. Copy the file into a project and it runs unchanged on all four platforms; the command above is the whole setup.

Running hereCrossFly 0.1.32
Starting the app…

Compiled to WebAssembly-era JavaScript by TeaVM — the same code, not a video of it. · All examples

What it uses

Framework types this example touches. The linked ones have a reference page.

ButtonComponentHintLabelNumericKeypadScreenStylesTextFieldWidgets

The source

Complete and unabridged. This is the file, not an excerpt of it.

Raw
dev/cf/recipe/R27Focus.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Filling a form without touching the screen.
 *
 * <p>Every CrossFly app runs on the desktop, and on the desktop a form is filled with the keyboard.
 * Tab walks the fields in the order they were <b>added</b>, which is why building a form in reading
 * order is not only tidier — it is the tab order, for free. Build it out of order and the user
 * tabs from the postcode back up to the surname.</p>
 *
 * <p>Two style flags decide who can be reached. {@code focusOnTouch} means tapping this gives it
 * the keyboard; {@code focusOnKey} means a keypress can move focus into it. A button that should
 * be reachable by Tab needs the second, and a display-only panel wants neither — a Tab stop on
 * something that does nothing is a Tab stop the user has to press through every time.</p>
 *
 * <p>The other keyboard is the one you bring with you: {@link NumericKeypad} is an on-screen pad
 * for a device that has no keys at all, or for a value that is only ever digits — a warehouse
 * terminal, a scale, a till.</p>
 */
public class R27Focus extends Recipe
{
   private Label reached;

   public String title()
   {
      return "Keyboard and focus";
   }

   public String note()
   {
      return "Tab follows the order you ADDED the fields, not where they ended up on screen. Build"
         + " in reading order and the tab order is correct without being designed.";
   }

   public void build(Component body) throws Exception
   {
      body.add(LAY_AUTO, outgap(label("Press Tab. It walks these in the order they were added.", 1f,
         false, Styles.captionColor, Position.START), 0, 0, 0, .8f));

      field(body, "Name");
      field(body, "Company");
      field(body, "Postcode");

      reached = label("Focus has not reached the button yet.", 1f, false, Styles.captionColor,
         Position.START);
      body.add(LAY_AUTO, outgap(reached, 0, .4f, 0, .6f));

      Button send = Tones.action("Send");
      // Reachable by keyboard, not only by pointer. Without this the last thing in the form is the
      // one thing a keyboard user cannot get to, and they have to reach for the mouse to finish.
      send.getStyle()
         .focusOnKey(true)
         .focusOnTouch(true);
      send.onAction(c -> Hint.show("Sent", 1500));
      send.testTag("focus.send");
      body.add(LAY_AUTO, outgap(send, 0, 0, 0, 1.4f));

      body.add(LAY_AUTO, Tones.head(0xE312, "A keypad of your own", Tones.highlight()));
      body.add(LAY_AUTO, outgap(label("For a device with no keyboard, or a value that is only ever"
         + " digits.", 1f, false, Styles.captionColor, Position.START), 0, 0, 0, .6f));

      final TextField total = new TextField();
      total.caption("Total");
      total.setMode(EditMode.CURRENCY);
      total.testTag("focus.total");
      body.add(LAY_AUTO, outgap(total, 0, 0, 0, .6f));

      // The keypad reports TOKENS, not characters — "0".."9", "," and BACK — so the screen decides
      // what each one means. That is what lets the same pad drive a currency field, a quantity and
      // a PIN without the pad knowing about any of them.
      NumericKeypad pad = new NumericKeypad(token ->
      {
         String now = total.getText();
         if (NumericKeypad.BACK.equals(token))
            total.setText(now.isEmpty() ? "" : now.substring(0, now.length() - 1));
         else
            total.setText(now + token);
         Screen.repaint();
      });
      pad.testTag("focus.keypad");
      body.add(LAY_AUTO, pad);
   }

   private void field(Component body, String caption) throws Exception
   {
      TextField f = new TextField();
      f.caption(caption);
      f.testTag("focus." + caption.toLowerCase());
      // Fired when the field takes the keyboard. On a phone this is where a screen scrolls the
      // field clear of the on-screen keyboard, which is what Device.shiftScreen is for.
      f.onFocus(c ->
      {
         reached.setText("Focus is on: " + caption);
         reached.setValidLayout(false);
         Screen.repaint();
      });
      body.add(LAY_AUTO, outgap(f, 0, 0, 0, .6f));
   }
}
For an AI assistant

Machine-readable versions of this page, so a model can read the shelf instead of guessing at the API.

This example, as JSONThis example, as Markdown
PreviousImagesNextWriting your own component