001package org.jdesktop.swingx.plaf; 002 003import java.awt.Component; 004import java.awt.Container; 005import java.awt.Dimension; 006import java.awt.Insets; 007import java.awt.Rectangle; 008import java.awt.event.ActionEvent; 009import java.awt.event.ActionListener; 010import java.beans.PropertyChangeEvent; 011import java.beans.PropertyChangeListener; 012 013import javax.swing.Icon; 014import javax.swing.JButton; 015import javax.swing.JComponent; 016import javax.swing.SwingUtilities; 017import javax.swing.UIManager; 018import javax.swing.event.DocumentEvent; 019import javax.swing.event.DocumentListener; 020import javax.swing.plaf.TextUI; 021import javax.swing.plaf.UIResource; 022import javax.swing.text.Document; 023 024import org.jdesktop.swingx.JXSearchField; 025import org.jdesktop.swingx.JXSearchField.LayoutStyle; 026import org.jdesktop.swingx.prompt.BuddySupport; 027import org.jdesktop.swingx.search.NativeSearchFieldSupport; 028 029/** 030 * The default {@link JXSearchField} UI delegate. 031 * 032 * @author Peter Weishapl <petw@gmx.net> 033 * 034 */ 035public class SearchFieldUI extends BuddyTextFieldUI { 036 /** 037 * The search field that we're a UI delegate for. Initialized by the 038 * <code>installUI</code> method, and reset to null by 039 * <code>uninstallUI</code>. 040 * 041 * @see #installUI 042 * @see #uninstallUI 043 */ 044 protected JXSearchField searchField; 045 046 private Handler handler; 047 048 public static final Insets NO_INSETS = new Insets(0, 0, 0, 0); 049 050 public SearchFieldUI(TextUI delegate) { 051 super(delegate); 052 } 053 054 private Handler getHandler() { 055 if (handler == null) { 056 handler = new Handler(); 057 } 058 return handler; 059 } 060 061 /** 062 * Calls {@link #installDefaults()}, adds the search, clear and popup button 063 * to the search field and registers a {@link PropertyChangeListener} ad 064 * {@link DocumentListener} and an {@link ActionListener} on the popup 065 * button. 066 */ 067 @Override 068 public void installUI(JComponent c) { 069 searchField = (JXSearchField) c; 070 071 super.installUI(c); 072 073 installDefaults(); 074 layoutButtons(); 075 076 configureListeners(); 077 } 078 079 private void configureListeners() { 080 if (isNativeSearchField()) { 081 popupButton().removeActionListener(getHandler()); 082 searchField.removePropertyChangeListener(getHandler()); 083 } else { 084 popupButton().addActionListener(getHandler()); 085 searchField.addPropertyChangeListener(getHandler()); 086 } 087 088 // add support for instant search mode in any case. 089 searchField.getDocument().addDocumentListener(getHandler()); 090 } 091 092 private boolean isNativeSearchField() { 093 return NativeSearchFieldSupport.isNativeSearchField(searchField); 094 } 095 096 @Override 097 protected BuddyLayoutAndBorder createBuddyLayoutAndBorder() { 098 return new BuddyLayoutAndBorder() { 099 /** 100 * This does nothing, if the search field is rendered natively on 101 * Leopard. 102 */ 103 @Override 104 protected void replaceBorderIfNecessary() { 105 if (!isNativeSearchField()) { 106 super.replaceBorderIfNecessary(); 107 } 108 } 109 110 /** 111 * Return zero, when the search field is rendered natively on 112 * Leopard, to make painting work correctly. 113 */ 114 @Override 115 public Dimension preferredLayoutSize(Container parent) { 116 if (isNativeSearchField()) { 117 return new Dimension(); 118 } else { 119 return super.preferredLayoutSize(parent); 120 } 121 } 122 123 /** 124 * Prevent 'jumping' when text is entered: Include the clear button, 125 * when layout style is Mac. When layout style is Vista: Take the 126 * clear button's preferred width if its either greater than the 127 * search button's pref. width or greater than the popup button's 128 * pref. width when a popup menu is installed and not using a 129 * seperate popup button. 130 */ 131 @Override 132 public Insets getBorderInsets(Component c) { 133 Insets insets = super.getBorderInsets(c); 134 if (searchField != null && !isNativeSearchField()) { 135 if (isMacLayoutStyle()) { 136 if (!clearButton().isVisible()) { 137 insets.right += clearButton().getPreferredSize().width; 138 } 139 } else { 140 JButton refButton = popupButton(); 141 if (searchField.getFindPopupMenu() == null 142 ^ searchField.isUseSeperatePopupButton()) { 143 refButton = searchButton(); 144 } 145 146 int clearWidth = clearButton().getPreferredSize().width; 147 int refWidth = refButton.getPreferredSize().width; 148 int overSize = clearButton().isVisible() ? refWidth 149 - clearWidth : clearWidth - refWidth; 150 if (overSize > 0) { 151 insets.right += overSize; 152 } 153 } 154 155 } 156 return insets; 157 } 158 }; 159 } 160 161 private void layoutButtons() { 162 BuddySupport.removeAll(searchField); 163 164 if (isNativeSearchField()) { 165 return; 166 } 167 168 if (isMacLayoutStyle()) { 169 BuddySupport.addLeft(searchButton(), searchField); 170 } else { 171 BuddySupport.addRight(searchButton(), searchField); 172 } 173 174 BuddySupport.addRight(clearButton(), searchField); 175 176 if (usingSeperatePopupButton()) { 177 BuddySupport.addRight(BuddySupport.createGap(getPopupOffset()), 178 searchField); 179 } 180 181 if (usingSeperatePopupButton() || !isMacLayoutStyle()) { 182 BuddySupport.addRight(popupButton(), searchField); 183 } else { 184 BuddySupport.addLeft(popupButton(), searchField); 185 } 186 } 187 188 private boolean isMacLayoutStyle() { 189 return searchField.getLayoutStyle() == LayoutStyle.MAC; 190 } 191 192 /** 193 * Initialize the search fields various properties based on the 194 * corresponding "SearchField.*" properties from defaults table. The 195 * {@link JXSearchField}s layout is set to the value returned by 196 * <code>createLayout</code>. Also calls {@link #replaceBorderIfNecessary()} 197 * and {@link #updateButtons()}. This method is called by 198 * {@link #installUI(JComponent)}. 199 * 200 * @see #installUI 201 * @see #createLayout 202 * @see JXSearchField#customSetUIProperty(String, Object) 203 */ 204 protected void installDefaults() { 205 if (isNativeSearchField()) { 206 return; 207 } 208 209 if (UIManager.getBoolean("SearchField.useSeperatePopupButton")) { 210 searchField.customSetUIProperty("useSeperatePopupButton", 211 Boolean.TRUE); 212 } else { 213 searchField.customSetUIProperty("useSeperatePopupButton", 214 Boolean.FALSE); 215 } 216 217 searchField.customSetUIProperty("layoutStyle", UIManager 218 .get("SearchField.layoutStyle")); 219 searchField.customSetUIProperty("promptFontStyle", UIManager 220 .get("SearchField.promptFontStyle")); 221 222 if (shouldReplaceResource(searchField.getOuterMargin())) { 223 searchField.setOuterMargin(UIManager 224 .getInsets("SearchField.buttonMargin")); 225 } 226 227 updateButtons(); 228 229 if (shouldReplaceResource(clearButton().getIcon())) { 230 clearButton().setIcon(UIManager.getIcon("SearchField.clearIcon")); 231 } 232 if (shouldReplaceResource(clearButton().getPressedIcon())) { 233 clearButton().setPressedIcon( 234 UIManager.getIcon("SearchField.clearPressedIcon")); 235 } 236 if (shouldReplaceResource(clearButton().getRolloverIcon())) { 237 clearButton().setRolloverIcon( 238 UIManager.getIcon("SearchField.clearRolloverIcon")); 239 } 240 241 searchButton().setIcon( 242 getNewIcon(searchButton().getIcon(), "SearchField.icon")); 243 244 popupButton().setIcon( 245 getNewIcon(popupButton().getIcon(), "SearchField.popupIcon")); 246 popupButton().setRolloverIcon( 247 getNewIcon(popupButton().getRolloverIcon(), 248 "SearchField.popupRolloverIcon")); 249 popupButton().setPressedIcon( 250 getNewIcon(popupButton().getPressedIcon(), 251 "SearchField.popupPressedIcon")); 252 } 253 254 /** 255 * Removes all installed listeners, the layout and resets the search field 256 * original border and removes all children. 257 */ 258 @Override 259 public void uninstallUI(JComponent c) { 260 super.uninstallUI(c); 261 262 searchField.removePropertyChangeListener(getHandler()); 263 searchField.getDocument().removeDocumentListener(getHandler()); 264 popupButton().removeActionListener(getHandler()); 265 266 searchField.setLayout(null); 267 searchField.removeAll(); 268 searchField = null; 269 } 270 271 /** 272 * Returns true if <code>o</code> is <code>null</code> or of instance 273 * {@link UIResource}. 274 * 275 * @param o an object 276 * @return true if <code>o</code> is <code>null</code> or of instance 277 * {@link UIResource} 278 */ 279 protected boolean shouldReplaceResource(Object o) { 280 return o == null || o instanceof UIResource; 281 } 282 283 /** 284 * Convience method for only replacing icons if they have not been 285 * customized by the user. Returns the icon from the defaults table 286 * belonging to <code>resKey</code>, if 287 * {@link #shouldReplaceResource(Object)} with the <code>icon</code> as a 288 * parameter returns <code>true</code>. Otherwise returns <code>icon</code>. 289 * 290 * @param icon the current icon 291 * @param resKey the resource key identifying the default icon 292 * @return the new icon 293 */ 294 protected Icon getNewIcon(Icon icon, String resKey) { 295 Icon uiIcon = UIManager.getIcon(resKey); 296 if (shouldReplaceResource(icon)) { 297 return uiIcon; 298 } 299 return icon; 300 } 301 302 /** 303 * Convienence method. 304 * 305 * @see JXSearchField#getCancelButton() 306 * @return the clear button 307 */ 308 protected final JButton clearButton() { 309 return searchField.getCancelButton(); 310 } 311 312 /** 313 * Convienence method. 314 * 315 * @see JXSearchField#getFindButton() 316 * @return the search button 317 */ 318 protected final JButton searchButton() { 319 return searchField.getFindButton(); 320 } 321 322 /** 323 * Convienence method. 324 * 325 * @see JXSearchField#getPopupButton() 326 * @return the popup button 327 */ 328 protected final JButton popupButton() { 329 return searchField.getPopupButton(); 330 } 331 332 /** 333 * Returns <code>true</code> if 334 * {@link JXSearchField#isUseSeperatePopupButton()} is <code>true</code> and 335 * a search popup menu has been set. 336 * 337 * @return the popup button is used in addition to the search button 338 */ 339 public boolean usingSeperatePopupButton() { 340 return searchField.isUseSeperatePopupButton() 341 && searchField.getFindPopupMenu() != null; 342 } 343 344 /** 345 * Returns the number of pixels between the popup button and the clear (or 346 * search) button as specified in the default table by 347 * 'SearchField.popupOffset'. Returns 0 if 348 * {@link #usingSeperatePopupButton()} returns <code>false</code> 349 * 350 * @return number of pixels between the popup button and the clear (or 351 * search) button 352 */ 353 protected int getPopupOffset() { 354 if (usingSeperatePopupButton()) { 355 return UIManager.getInt("SearchField.popupOffset"); 356 } 357 return 0; 358 } 359 360 /** 361 * Sets the visibility of the search, clear and popup buttons depending on 362 * the search mode, layout stye, search text, search popup menu and the use 363 * of a seperate popup button. Also resets the search buttons pressed and 364 * rollover icons if the search field is in regular search mode or clears 365 * the icons when the search field is in instant search mode. 366 */ 367 protected void updateButtons() { 368 clearButton().setVisible( 369 (!searchField.isRegularSearchMode() || searchField 370 .isMacLayoutStyle()) 371 && hasText()); 372 373 boolean clearNotHere = (searchField.isMacLayoutStyle() || !clearButton() 374 .isVisible()); 375 376 searchButton() 377 .setVisible( 378 (searchField.getFindPopupMenu() == null || usingSeperatePopupButton()) 379 && clearNotHere); 380 popupButton().setVisible( 381 searchField.getFindPopupMenu() != null 382 && (clearNotHere || usingSeperatePopupButton())); 383 384 if (searchField.isRegularSearchMode()) { 385 searchButton().setRolloverIcon( 386 getNewIcon(searchButton().getRolloverIcon(), 387 "SearchField.rolloverIcon")); 388 searchButton().setPressedIcon( 389 getNewIcon(searchButton().getPressedIcon(), 390 "SearchField.pressedIcon")); 391 } else { 392 // no action, therefore no rollover icon. 393 if (shouldReplaceResource(searchButton().getRolloverIcon())) { 394 searchButton().setRolloverIcon(null); 395 } 396 if (shouldReplaceResource(searchButton().getPressedIcon())) { 397 searchButton().setPressedIcon(null); 398 } 399 } 400 } 401 402 private boolean hasText() { 403 return searchField.getText() != null 404 && searchField.getText().length() > 0; 405 } 406 407 class Handler implements PropertyChangeListener, ActionListener, 408 DocumentListener { 409 @Override 410 public void propertyChange(PropertyChangeEvent evt) { 411 String prop = evt.getPropertyName(); 412 Object src = evt.getSource(); 413 414 if (src.equals(searchField)) { 415 if ("findPopupMenu".equals(prop) || "searchMode".equals(prop) 416 || "useSeperatePopupButton".equals(prop) 417 || "searchMode".equals(prop) 418 || "layoutStyle".equals(prop)) { 419 layoutButtons(); 420 updateButtons(); 421 } else if ("document".equals(prop)) { 422 Document doc = (Document) evt.getOldValue(); 423 if (doc != null) { 424 doc.removeDocumentListener(this); 425 } 426 doc = (Document) evt.getNewValue(); 427 if (doc != null) { 428 doc.addDocumentListener(this); 429 } 430 } 431 } 432 } 433 434 /** 435 * Shows the search popup menu, if installed. 436 */ 437 @Override 438 public void actionPerformed(ActionEvent e) { 439 if (searchField.getFindPopupMenu() != null) { 440 Component src = SearchFieldAddon.SEARCH_FIELD_SOURCE 441 .equals(UIManager.getString("SearchField.popupSource")) ? searchField 442 : (Component) e.getSource(); 443 444 Rectangle r = SwingUtilities.getLocalBounds(src); 445 int popupWidth = searchField.getFindPopupMenu() 446 .getPreferredSize().width; 447 int x = searchField.isVistaLayoutStyle() 448 || usingSeperatePopupButton() ? r.x + r.width 449 - popupWidth : r.x; 450 searchField.getFindPopupMenu().show(src, x, r.y + r.height); 451 } 452 } 453 454 @Override 455 public void changedUpdate(DocumentEvent e) { 456 update(); 457 } 458 459 @Override 460 public void insertUpdate(DocumentEvent e) { 461 update(); 462 } 463 464 @Override 465 public void removeUpdate(DocumentEvent e) { 466 update(); 467 } 468 469 /** 470 * Called when the search text changes. Calls 471 * {@link JXSearchField#postActionEvent()} In instant search mode or 472 * starts the search field instant search timer if the instant search 473 * delay is greater 0. 474 */ 475 private void update() { 476 if (searchField.isInstantSearchMode()) { 477 searchField.getInstantSearchTimer().stop(); 478 // only use timer when delay greater 0. 479 if (searchField.getInstantSearchDelay() > 0) { 480 searchField.getInstantSearchTimer().setInitialDelay( 481 searchField.getInstantSearchDelay()); 482 searchField.getInstantSearchTimer().start(); 483 } else { 484 searchField.postActionEvent(); 485 } 486 } 487 488 updateButtons(); 489 } 490 } 491}