package net.sourceforge.fidocadj.macropicker; import java.awt.*; import java.awt.event.*; import java.awt.font.LineMetrics; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; import net.sourceforge.fidocadj.globals.*; /** * A text field for search/filter interfaces. The extra functionality includes * a placeholder string (when the user hasn't yet typed anything), and a button * to clear the currently-entered text. // TODO: add a menu of recent searches. // TODO: make recent searches persistent. * * @author Elliott Hughes * http://elliotth.blogspot.com/2004/09/cocoa-like-search-field-for-java.html */ public class SearchField extends JTextField implements FocusListener { private static final Border CANCEL_BORDER = new CancelBorder(); private boolean sendsNotificationForEachKeystroke = false; private static final boolean showingPlaceholderText = false; private boolean armed = false; private String placeholderText; /** Constructor. @param placeholderText the text to be shown as a placeholder when a search is not being done. */ public SearchField(String placeholderText) { super(15); // putClientProperty("JTextField.variant", "search"); // putClientProperty("JTextField.Search.Prompt", placeholderText); putClientProperty("Quaqua.TextField.style", "search"); this.placeholderText = placeholderText; initBorder(); initKeyListener(); addFocusListener(this); } /** Standard constructor. The placeholder text will be "Search". */ public SearchField() { this("Search"); } /** We need to override the paintComponent method. For some reason, on MacOSX systems the background is not painted when the text field is rounded and appears like a standard search text field. This is quite embarassing when using an unified toolbar style like in Leopard and Snow Leopard. For this reason, here we paint the background if needed. @param g the graphic context to use. */ public void paintComponent(Graphics g) { if(Globals.weAreOnAMac) { // This is useful only on Macintosh, since the text field shown is // rounded. Rectangle r = getBounds(); int x = r.x + 4; int y = r.y + 4; int width = r.width - 8; int height = r.height - 8; g.setColor(getBackground()); g.fillOval(x - 2, y, height, height); g.fillOval(x + width - height + 2, y, height, height); g.fillRect(x + height / 2, y, width - height, height); } // Once the new background is drawn, we can proceed with the rest of // the component. super.paintComponent(g); // At previous code, this document model had returned placeholder text // when waiting focus. // The model must be return zero length string in the situation. showPlaceHolder(g); } /** * Draws placeholder text. */ private void showPlaceHolder(Graphics g) { // It works fine on Windows. // Other environment have not been tested yet. int left, bottom; float fontHeight; Font f; LineMetrics lm; // Get font height. f = g.getFont(); lm = f.getLineMetrics(placeholderText, g.getFontMetrics().getFontRenderContext()); fontHeight = lm.getHeight(); // Calculate text position. left = getBorder().getBorderInsets(this).left; bottom = (int) (getHeight() / 2.0 + fontHeight / 2.0); // Show placeholder text when focused. if (!isFocusOwner() && getText().length() == 0) { g.setColor(Color.GRAY); g.drawString(placeholderText, left, bottom); } } private void initBorder() { setBorder(new CompoundBorder(getBorder(), CANCEL_BORDER)); //getBorder().setOpaque(true); //getContentPane().setOpaque(true); MouseInputListener mouseInputListener = new CancelListener(); addMouseListener(mouseInputListener); addMouseMotionListener(mouseInputListener); setMaximumSize(new Dimension(5000, 30)); } /** Add a key listener */ private void initKeyListener() { addKeyListener(new KeyAdapter() { public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { cancel(); } else if (sendsNotificationForEachKeystroke) { maybeNotify(); } } public void keyPressed(KeyEvent e) { // If the search field has the focus, it will be te only // recipient of the key strokes (solves bug #50). // Do this only for R and S keys. // About this bug. // The top component takes all key events regardless of focus??? if(isFocusOwner() && (e.getKeyCode() == KeyEvent.VK_R || e.getKeyCode() == KeyEvent.VK_S)) { e.consume(); } } }); } private void cancel() { setText(""); postActionEvent(); } private void maybeNotify() { if (showingPlaceholderText) { return; } postActionEvent(); } /** Edit the send notification for each keystroke. @param eachKeystroke true if the property should be active. */ public void setSendsNotificationForEachKeystroke(boolean eachKeystroke) { this.sendsNotificationForEachKeystroke = eachKeystroke; } /** Draw the cancel button as a gray circle with a white cross inside. */ static class CancelBorder extends EmptyBorder { private static final Color GRAY = new Color(0.7f, 0.7f, 0.7f); /** Standard constructor. */ CancelBorder() { super(0, 20, 0, 15); } /** Paint the border of the button. @param c the component. @param gc the graphic context. @param x the x coordinate of the left side of the button. @param y the y coordinate of the top side of the button. @param width the width of the button. @param height the height of the button. */ public void paintBorder(Component c, Graphics gc, int x, int y, int width, int height) { SearchField field = (SearchField) c; Graphics2D g = (Graphics2D) gc; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); final int circleL = 14; final int lensX = x; final int lensY = y; final int lensL = 12; super.paintBorder(c, gc, x, y, width, height); g.setColor(Color.GRAY); g.fillOval(lensX, lensY, lensL, lensL); g.setStroke(new BasicStroke(3)); g.drawLine(lensX + lensL / 2, lensY + lensL / 2, lensX + circleL, lensY + circleL); g.setStroke(new BasicStroke(1)); g.setColor(Color.WHITE); g.fillOval(lensX + 2, lensY + 2, lensL - 4, lensL - 4); g.setColor(field.armed ? Color.GRAY : GRAY); if (field.showingPlaceholderText || field.getText().length() == 0) { return; } final int circleX = x + width - circleL; final int circleY = y + (height - 1 - circleL) / 2; g.setColor(field.armed ? Color.GRAY : GRAY); g.fillOval(circleX, circleY, circleL, circleL); final int lineL = circleL - 8; final int lineX = circleX + 4; final int lineY = circleY + 4; g.setColor(Color.WHITE); g.drawLine(lineX, lineY, lineX + lineL, lineY + lineL); g.drawLine(lineX, lineY + lineL, lineX + lineL, lineY); } } /** Handles a click on the cancel button by clearing the text and notifying any ActionListeners. */ class CancelListener extends MouseInputAdapter { private boolean isOverButton(MouseEvent e) { // If the button is down, we might be outside the component // without having had mouseExited invoked. if (!contains(e.getPoint())) { return false; } // In lieu of proper hit-testing for the circle, check that // the mouse is somewhere in the border. Rectangle innerArea = SwingUtilities.calculateInnerArea(SearchField.this, null); return !innerArea.contains(e.getPoint()); } public void mouseDragged(MouseEvent e) { arm(e); } public void mouseEntered(MouseEvent e) { arm(e); } public void mouseExited(MouseEvent e) { disarm(); } public void mousePressed(MouseEvent e) { arm(e); } public void mouseReleased(MouseEvent e) { if (armed) { cancel(); } disarm(); } private void arm(MouseEvent e) { armed = isOverButton(e) && SwingUtilities.isLeftMouseButton(e); repaint(); } private void disarm() { armed = false; repaint(); } } /** For the FocusListener interface. The field gained focus. @param e the focus event. */ public void focusGained(FocusEvent e) { repaint(); } /** For the FocusListener interface. The field lost focus. @param e the focus event. */ public void focusLost(FocusEvent e) { repaint(); } }