Saturday, December 26, 2009

Survey time! Please take a minute to comment.

This time last year I merged all my blog projects into one codebase, and I moved that codebase to a dev.java.net project. I couldn't be happier: this was a great move. It's backed up, it's easy to check out the workspace on other machines, etc.

Also I automated the process of building/updating the jars: one java app called BlogUpdater combs through the workspace and creates a jar for every java file with a main() method. As more and more projects share small common files this really helped me keep things up-to-date.

Help Me Help You


Now it's that time of year again where I want to step back and look at how I do what I do. What can be improved? Both for me and for you. Any feedback is welcome. Feel free to email me or post it here as a comment.

Specific conversation starters could -- but don't have to -- include:

  • Currently I list every blog article in a list on the left. Is this a good format? There are over 40 things in this list. By this time next year: there may be 50 or 60? Here is a similar repository that follows a very different format. Also Ken's blog is much less cluttered. Thoughts?
  • What subjects would you like to hear about? Writing these things up takes time... if I can cater to what you're interested in it will be a win-win for both of us.
  • In a given project: what do you want to hear about? I don't have a fixed template: so far I've just rambled about whatever I think is interesting. This might be performance, how to render something, architecture, etc.
  • On a bigger, more abstract scale: does anyone have any suggestions for other ways to focus this time & energy? Maybe I could join forces with other bloggers? (Who did you have in mind?) Or I could work on specific open-source java projects my skill set might lend itself to? etc. If a great suggestion comes up: I might just freeze this blog the way it is and pick up that other project.
  • Does it bother anyone that I don't have a solid versioning system? (That is: if you use the ColorPicker project, there is no version number associated with it.)
  • In the articles would you like more code samples? More screenshots? More diagrams?
  • Does anything about my programming style prevent you from integrating my jars into your own projects? Anything in the java code that you'd like to see changed? Don't be shy!

  • I recently sketched out a list of big changes I wanted to make to my blog and its presentation; but then I decided it might be much more useful to just ask you, my readers, what you think first...

    p.s. if you like things just the way they are, and would like to see more of "the same" articles: please say that too. The only way I'll gauge the majority opinion is if you speak up. :)

    Thanks for reading, and thanks for the 9,000+ hits I've received this year. (Give or take a few thousand hits that might just be search engine bots?)

    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.

    Sunday, December 6, 2009

    Text: Searching JTextComponents

    This entry has to do with searching for a phrase in JTextComponents. There's a lot of ground to cover, so I'll start you off with the demo and then go into details. (Give this applet a few seconds to load... it has a lot of text to read.)


    This jar (source included) is available here.

    Text Search Bars


    What is the point of a search bar? This is an interface decision more than a technical one. A search bar has several advantages over a search dialog:
    1. A dialog will physically be in the wrong location. It will start out centered, which probably covers part of the text the user wants to search.
    2. Providing a search bar in a fixed location will help users visually scan the interface more easily. They'll know where to look. It's a matter of identifying a fixed point vs searching for it.
    3. Dialogs are usually (and perhaps originally) designed for the convenience of programmers. You can cram a program full of thousands of dialogs... but the screen real estate is unlimited. It is much harder to create a well-balanced window with just the right amount of essential interface controls without becoming too cluttered. It takes more work, and we usually don't bother.
    4. Of course a modal search dialog is worse. Modal dialogs are discouraged here, among other places.

    I modeled these search bars after the ones I saw in Firefox and Safari. They include:

    • a search field
    • next/previous buttons
    • a checkbox for matching case
    • a dismiss button
    • a label including the occurrences of the search phrase
    • a toggle button for "Highlight All"

    The order (and presence) varies, but those are the basics. Safari automatically highlights all, and Firefox uses a toggle button. The anchor varies in both toolbars, but the components are still listed in about that order.

    Text Search Dialog


    Of course sometimes a dialog might be appropriate: that's up to you to decide. If searching is a feature so rarely used that it doesn't make sense to keep it in the main interface: that's justifiable. Or if your software is so versatile that searching is nothing but an auxiliary function? That's OK too.

    So I included a minimal dialog. It automatically performs case insensitive, wrap-around searches. It's not modal, but it does dismiss itself when the user hits the return key.

    No matter which model you use (a search dialog or a search bar), you probably want to also include typical menu shortcuts for find, find again, and find previous.

    Searching Text


    Most of the rest of this project was GUI work: so I was in my prime. The actual methods that do the searching/counting, though... I admit I'm pretty clueless about.

    My first instinct was to convert the document into a String, and then use .indexOf() and .lastIndexOf() to search for everything. Now I have absolutely no proof for what I'm about to say, but that sure feels like a horrible approach. Especially when we have to convert the entire string to a specific case to avoid case sensitivity.

    So instead I went spelunking around, and tried using the Segment class. Now the method is much hairier, but it walks through chunks of characters at a time to find the search phrase: at no time do I have an extra copy of the whole document floating around.

    (Remember when we count the occurrences of the search phrase: we perform a bajillion searches, so this method needs to be relatively light.)

    If anyone has any experience/suggestions on this subject, here is the code in question.

    The Visuals


    When you use the Safari-style search bar you're introducing two extra layers of visual effects into your JFrame:

    The middle layer is known as the TextHighlightSheet. The topmost layer is a SearchHighlight. These layers have a few of things in common:
    1. They are both added to the JLayeredPane.
    2. They both are tied to their "parent" JTextComponent. (It's not technically their parent component as far as the Swing hierarchy is concerned, but it's useful to think of them as bound to that text component.) When AncestorListeners or ComponentListeners are notified that the TextComponent has moved around: these other layers have to immediately respond.
    3. They're both animated. Or at least they can be. The middle sheet may fade in/out, and the highlight can have any number of customized animations.

    Now I'll discuss each in a little more detail.

    The TextHighlightSheet


    This is the more clever component: it has to highlight all the occurrences of the search phrase, without looking at irrelevant hits. So it will first calculate the index that is mapped to the point (0, 0) of the viewport, and then the index that is mapped to (width, height). Then search only within those indices, and calculate the shapes of all the hits.

    The hits are outlined in rectangles, and Area objects are used to join overlapping rectangles. Usually I avoid the Area class like the plague, but combined rectangles shouldn't be a hassle: they're small, and they contain only lines. Paint those rectangles white, and then paint everything around them in a translucent black (to add the overall shadow).

    Finally a combination of the right clipping, translation, and a TextOnlyGraphics2D let us call parentTextComponent.paint(g) to render the text.

    The Firefox search bar uses the same component as the Safari search bar. (Although in the Firefox bar you have to explicitly turn it on with the "Highlight All" button.) The Firefox sheet, however, doesn't have a translucent shadow. Also it uses different padding and colors. All of this is covered in the javadocs if you really want to customize it.

    The sheet has isActive() and setActive() methods to control whether it is visible. The TextSearchBar automatically follows one of two behaviors:

    1. If the "Highlight All" button is visible: then that button is the only factor controlling whether the sheet is active. This is how Firefox behaves.
    2. If the "Highlight All" button is invisible: then the sheet is made active only when the search field has the keyboard focus. This is how Safari behaves.

    SearchHighlight


    This is a very different component, and is subclassed directly into the look you're aiming for. The AbstractSearchHighlight subclasses into the AquaSearchHighlight, the FirefoxSearchHighlight, and the NudgeSearchHighlight.

    When constructed this component creates a BufferedImage of one particular hit of the search phrase. Then a javax.swing.Timer is activated, and this component is ready for action.

    The subclasses can animate the transform and opacity of the image over the duration of the animation.

    It is important that the sheet and the effect be designed to visually complement each other. For example: the highlight sheet for the Safari search bar is designed to work with the Aqua or the Nudge effects, but the Firefox effect uses smaller padding around the hit phrases.

    Also the Firefox effect cheats and uses an infinite duration, so the effect never really goes away. I'm not sure exactly what should happen in this scenario if the JTextComponent is editable? (Any thoughts?) I guess the effect should go away as soon you modify the document such that that particular hit phrase is changed? This is not an issue in the Safari/Nudge models, simply because the animation will quickly dispose of itself. So this is a bug in the Firefox effect, but I'm not sure what the ideal behavior is.

    Each effect is created by calling TextSearch.highlight(). This looks up the value for UIManager.get("textSearchHighlightEffect"), and instantiates the class name provided. I won't go into detail here, but the point is: you can customize this if you want. (You don't even have to extend the com.bric.plaf.AbstractSearchHighlight if you don't want to.)

    Conclusion


    In v1.0 of this project I just included the search bars and the effects. In v2.0 I added the middle sheet. If I get around to a v3.0: what next? (Aside from existing bugs.)

    Speaking of bugs: I suppose you also need to be able to perform a revised search with every key stroke in the search field. This is how Firefox behaves. This is slightly different from hitting the enter key in the text field, because that will search for the next occurrence starting at the end of the current selection... but continuous typing really needs a search that starts at the beginning of the current selection. Another day, maybe...