Monday, December 14, 2009

Text: Prompts and Search Fields

Here's what I want to replicate:


The concept of a text field is nothing new, but this undertaking requires two other components:

  1. A TextFieldUI with rounded corners, and a search icon.
  2. Prompting: gray text prompting the user with instructions or other useful information.

Apple's Solution


It's elegant. It's simple:
textField.putClientProperty( "JTextField.variant", "search");
textField.putClientProperty( "JTextField.Search.Prompt", prompt);

The first line is well documented here. The second line is only casually mentioned here.

It's a great solution -- if you're only trying to support Macs. What I want is a cross-platform solution. This is the same problem that triggered my new ButtonUI's, and an indeterminate progress widget. If I may get on my soap box for a minute: some of the client properties Apple's Java team implements are really not a good use of their time. To be fair: some properties they implement are great (window opacity, types of title bars, window decorations, etc). Meanwhile there are JVM-crashing bugs, accessibility bugs, etc, that Apple's team can address but I cannot... so why implement properties like this? (end of rant.)

Creating a RoundTextFieldUI


So a pure-Java solution requires making a new TextFieldUI. This wasn't so hard, if you extend the BasicTextFieldUI. The key was figuring out I had to override the getVisibleEditorRect() method. This controls the rectangle the text appears in, so I can offset everything to draw the rounded rectangle.

Also by overriding modelToView() I was able to tweak the caret height (the x-position and width I didn't change).

Here is the class I came up with. It includes a client property "useSearchIcon" to render a magnifying glass icon.

That part was surprisingly simple. Next I wanted to deal with prompting.

Using XSwingX


This is a little project hosted at code.google.com. It has prompting built-in, and a very simple programming interface. But it was surprisingly tricky to put a demo together. It requires the SwingX package. But not the latest version: there are some incompatibilities in the latest version of SwingX integrated with XSwingX. We need version 0.9.2.

Here is a jar that includes all the necessary classes to get the job done. But if you start counting: that comes to 32 classes. Also if you start looking at the source code: the word "hack" keeps coming up. It doesn't seem like a great solution.

My Own Approach


I wanted to try something simple. Something done in one, maybe two classes. My first thought was to extend JTextField, and shuffle the color/text based on when the text field has the focus:

public class PromptTextField extends JTextField {
static FocusListener focusListener = new FocusListener() {
public void focusGained(FocusEvent e) {
PromptTextField src = (PromptTextField)e.getSource();
if(src.state==STATE_PROMPT) {
src.setState(STATE_NORMAL);
}
}

public void focusLost(FocusEvent e) {
PromptTextField src = (PromptTextField)e.getSource();
if(src.getText().length()==0)
src.setState(STATE_PROMPT);
}
};
private static final int STATE_UNDEFINED = 0;
private static final int STATE_NORMAL = 1;
private static final int STATE_PROMPT = 2;

private Color normalColor = SystemColor.textText;
protected Color promptColor = Color.gray;
protected String promptText;
private int state = STATE_UNDEFINED;

public PromptTextField(String text, String prompt, int columns) {
super(text, columns);
promptText = prompt;
normalColor = getForeground();
addFocusListener(focusListener);
if(text.length()==0) {
setState(STATE_PROMPT);
} else {
setState(STATE_NORMAL);
}
}

public String getText() {
if(state==STATE_PROMPT) {
return "";
}
return super.getText();
}

private void setState(int s) {
if(s==state)
return;
state = s;
if(s==STATE_PROMPT) {
if(state==STATE_NORMAL && getText().length()>0)
throw new IllegalArgumentException("the state should not be set to STATE_PROMPT if there is text already in the text field (\""+getText()+"\")");
super.setForeground(promptColor);
super.setText(promptText);
} else if(s==STATE_NORMAL) {
super.setForeground(normalColor);
super.setText("");
}
}

public void setText(String s) {
if(state==STATE_UNDEFINED) {
super.setText(s);
return;
}
if(s==null) s = "";
if(hasFocus() || s.length()>0) {
state = STATE_NORMAL;
super.setForeground(normalColor);
super.setText(s);
} else if(s.length()==0) {
state = STATE_PROMPT;
super.setForeground(promptColor);
super.setText(promptText);
}
}
}

I was impressed that, well, that this worked. It's very minimalist. However it's also not very good: it's an awkward hack that overrides the getText() and setText() methods. This means the getText() method might reflect something that has nothing in common with the data stored in getDocument().

Rob Camick's Approach


Who is Rob Camick? I don't know. But now that I found this article I'm going to pay closer attention. He talks about another model to achieve prompting: add a component over the text field.

It's simple. It's not a "hack," it's just plain old clever.

There's a problem, though: text placement. In my case -- because I'm using a TextFieldUI of my own creation -- it'll be hard to line up the prompted text exactly over the existing text.

... unless I use a TextOnlyGraphics2D and the same type of TextFieldUI.

public class TextFieldPrompt extends JTextField {
public TextFieldPrompt(JTextField parent,
Color promptColor,
String promptText) {
super(promptText);

if(promptColor==null)
promptColor = Color.gray;

parent.add(this);

setFocusable(false);
setEditable(false);
setForeground(promptColor);
setOpaque(false);

... install listeners & UI ...
}


public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
g2 = new TextOnlyGraphics2D( g2, null );
super.paint( g2 );
}

public boolean contains(int x, int y) {
return false;
}
}

Conclusion


Here is an applet demonstrating the final model:


This jar (source included) is available here.

No comments:

Post a Comment