1 /*
   2  * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.apple.laf;
  27 
  28 import java.awt.*;
  29 import java.awt.event.*;
  30 import java.beans.*;
  31 
  32 import javax.swing.*;
  33 import javax.swing.event.*;
  34 import javax.swing.plaf.TextUI;
  35 import javax.swing.text.JTextComponent;
  36 
  37 import apple.laf.JRSUIConstants.*;
  38 
  39 import com.apple.laf.AquaIcon.DynamicallySizingJRSUIIcon;
  40 import com.apple.laf.AquaUtilControlSize.*;
  41 import com.apple.laf.AquaUtils.*;
  42 
  43 public class AquaTextFieldSearch {
  44     private static final String VARIANT_KEY = "JTextField.variant";
  45     private static final String SEARCH_VARIANT_VALUE = "search";
  46 
  47     private static final String FIND_POPUP_KEY = "JTextField.Search.FindPopup";
  48     private static final String FIND_ACTION_KEY = "JTextField.Search.FindAction";
  49     private static final String CANCEL_ACTION_KEY = "JTextField.Search.CancelAction";
  50     private static final String PROMPT_KEY = "JTextField.Search.Prompt";
  51 
  52     private static final SearchFieldPropertyListener SEARCH_FIELD_PROPERTY_LISTENER = new SearchFieldPropertyListener();
  53     protected static void installSearchFieldListener(final JTextComponent c) {
  54         c.addPropertyChangeListener(SEARCH_FIELD_PROPERTY_LISTENER);
  55     }
  56 
  57     protected static void uninstallSearchFieldListener(final JTextComponent c) {
  58         c.removePropertyChangeListener(SEARCH_FIELD_PROPERTY_LISTENER);
  59     }
  60 
  61     static class SearchFieldPropertyListener implements PropertyChangeListener {
  62         public void propertyChange(final PropertyChangeEvent evt) {
  63             final Object source = evt.getSource();
  64             if (!(source instanceof JTextComponent)) return;
  65 
  66             final String propertyName = evt.getPropertyName();
  67             if (!VARIANT_KEY.equals(propertyName) &&
  68                 !FIND_POPUP_KEY.equals(propertyName) &&
  69                 !FIND_ACTION_KEY.equals(propertyName) &&
  70                 !CANCEL_ACTION_KEY.equals(propertyName) &&
  71                 !PROMPT_KEY.equals(propertyName)) {
  72                 return;
  73             }
  74 
  75             final JTextComponent c = (JTextComponent)source;
  76             if (wantsToBeASearchField(c)) {
  77                 uninstallSearchField(c);
  78                 installSearchField(c);
  79             } else {
  80                 uninstallSearchField(c);
  81             }
  82         }
  83     }
  84 
  85     protected static boolean wantsToBeASearchField(final JTextComponent c) {
  86         return SEARCH_VARIANT_VALUE.equals(c.getClientProperty(VARIANT_KEY));
  87     }
  88 
  89     protected static boolean hasPopupMenu(final JTextComponent c) {
  90         return (c.getClientProperty(FIND_POPUP_KEY) instanceof JPopupMenu);
  91     }
  92 
  93     protected static final RecyclableSingleton<SearchFieldBorder> instance = new RecyclableSingletonFromDefaultConstructor<SearchFieldBorder>(SearchFieldBorder.class);
  94     public static SearchFieldBorder getSearchTextFieldBorder() {
  95         return instance.get();
  96     }
  97 
  98     protected static void installSearchField(final JTextComponent c) {
  99         final SearchFieldBorder border = getSearchTextFieldBorder();
 100         c.setBorder(border);
 101         c.setLayout(border.getCustomLayout());
 102         c.add(getFindButton(c), BorderLayout.WEST);
 103         c.add(getCancelButton(c), BorderLayout.EAST);
 104         c.add(getPromptLabel(c), BorderLayout.CENTER);
 105 
 106         final TextUI ui = c.getUI();
 107         if (ui instanceof AquaTextFieldUI) {
 108             ((AquaTextFieldUI)ui).setPaintingDelegate(border);
 109         }
 110     }
 111 
 112     protected static void uninstallSearchField(final JTextComponent c) {
 113         c.setBorder(UIManager.getBorder("TextField.border"));
 114         c.removeAll();
 115 
 116         final TextUI ui = c.getUI();
 117         if (ui instanceof AquaTextFieldUI) {
 118             ((AquaTextFieldUI)ui).setPaintingDelegate(null);
 119         }
 120     }
 121 
 122     // The "magnifying glass" icon that sometimes has a downward pointing triangle next to it
 123     // if a popup has been assigned to it. It does not appear to have a pressed state.
 124     protected static DynamicallySizingJRSUIIcon getFindIcon(final JTextComponent text) {
 125         return (text.getClientProperty(FIND_POPUP_KEY) == null) ?
 126             new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(25, 22).alterMargins(0, 4, 0, -5))) {
 127                 public void initJRSUIState() {
 128                     painter.state.set(Widget.BUTTON_SEARCH_FIELD_FIND);
 129                 }
 130             }
 131         :
 132             new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(25, 22).alterMargins(0, 4, 0, 2))) {
 133                 public void initJRSUIState() {
 134                     painter.state.set(Widget.BUTTON_SEARCH_FIELD_FIND);
 135                 }
 136             }
 137         ;
 138     }
 139 
 140     // The "X in a circle" that only shows up when there is text in the search field.
 141     protected static DynamicallySizingJRSUIIcon getCancelIcon() {
 142         return new DynamicallySizingJRSUIIcon(new SizeDescriptor(new SizeVariant(22, 22).alterMargins(0, 0, 0, 4))) {
 143             public void initJRSUIState() {
 144                 painter.state.set(Widget.BUTTON_SEARCH_FIELD_CANCEL);
 145             }
 146         };
 147     }
 148 
 149     protected static State getState(final JButton b) {
 150         if (!AquaFocusHandler.isActive(b)) return State.INACTIVE;
 151         if (b.getModel().isPressed()) return State.PRESSED;
 152         return State.ACTIVE;
 153     }
 154 
 155     protected static JButton createButton(final JTextComponent c, final DynamicallySizingJRSUIIcon icon) {
 156         final JButton b = new JButton()
 157 //        {
 158 //            public void paint(Graphics g) {
 159 //                super.paint(g);
 160 //
 161 //                g.setColor(Color.green);
 162 //                g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
 163 //            }
 164 //        }
 165         ;
 166 
 167         final Insets i = icon.sizeVariant.margins;
 168         b.setBorder(BorderFactory.createEmptyBorder(i.top, i.left, i.bottom, i.right));
 169 
 170         b.setIcon(icon);
 171         b.setBorderPainted(false);
 172         b.setFocusable(false);
 173         b.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
 174         b.addChangeListener(new ChangeListener() {
 175             public void stateChanged(final ChangeEvent e) {
 176                 icon.painter.state.set(getState(b));
 177             }
 178         });
 179         b.addMouseListener(new MouseAdapter() {
 180             public void mousePressed(final MouseEvent e) {
 181                 c.requestFocusInWindow();
 182             }
 183         });
 184 
 185         return b;
 186     }
 187 
 188     protected static JButton getFindButton(final JTextComponent c) {
 189         final DynamicallySizingJRSUIIcon findIcon = getFindIcon(c);
 190         final JButton b = createButton(c, findIcon);
 191         b.setName("find");
 192 
 193         final Object findPopup = c.getClientProperty(FIND_POPUP_KEY);
 194         if (findPopup instanceof JPopupMenu) {
 195             // if we have a popup, indicate that in the icon
 196             findIcon.painter.state.set(Variant.MENU_GLYPH);
 197 
 198             b.addMouseListener(new MouseAdapter() {
 199                 public void mousePressed(final MouseEvent e) {
 200                     ((JPopupMenu)findPopup).show(b, 8, b.getHeight() - 2);
 201                     c.requestFocusInWindow();
 202                     c.repaint();
 203                 }
 204             });
 205         }
 206 
 207         final Object findAction = c.getClientProperty(FIND_ACTION_KEY);
 208         if (findAction instanceof ActionListener) {
 209             b.addActionListener((ActionListener)findAction);
 210         }
 211 
 212         return b;
 213     }
 214 
 215     private static Component getPromptLabel(final JTextComponent c) {
 216         final JLabel label = new JLabel();
 217         label.setForeground(UIManager.getColor("TextField.inactiveForeground"));
 218 
 219         c.getDocument().addDocumentListener(new DocumentListener() {
 220             public void changedUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
 221             public void insertUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
 222             public void removeUpdate(final DocumentEvent e) { updatePromptLabel(label, c); }
 223         });
 224         c.addFocusListener(new FocusAdapter() {
 225             public void focusGained(final FocusEvent e) { updatePromptLabel(label, c); }
 226             public void focusLost(final FocusEvent e) { updatePromptLabel(label, c); }
 227         });
 228         updatePromptLabel(label, c);
 229 
 230         return label;
 231     }
 232 
 233     static void updatePromptLabel(final JLabel label, final JTextComponent text) {
 234         if (SwingUtilities.isEventDispatchThread()) {
 235             updatePromptLabelOnEDT(label, text);
 236         } else {
 237             SwingUtilities.invokeLater(new Runnable() {
 238                 public void run() { updatePromptLabelOnEDT(label, text); }
 239             });
 240         }
 241     }
 242 
 243     static void updatePromptLabelOnEDT(final JLabel label, final JTextComponent text) {
 244         String promptText = " ";
 245         if (!text.hasFocus() && "".equals(text.getText())) {
 246             final Object prompt = text.getClientProperty(PROMPT_KEY);
 247             if (prompt != null) promptText = prompt.toString();
 248         }
 249         label.setText(promptText);
 250     }
 251 
 252     protected static JButton getCancelButton(final JTextComponent c) {
 253         final JButton b = createButton(c, getCancelIcon());
 254         b.setName("cancel");
 255 
 256         final Object cancelAction = c.getClientProperty(CANCEL_ACTION_KEY);
 257         if (cancelAction instanceof ActionListener) {
 258             b.addActionListener((ActionListener)cancelAction);
 259         }
 260 
 261         b.addActionListener(new AbstractAction("cancel") {
 262             public void actionPerformed(final ActionEvent e) {
 263                 c.setText("");
 264             }
 265         });
 266 
 267         c.getDocument().addDocumentListener(new DocumentListener() {
 268             public void changedUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 269             public void insertUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 270             public void removeUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 271         });
 272 
 273         updateCancelIcon(b, c);
 274         return b;
 275     }
 276 
 277     // <rdar://problem/6444328> JTextField.variant=search: not thread-safe
 278     static void updateCancelIcon(final JButton button, final JTextComponent text) {
 279         if (SwingUtilities.isEventDispatchThread()) {
 280             updateCancelIconOnEDT(button, text);
 281         } else {
 282             SwingUtilities.invokeLater(new Runnable() {
 283                 public void run() { updateCancelIconOnEDT(button, text); }
 284             });
 285         }
 286     }
 287 
 288     static void updateCancelIconOnEDT(final JButton button, final JTextComponent text) {
 289         button.setVisible(!"".equals(text.getText()));
 290     }
 291 
 292     // subclass of normal text border, because we still want all the normal text field behaviors
 293     static class SearchFieldBorder extends AquaTextFieldBorder implements JComponentPainter {
 294         protected boolean reallyPaintBorder;
 295 
 296         public SearchFieldBorder() {
 297             super(new SizeDescriptor(new SizeVariant().alterMargins(6, 31, 6, 24).alterInsets(3, 3, 3, 3)));
 298             painter.state.set(Widget.FRAME_TEXT_FIELD_ROUND);
 299         }
 300 
 301         public SearchFieldBorder(final SearchFieldBorder other) {
 302             super(other);
 303         }
 304 
 305         public void paint(final JComponent c, final Graphics g, final int x, final int y, final int w, final int h) {
 306             reallyPaintBorder = true;
 307             paintBorder(c, g, x, y, w, h);
 308             reallyPaintBorder = false;
 309         }
 310 
 311         // apparently without adjusting for odd height pixels, the search field "wobbles" relative to it's contents
 312         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 313             if (!reallyPaintBorder) return;
 314             super.paintBorder(c, g, x, y - (height % 2), width, height);
 315         }
 316 
 317         public Insets getBorderInsets(final Component c) {
 318             if (doingLayout) return new Insets(0, 0, 0, 0);
 319 
 320             if (!hasPopupMenu((JTextComponent)c)) {
 321                 return new Insets(sizeVariant.margins.top, sizeVariant.margins.left - 7, sizeVariant.margins.bottom, sizeVariant.margins.right);
 322             }
 323 
 324             return sizeVariant.margins;
 325         }
 326 
 327         protected boolean doingLayout;
 328         protected LayoutManager getCustomLayout() {
 329             // unfortunately, the default behavior of BorderLayout, which accommodates for margins
 330             // is not what we want, so we "turn off margins" for layout for layout out our buttons
 331             return new BorderLayout(0, 0) {
 332                 public void layoutContainer(final Container target) {
 333                     doingLayout = true;
 334                     super.layoutContainer(target);
 335                     doingLayout = false;
 336                 }
 337             };
 338         }
 339     }
 340 }