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 }