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 }