/*
* Copyright 2011 Luke Usherwood.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.ui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.net.URL;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonModel;
import javax.swing.DefaultButtonModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import net.bettyluke.swing.SwingUtils;
import net.bettyluke.tracinstant.data.SavedSearch;
import net.bettyluke.util.DocUtils;
public class SearchComboEditor extends JTextField {
private final static class GradientBox extends Box {
GradientBox() {
super(BoxLayout.Y_AXIS);
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Shape clip = g2.getClip();
int h = getHeight();
Color bg = getBackground();
g2.setPaint(new GradientPaint(0, 0, bg, 0, h, bg.darker()));
g2.fill(clip);
}
}
private class SavedSearchCallout extends CalloutOverlay {
private final JTextField shorthand = new JTextField();
private final JTextField desc = new JTextField();
public SavedSearchCallout(JFrame wind) {
super(wind, new GradientBox());
final Action doneAction = createDoneAction();
final Action cancelAction = createCancelAction();
final Action removeSearchAction = createRemoveSearchAction();
JButton removeStarButton = new JButton(removeSearchAction);
removeStarButton.setAlignmentX(0.0f);
Box buttonBox = Box.createHorizontalBox();
buttonBox.add(Box.createHorizontalGlue());
buttonBox.add(new JButton(doneAction));
buttonBox.add(Box.createHorizontalStrut(6));
buttonBox.add(new JButton(cancelAction));
Box box = (GradientBox) getContent();
box.add(removeStarButton);
box.add(Box.createVerticalStrut(6));
box.add(new JLabel("Description:"));
box.add(desc);
box.add(Box.createVerticalStrut(6));
box.add(new JLabel("Shorthand:"));
box.add(shorthand);
box.add(Box.createVerticalStrut(6));
box.add(buttonBox);
box.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
box.setBackground(new Color(220, 230, 250));
box.setOpaque(true);
for (Component comp : box.getComponents()) {
((JComponent) comp).setAlignmentX(0f);
}
populateSavedSearchFields();
addAncestorAcceleratorKey(box, doneAction, KeyEvent.VK_ENTER);
addAncestorAcceleratorKey(box, cancelAction, KeyEvent.VK_ESCAPE);
removeSearchAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
// Set the action for (e.g.) when the user clicks outside the callout.
addDismissListener(doneAction);
}
@Override
public void showAt(int x, int y) {
super.showAt(x, y);
SwingUtilities.invokeLater(() -> desc.requestFocusInWindow());
}
private AbstractAction createDoneAction() {
return new AbstractAction("Done") {
@Override
public void actionPerformed(ActionEvent e) {
applyChanges();
}
};
}
private Action createCancelAction() {
return new AbstractAction("Cancel") {
@Override
public void actionPerformed(ActionEvent e) {
dismiss();
}
};
}
private Action createRemoveSearchAction() {
return new AbstractAction("Remove saved search") {
@Override
public void actionPerformed(ActionEvent e) {
clearSavedSearch();
dismiss();
}
};
}
private void populateSavedSearchFields() {
SavedSearch ss = comboModel.findSearch(getText());
shorthand.setText((ss == null || ss.alias == null) ? "" : ss.alias);
desc.setText((ss == null || ss.name == null) ? "" : ss.name);
}
private void addAncestorAcceleratorKey(
JComponent ancestor, Action action, int keyCode)
{
ancestor.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke(keyCode, 0),
action.getValue(Action.NAME));
ancestor.getActionMap().put(action.getValue(Action.NAME), action);
}
protected void applyChanges() {
SavedSearch ss = new SavedSearch(getText(), shorthand.getText(), desc.getText());
comboModel.updateSearch(ss);
dismiss();
}
@Override
public void dismiss() {
super.dismiss();
getParent().requestFocusInWindow();
}
}
private static final Color MID_SHADOW = new Color(199, 202, 207);
private static final Color LIGHTER_SHADOW = new Color(203, 203, 204);
private static final Color DARK_BORDER = new Color(141, 142, 143);
private static enum StarIcons {
NORMAL("res/star_grey.png"), SELECTED("res/star_yellow.png"), ROLLOVER(
"res/star_grey_roll.png"), SELECTED_ROLLOVER(
"res/star_yellow_roll.png"), PRESSED("res/star_yellow_pressed.png");
private ImageIcon icon;
private StarIcons(String resourcePath) {
icon = createImageIcon(resourcePath);
}
/** Returns an ImageIcon, or null if the path was invalid. */
protected static ImageIcon createImageIcon(String path) {
URL imgURL = SearchComboEditor.class.getResource(path);
return new ImageIcon(imgURL);
}
public Icon getIcon() {
return icon;
}
}
private static final int iconWidth;
private static final int iconHeight;
static {
Icon normal = StarIcons.NORMAL.getIcon();
iconHeight = normal.getIconHeight();
iconWidth = normal.getIconWidth();
}
private class Listener implements MouseListener, MouseMotionListener {
private Boolean lastOverStar = null;
@Override
public void mouseMoved(MouseEvent e) {
Boolean overStar = isOverStar(e.getX(), e.getY());
starModel.setRollover(overStar);
// Avoid extra work updating cursor
if (!overStar.equals(lastOverStar)) {
lastOverStar = overStar;
int cursor = overStar ? Cursor.DEFAULT_CURSOR : Cursor.TEXT_CURSOR;
((Component) e.getSource()).setCursor(Cursor.getPredefinedCursor(cursor));
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (Boolean.TRUE.equals(lastOverStar) && !isOverStar(e.getX(), e.getY())) {
exitStar();
}
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
exitStar();
}
@Override
public void mousePressed(MouseEvent e) {
if (isMouseEventRelevant(e)) {
starModel.setPressed(true);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isMouseEventRelevant(e) && starModel.isPressed()) {
if (starModel.isSelected()) {
showCallout();
} else {
quickSaveSearch();
}
}
starModel.setPressed(false);
}
@Override
public void mouseClicked(MouseEvent e) {
}
private boolean isMouseEventRelevant(MouseEvent e) {
return SwingUtilities.isLeftMouseButton(e) && isOverStar(e.getX(), e.getY());
}
private void exitStar() {
starModel.setRollover(false);
starModel.setPressed(false);
lastOverStar = null;
}
}
private ButtonModel starModel;
private final int extraLeft = 4;
private final int extraRight = 6 + iconWidth;
private SavedSearchCallout callout;
private final SearchComboBoxModel comboModel;
public SearchComboEditor(SearchComboBoxModel comboModel, String value, int n) {
super(value, n);
this.comboModel = comboModel;
// A crack an mimicking a Nimbus combo editor's border, kind of.
// See also paintComponent(...)
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEmptyBorder(2, 2, 2, 0),
BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, DARK_BORDER),
BorderFactory.createEmptyBorder(0, extraLeft, 0, extraRight))));
starModel = new DefaultButtonModel();
starModel.addChangeListener(e -> repaint());
getDocument().addDocumentListener(DocUtils.newOnAnyEventListener(this::updateStar));
Listener ml = new Listener();
addMouseListener(ml);
addMouseMotionListener(ml);
}
protected void updateStar() {
boolean found = comboModel.findSearch(getText()) != null;
starModel.setSelected(found);
}
public void quickSaveSearch() {
SavedSearch ss = new SavedSearch(getText());
comboModel.updateSearch(ss);
}
public void clearSavedSearch() {
SavedSearch ss = comboModel.findSearch(getText());
if (ss != null) {
comboModel.removeElement(ss);
}
comboModel.setSelectedItem(ss);
}
// workaround for 4530952
@Override
public void setText(String s) {
if (getText().equals(s)) {
return;
}
super.setText(s);
}
@Override
protected void paintComponent(Graphics g) {
Insets in = getInsets();
int y = in.top;
int left = in.left - extraLeft;
int right = getWidth() - in.right + extraRight;
int bottom = getHeight() - in.bottom - 1;
// All this faffing-around may be more reason to ditch Combo and use a simple
// text field
g.setColor(getBackground());
g.fillRect(right - 3, in.top, right, getHeight() - in.bottom - in.top);
super.paintComponent(g);
g.setColor(MID_SHADOW);
g.drawLine(left, y, right, y);
g.setColor(LIGHTER_SHADOW);
++y;
g.drawLine(left, y, right, y);
g.setColor(MID_SHADOW);
g.drawLine(left, in.top, left, bottom);
g.drawLine(left, bottom, right, bottom);
Icon star = getCurrentStarIcon();
star.paintIcon(this, g, getStarX(), getStarY());
}
private Icon getCurrentStarIcon() {
if (starModel.isPressed()) {
return StarIcons.PRESSED.icon;
}
SearchComboEditor.StarIcons st = starModel.isRollover() ?
(starModel.isSelected() ? StarIcons.SELECTED_ROLLOVER : StarIcons.ROLLOVER) :
(starModel.isSelected() ? StarIcons.SELECTED : StarIcons.NORMAL);
return st.getIcon();
}
public int getStarX() {
// The star is within the editor's border.
return getWidth() - getInsets().right + 1;
}
public int getStarY() {
Insets insets = getInsets();
int space = getHeight() - insets.top - insets.bottom - iconHeight;
return insets.top + space / 2;
}
public int getStarWidth() {
return iconWidth;
}
public int getStarHeight() {
return iconHeight;
}
public boolean isOverStar(int x, int y) {
// Y ignored: don't leave "cracks" that don't work above & below the star.
return x >= getStarX();
}
public void showCallout() {
if (callout != null) {
callout.dismiss();
}
JFrame wind = (JFrame) SwingUtils.getWindowForComponent(this);
Point pt = pointToShowCallout(wind);
callout = new SavedSearchCallout(wind);
callout.showAt(pt.x, pt.y);
}
private Point pointToShowCallout(JFrame wind) {
int x = getStarX() + getStarWidth() / 2;
int y = getStarY() + getStarHeight();
Point pt = SwingUtilities.convertPoint(this, x, y, wind.getContentPane());
return pt;
}
}