/*
* Phon - An open source tool for research in phonology.
* Copyright (C) 2008 The Phon Project, Memorial University <http://phon.ling.mun.ca>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph.app.components;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
/**
* A search field with optional context button. The field displays a prompt
* when the text field text is empty.
*/
public class SearchField extends JTextField {
/** Search icon size */
private static final int ICON_SIZE = 16;
/** Popup menu used for context item */
private JPopupMenu contextPopup;
/**
* Text field state
*/
public static enum FieldState {
/** Undefined state */
UNDEFINED(Color.RED),
/** Prompt (no input) */
PROMPT(Color.GRAY),
/** Regular input state */
INPUT(SystemColor.textText);
private Color color;
FieldState(Color color) {
this.color = color;
}
/**
* Gets the color used for this state.
*
* @return the color
*/
public Color getColor() {
return color;
}
};
private final static String STATE_PROPERTY = "_search_field_state_";
/**
* Current state
*/
private FieldState fieldState = FieldState.UNDEFINED;
/**
* Search field prompt
*/
private String prompt;
/**
* Search context button
*/
private SearchFieldButton ctxButton;
private SearchFieldButton endButton;
/**
* Constructs a search field with a default prompt.
*/
public SearchField() {
this("Search");
}
/**
* Constructs a search field with a specified prompt.
*
* @param prompt the prompt
*/
public SearchField(String prompt) {
init();
this.prompt = prompt;
setState(FieldState.PROMPT);
addFocusListener(focusStateListener);
}
@Override
public Dimension getPreferredSize() {
Dimension retVal = super.getPreferredSize();
retVal.height = Math.max(retVal.height, 25);
return retVal;
}
private BufferedImage clearIcn = null;
private BufferedImage createClearIcon() {
if(clearIcn == null) {
clearIcn = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D)clearIcn.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Ellipse2D circle = new Ellipse2D.Float(2, 2, ICON_SIZE - 2, ICON_SIZE - 2);
g2d.setColor(FieldState.PROMPT.getColor());
g2d.fill(circle);
Stroke s = new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
g2d.setStroke(s);
g2d.setColor(Color.WHITE);
g2d.drawLine(6, 6, ICON_SIZE - 5, ICON_SIZE - 5);
g2d.drawLine(ICON_SIZE - 5, 6, 6, ICON_SIZE - 5);
}
return clearIcn;
}
private BufferedImage searchIcn = null;
private BufferedImage createSearchIcon() {
if(searchIcn == null) {
BufferedImage retVal = new BufferedImage(ICON_SIZE + 8, ICON_SIZE,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D)retVal.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Ellipse2D circle = new Ellipse2D.Float(2, 2,
10, 10);
Line2D stem = new Line2D.Float(11, 11, ICON_SIZE - 2, ICON_SIZE - 2);
Polygon tri = new Polygon();
tri.addPoint(16, 8);
tri.addPoint(24, 8);
tri.addPoint(20, 12);
// Line2D triA = new Line2D.Float(14.0f, 9.0f, 17.0f, 9.0f);
// Line2D triB = new Line2D.Float(17.0f, 9.0f, 15.5f, 11.0f);
// Line2D triC = new Line2D.Float(15.5f, 11.0f, 14.0f, 9.0f);
Stroke s = new BasicStroke(2.3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
g2d.setStroke(s);
g2d.setColor(FieldState.PROMPT.getColor());
g2d.draw(circle);
g2d.draw(stem);
g2d.fillPolygon(tri);
// s = new BasicStroke(0.5f);
// g2d.setStroke(s);
//
// g2d.draw(triA);
// g2d.draw(triB);
// g2d.draw(triC);
searchIcn = retVal;
}
return searchIcn;
}
private void init() {
// load search icon
searchIcn = createSearchIcon();
final int borderInset = 10;
ctxButton = new SearchFieldButton(SwingConstants.LEFT, createSearchIcon());
ctxButton.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if(contextPopup != null) {
final int x = ctxButton.getX();
final int y = ctxButton.getY() + ctxButton.getHeight();
contextPopup.show(SearchField.this, x, y);
}
}
});
ctxButton.setCursor(Cursor.getDefaultCursor());
super.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
ctxButton.setBounds(0, 0, searchIcn.getWidth() + borderInset, getHeight());
endButton.setBounds(getWidth() - (ICON_SIZE + borderInset), 0, ICON_SIZE + borderInset, getHeight());
}
});
add(ctxButton);
endButton = new SearchFieldButton(SwingConstants.RIGHT, null);
endButton.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setText("");
}
});
endButton.setCursor(Cursor.getDefaultCursor());
add(endButton);
// setup an empty border allowing for the
// extra space needed for drawing
final int leftSpace = searchIcn.getWidth() + borderInset;
final int rightSpace = ICON_SIZE + borderInset;
final int topSpace = 0;
final int btmSpace = 0;
final Border emptyBorder = BorderFactory.createEmptyBorder(topSpace, leftSpace, btmSpace, rightSpace);
final Border matteBorder = BorderFactory.createMatteBorder(1, 0, 1, 0, FieldState.PROMPT.getColor());
setBorder(BorderFactory.createCompoundBorder(emptyBorder, matteBorder));
setBackground(Color.WHITE);
setOpaque(false);
}
/**
* Sets the popup menu used for the context button.
*
* @param contextPopup the popup menu, or <code>null</code> if no popup
* should be used
*/
public void setContextPopup(JPopupMenu contextPopup) {
this.contextPopup = contextPopup;
}
/**
* Gets the popup menu used for the context button.
*
* @return the popup menu, or <code>null</code> if one is not set
*/
public JPopupMenu getContextPopup() {
return contextPopup;
}
@Override
public String getText() {
String retVal = super.getText();
if(this.fieldState == FieldState.PROMPT) {
retVal = "";
}
return retVal;
}
@Override
public void setText(String s) {
if(s == null) s = "";
super.setText(s);
if(hasFocus() && s.length() > 0) {
setState(FieldState.INPUT);
} else if(s.length() == 0) {
setState(FieldState.PROMPT);
}
}
/**
* Gets the prompt text for the search field.
*
* @return the prompt text
*/
public String getPrompt() {
return prompt;
}
/**
* Sets the prompt text for the search field.
*
* @param prompt the prompt text
*/
public void setPrompt(String prompt) {
this.prompt = prompt;
if(getState() == FieldState.PROMPT)
super.setText(prompt);
}
/**
* Set state of field
*
* @param state the new state for this field
*/
public void setState(FieldState state) {
if(this.fieldState == state) return;
FieldState oldState = this.fieldState;
this.fieldState = state;
if(this.fieldState == FieldState.PROMPT) {
if(oldState == FieldState.INPUT && super.getText().length() > 0)
throw new IllegalStateException("Cannot set state to PROMPT when field has input.");
super.setForeground(this.fieldState.getColor());
super.setText(prompt);
endButton.setIcn(null);
endButton.setEnabled(false);
} else if(this.fieldState == FieldState.INPUT) {
super.setForeground(this.fieldState.getColor());
super.setText("");
endButton.setIcn(createClearIcon());
endButton.setEnabled(true);
}
super.firePropertyChange(STATE_PROPERTY, oldState, this.fieldState);
}
/**
* Gets the current state.
*
* @return the state
*/
public FieldState getState() {
return this.fieldState;
}
/**
* State change on focus
*
*/
private static FocusListener focusStateListener = new FocusListener() {
@Override
public void focusGained(FocusEvent arg0) {
SearchField sf = (SearchField)arg0.getSource();
if(sf.fieldState == FieldState.PROMPT) {
sf.setState(FieldState.INPUT);
}
}
@Override
public void focusLost(FocusEvent arg0) {
SearchField sf = (SearchField)arg0.getSource();
if(sf.getText().length()==0) {
sf.setState(FieldState.PROMPT);
}
}
};
/**
* Custom shaped button for the search field
*/
private class SearchFieldButton extends JButton {
private int side = SwingConstants.LEFT;
private Image icn = null;
public SearchFieldButton(int side, Image icn) {
this.side = side;
this.icn = icn;
super.setOpaque(false);
}
public void setIcn(Image icn) {
this.icn = icn;
}
@Override
protected void paintComponent(Graphics g) {
// setup graphics context
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// create button shape
int w = super.getWidth();
int h = super.getHeight();
Area btnShape = new Area();
if(side == SwingConstants.LEFT) {
// Shape circle = new Ellipse2D.Float(1, 0, h-1.0f, h-1.0f);
Shape roundRect = new RoundRectangle2D.Float(1.0f, 0.0f, w*2, h-1, h, h);
// Shape square = new Rectangle2D.Float(h/2.0f+1.0f, 0.0f, w-(h/2.0f)+1, h-1.0f);
btnShape.add(new Area(roundRect));
// btnShape.add(new Area(square));
} else if(side == SwingConstants.RIGHT) {
Shape roundRect = new RoundRectangle2D.Float(-w, 0.0f, w*2-1, h-1, h, h);
// Shape square = new Rectangle2D.Float(0.0f, 0.0f, w/2, h-1.0f);
btnShape.add(new Area(roundRect));
// btnShape.add(new Area(square));
}
// GradientPaint gp = new GradientPaint(new Point(0,0), new Color(215, 215, 215),
// new Point(0, h), new Color(200, 200, 200));
// g2d.setColor(gp);
// g2d.setPaint(gp);
g2d.setColor(SearchField.this.getBackground());
g2d.fill(btnShape);
// there is sometimes a single pixel artifact left
// over from the shape intersection. fix this
if(side == SwingConstants.LEFT) {
g2d.fillRect(h/2, 1, 2, h-1);
} else if(side == SwingConstants.RIGHT) {
g2d.fillRect(getWidth()-(h/2)-1, 1, 2, h-1);
}
g2d.setColor(FieldState.PROMPT.getColor());
g2d.draw(btnShape);
if(icn != null ) {
int btnY = h/2 - icn.getHeight(this)/2;
int btnX = w/2 - icn.getWidth(this)/2;
g2d.drawImage(icn, btnX, btnY, null);
}
// Rectangle2D rectToRemove = new Rectangle2D.Float(0, h/2, w, h/2);
// Area areaToRemove = new Area(rectToRemove);
// Area topArea = (Area)btnShape.clone();
// topArea.subtract(areaToRemove);
// gp = new GradientPaint(new Point(0,0), new Color(255, 255, 255, 75),
// new Point(0, h/2), new Color(255, 255, 255, 25));
// g2d.setPaint(gp);
// g2d.fill(topArea);
}
}
}