1 /*
   2  * Copyright (c) 2011, 2014, 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     @SuppressWarnings("serial") // anonymous class inside
 253     protected static JButton getCancelButton(final JTextComponent c) {
 254         final JButton b = createButton(c, getCancelIcon());
 255         b.setName("cancel");
 256 
 257         final Object cancelAction = c.getClientProperty(CANCEL_ACTION_KEY);
 258         if (cancelAction instanceof ActionListener) {
 259             b.addActionListener((ActionListener)cancelAction);
 260         }
 261 
 262         b.addActionListener(new AbstractAction("cancel") {
 263             public void actionPerformed(final ActionEvent e) {
 264                 c.setText("");
 265             }
 266         });
 267 
 268         c.getDocument().addDocumentListener(new DocumentListener() {
 269             public void changedUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 270             public void insertUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 271             public void removeUpdate(final DocumentEvent e) { updateCancelIcon(b, c); }
 272         });
 273 
 274         updateCancelIcon(b, c);
 275         return b;
 276     }
 277 
 278     // <rdar://problem/6444328> JTextField.variant=search: not thread-safe
 279     static void updateCancelIcon(final JButton button, final JTextComponent text) {
 280         if (SwingUtilities.isEventDispatchThread()) {
 281             updateCancelIconOnEDT(button, text);
 282         } else {
 283             SwingUtilities.invokeLater(new Runnable() {
 284                 public void run() { updateCancelIconOnEDT(button, text); }
 285             });
 286         }
 287     }
 288 
 289     static void updateCancelIconOnEDT(final JButton button, final JTextComponent text) {
 290         button.setVisible(!"".equals(text.getText()));
 291     }
 292 
 293     // subclass of normal text border, because we still want all the normal text field behaviors
 294     static class SearchFieldBorder extends AquaTextFieldBorder implements JComponentPainter {
 295         protected boolean reallyPaintBorder;
 296 
 297         public SearchFieldBorder() {
 298             super(new SizeDescriptor(new SizeVariant().alterMargins(6, 31, 6, 24).alterInsets(3, 3, 3, 3)));
 299             painter.state.set(Widget.FRAME_TEXT_FIELD_ROUND);
 300         }
 301 
 302         public SearchFieldBorder(final SearchFieldBorder other) {
 303             super(other);
 304         }
 305 
 306         public void paint(final JComponent c, final Graphics g, final int x, final int y, final int w, final int h) {
 307             reallyPaintBorder = true;
 308             paintBorder(c, g, x, y, w, h);
 309             reallyPaintBorder = false;
 310         }
 311 
 312         // apparently without adjusting for odd height pixels, the search field "wobbles" relative to it's contents
 313         public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
 314             if (!reallyPaintBorder) return;
 315             super.paintBorder(c, g, x, y - (height % 2), width, height);
 316         }
 317 
 318         public Insets getBorderInsets(final Component c) {
 319             if (doingLayout) return new Insets(0, 0, 0, 0);
 320 
 321             if (!hasPopupMenu((JTextComponent)c)) {
 322                 return new Insets(sizeVariant.margins.top, sizeVariant.margins.left - 7, sizeVariant.margins.bottom, sizeVariant.margins.right);
 323             }
 324 
 325             return sizeVariant.margins;
 326         }
 327 
 328         protected boolean doingLayout;
 329         @SuppressWarnings("serial") // anonymous class inside
 330         protected LayoutManager getCustomLayout() {
 331             // unfortunately, the default behavior of BorderLayout, which accommodates for margins
 332             // is not what we want, so we "turn off margins" for layout for layout out our buttons
 333             return new BorderLayout(0, 0) {
 334                 public void layoutContainer(final Container target) {
 335                     doingLayout = true;
 336                     super.layoutContainer(target);
 337                     doingLayout = false;
 338                 }
 339             };
 340         }
 341     }
 342 }