001/* 002 * $Id: SearchFactory.java 3927 2011-02-22 16:34:11Z kleopatra $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021package org.jdesktop.swingx.search; 022 023import java.awt.Component; 024import java.awt.Container; 025import java.awt.Dialog; 026import java.awt.Frame; 027import java.awt.KeyboardFocusManager; 028import java.awt.Point; 029import java.awt.Window; 030import java.awt.event.ActionEvent; 031import java.beans.PropertyChangeEvent; 032import java.beans.PropertyChangeListener; 033import java.lang.ref.WeakReference; 034import java.util.HashSet; 035import java.util.Iterator; 036import java.util.Set; 037 038import javax.swing.AbstractAction; 039import javax.swing.Action; 040import javax.swing.JComponent; 041import javax.swing.JOptionPane; 042import javax.swing.JToolBar; 043import javax.swing.KeyStroke; 044import javax.swing.SwingUtilities; 045import javax.swing.UIManager; 046 047import org.jdesktop.swingx.JXDialog; 048import org.jdesktop.swingx.JXFindBar; 049import org.jdesktop.swingx.JXFindPanel; 050import org.jdesktop.swingx.JXFrame; 051import org.jdesktop.swingx.JXRootPane; 052import org.jdesktop.swingx.plaf.LookAndFeelAddons; 053import org.jdesktop.swingx.plaf.UIDependent; 054import org.jdesktop.swingx.util.Utilities; 055 056/** 057 * Factory to create, configure and show application consistent 058 * search and find widgets. 059 * 060 * Typically a shared JXFindBar is used for incremental search, while 061 * a shared JXFindPanel is used for batch search. This implementation 062 * 063 * <ul> 064 * <li> JXFindBar - adds and shows it in the target's toplevel container's 065 * toolbar (assuming a JXRootPane) 066 * <li> JXFindPanel - creates a JXDialog, adds and shows the findPanel in the 067 * Dialog 068 * </ul> 069 * 070 * 071 * PENDING: JW - update (?) views/wiring on focus change. Started brute force - 072 * stop searching. This looks extreme confusing for findBars added to ToolBars 073 * which are empty except for the findbar. Weird problem if triggered from 074 * menu - find widget disappears after having been shown for an instance. 075 * Where's the focus? 076 * 077 * 078 * PENDING: add methods to return JXSearchPanels (for use by PatternMatchers). 079 * 080 * @author Jeanette Winzenburg 081 */ 082public class SearchFactory implements UIDependent { 083 private static class LaFListener implements PropertyChangeListener { 084 private final WeakReference<SearchFactory> ref; 085 086 public LaFListener(SearchFactory sf) { 087 this.ref = new WeakReference<SearchFactory>(sf); 088 } 089 090 /** 091 * {@inheritDoc} 092 */ 093 @Override 094 public void propertyChange(PropertyChangeEvent evt) { 095 SearchFactory sf = ref.get(); 096 097 if (sf == null) { 098 UIManager.removePropertyChangeListener(this); 099 } else if ("lookAndFeel".equals(evt.getPropertyName())) { 100 sf.updateUI(); 101 } 102 } 103 } 104 105 // PENDING: rename methods to batch/incremental instead of dialog/toolbar 106 107 static { 108 // Hack to enforce loading of SwingX framework ResourceBundle 109 LookAndFeelAddons.getAddon(); 110 } 111 112 private static SearchFactory searchFactory; 113 114 115 /** the shared find widget for batch-find. */ 116 protected JXFindPanel findPanel; 117 118 /** the shared find widget for incremental-find. */ 119 protected JXFindBar findBar; 120 /** this is a temporary hack: need to remove the useSearchHighlighter property. */ 121 protected JComponent lastFindBarTarget; 122 123 private boolean useFindBar; 124 125 private Point lastFindDialogLocation; 126 127 private FindRemover findRemover; 128 129 /** 130 * Returns the shared SearchFactory. 131 * 132 * @return the shared <code>SearchFactory</code> 133 */ 134 public static SearchFactory getInstance() { 135 if (searchFactory == null) { 136 searchFactory = new SearchFactory(); 137 } 138 return searchFactory; 139 } 140 141 /** 142 * Sets the shared SearchFactory. 143 * 144 * @param factory 145 */ 146 public static void setInstance(SearchFactory factory) { 147 searchFactory = factory; 148 } 149 150 public SearchFactory() { 151 UIManager.addPropertyChangeListener(new LaFListener(this)); 152 } 153 154 /** 155 * Returns a common Keystroke for triggering 156 * a search. Tries to be OS-specific. <p> 157 * 158 * PENDING: this should be done in the LF and the 159 * keyStroke looked up in the UIManager. 160 * 161 * @return the keyStroke to register with a findAction. 162 */ 163 public KeyStroke getSearchAccelerator() { 164 // JW: this should be handled by the LF! 165 // get the accelerator mnemonic from the UIManager 166 String findMnemonic = "F"; 167 KeyStroke findStroke = Utilities.stringToKey("D-" + findMnemonic); 168 // fallback for sandbox (this should be handled in Utilities instead!) 169 if (findStroke == null) { 170 findStroke = KeyStroke.getKeyStroke("control F"); 171 } 172 return findStroke; 173 174 } 175 /** 176 * Returns decision about using a batch- vs. incremental-find for the 177 * searchable. This implementation returns the useFindBar property directly. 178 * 179 * @param target - the component associated with the searchable 180 * @param searchable - the object to search. 181 * @return true if a incremental-find should be used, false otherwise. 182 */ 183 public boolean isUseFindBar(JComponent target, Searchable searchable) { 184 return useFindBar; 185 } 186 187 /** 188 * Sets the default search type to incremental or batch, for a 189 * true/false boolean. The default value is false (== batch). 190 * 191 * @param incremental a boolean to indicate the default search 192 * type, true for incremental and false for batch. 193 */ 194 public void setUseFindBar(boolean incremental) { 195 if (incremental == useFindBar) return; 196 this.useFindBar = incremental; 197 getFindRemover().endSearching(); 198 } 199 200 201 /** 202 * Shows an appropriate find widget targeted at the searchable. 203 * Opens a batch-find or incremental-find 204 * widget based on the return value of <code>isUseFindBar</code>. 205 * 206 * @param target - the component associated with the searchable 207 * @param searchable - the object to search. 208 * 209 * @see #isUseFindBar(JComponent, Searchable) 210 * @see #setUseFindBar(boolean) 211 */ 212 public void showFindInput(JComponent target, Searchable searchable) { 213 if (isUseFindBar(target, searchable)) { 214 showFindBar(target, searchable); 215 } else { 216 showFindDialog(target, searchable); 217 } 218 } 219 220//------------------------- incremental search 221 222 /** 223 * Show a incremental-find widget targeted at the searchable. 224 * 225 * This implementation uses a JXFindBar and inserts it into the 226 * target's toplevel container toolbar. 227 * 228 * PENDING: Nothing shown if there is no toolbar found. 229 * 230 * @param target - the component associated with the searchable 231 * @param searchable - the object to search. 232 */ 233 public void showFindBar(JComponent target, Searchable searchable) { 234 if (target == null) return; 235 if (findBar == null) { 236 findBar = getSharedFindBar(); 237 } else { 238 releaseFindBar(); 239 } 240 Window topLevel = SwingUtilities.getWindowAncestor(target); 241 if (topLevel instanceof JXFrame) { 242 JXRootPane rootPane = ((JXFrame) topLevel).getRootPaneExt(); 243 JToolBar toolBar = rootPane.getToolBar(); 244 if (toolBar == null) { 245 toolBar = new JToolBar(); 246 rootPane.setToolBar(toolBar); 247 } 248 toolBar.add(findBar, 0); 249 rootPane.revalidate(); 250 KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findBar); 251 252 } 253 lastFindBarTarget = target; 254 findBar.setLocale(target.getLocale()); 255 target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.TRUE); 256 getSharedFindBar().setSearchable(searchable); 257 installFindRemover(target, findBar); 258 } 259 260 /** 261 * Returns the shared JXFindBar. Creates and configures on 262 * first call. 263 * 264 * @return the shared <code>JXFindBar</code> 265 */ 266 public JXFindBar getSharedFindBar() { 267 if (findBar == null) { 268 findBar = createFindBar(); 269 configureSharedFindBar(); 270 } 271 return findBar; 272 } 273 274 /** 275 * Factory method to create a JXFindBar. 276 * 277 * @return the <code>JXFindBar</code> 278 */ 279 public JXFindBar createFindBar() { 280 return new JXFindBar(); 281 } 282 283 284 protected void installFindRemover(Container target, Container findWidget) { 285 if (target != null) { 286 getFindRemover().addTarget(target); 287 } 288 getFindRemover().addTarget(findWidget); 289 } 290 291 private FindRemover getFindRemover() { 292 if (findRemover == null) { 293 findRemover = new FindRemover(); 294 } 295 return findRemover; 296 } 297 298 /** 299 * convenience method to remove a component from its parent 300 * and revalidate the parent 301 */ 302 protected void removeFromParent(JComponent component) { 303 Container oldParent = component.getParent(); 304 if (oldParent != null) { 305 oldParent.remove(component); 306 if (oldParent instanceof JComponent) { 307 ((JComponent) oldParent).revalidate(); 308 } else { 309 // not sure... never have non-j comps 310 oldParent.invalidate(); 311 oldParent.validate(); 312 } 313 } 314 } 315 316 protected void stopSearching() { 317 if (findPanel != null) { 318 lastFindDialogLocation = hideSharedFindPanel(false); 319 findPanel.setSearchable(null); 320 } 321 if (findBar != null) { 322 releaseFindBar(); 323 } 324 } 325 326 /** 327 * Pre: findbar != null. 328 */ 329 protected void releaseFindBar() { 330 findBar.setSearchable(null); 331 if (lastFindBarTarget != null) { 332 lastFindBarTarget.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE); 333 lastFindBarTarget = null; 334 } 335 removeFromParent(findBar); 336 } 337 338 339 /** 340 * Configures the shared FindBar. This method is 341 * called once after creation of the shared FindBar. 342 * Subclasses can override to add configuration code. <p> 343 * 344 * Here: registers a custom action to remove the 345 * findbar from its ancestor container. 346 * 347 * PRE: findBar != null. 348 * 349 */ 350 protected void configureSharedFindBar() { 351 Action removeAction = new AbstractAction() { 352 353 @Override 354 public void actionPerformed(ActionEvent e) { 355 removeFromParent(findBar); 356// stopSearching(); 357// releaseFindBar(); 358 359 } 360 361 }; 362 findBar.getActionMap().put(JXDialog.CLOSE_ACTION_COMMAND, removeAction); 363 } 364 365//------------------------ batch search 366 367 /** 368 * Show a batch-find widget targeted at the given Searchable. 369 * 370 * This implementation uses a shared JXFindPanel contained 371 * JXDialog. 372 * 373 * @param target - 374 * the component associated with the searchable 375 * @param searchable - 376 * the object to search. 377 */ 378 public void showFindDialog(JComponent target, Searchable searchable) { 379 Window frame = null; //JOptionPane.getRootFrame(); 380 if (target != null) { 381 target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE); 382 frame = SwingUtilities.getWindowAncestor(target); 383// if (window instanceof Frame) { 384// frame = (Frame) window; 385// } 386 } 387 JXDialog topLevel = getDialogForSharedFindPanel(); 388 JXDialog findDialog; 389 if ((topLevel != null) && (topLevel.getOwner().equals(frame))) { 390 findDialog = topLevel; 391 // JW: #635-swingx - quick hack to update title to current locale ... 392// findDialog.setTitle(getSharedFindPanel().getName()); 393 KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findDialog); 394 } else { 395 Point location = hideSharedFindPanel(true); 396 if (frame instanceof Frame) { 397 findDialog = new JXDialog((Frame) frame, getSharedFindPanel()); 398 } else if (frame instanceof Dialog) { 399 // fix #215-swingx: had problems with secondary modal dialogs. 400 findDialog = new JXDialog((Dialog) frame, getSharedFindPanel()); 401 } else { 402 findDialog = new JXDialog(JOptionPane.getRootFrame(), getSharedFindPanel()); 403 } 404 // RJO: shouldn't we avoid overloaded useage like this in a JSR296 world? swap getName() for getTitle() here? 405// findDialog.setTitle(getSharedFindPanel().getName()); 406 // JW: don't - this will stay on top of all applications! 407 // findDialog.setAlwaysOnTop(true); 408 findDialog.pack(); 409 if (location == null) { 410 findDialog.setLocationRelativeTo(frame); 411 } else { 412 findDialog.setLocation(location); 413 } 414 } 415 if (target != null) { 416 findDialog.setLocale(target.getLocale()); 417 } 418 getSharedFindPanel().setSearchable(searchable); 419 installFindRemover(target, findDialog); 420 findDialog.setVisible(true); 421 } 422 423 424 /** 425 * Returns the shared JXFindPanel. Lazyly creates and configures on 426 * first call. 427 * 428 * @return the shared <code>JXFindPanel</code> 429 */ 430 public JXFindPanel getSharedFindPanel() { 431 if (findPanel == null) { 432 findPanel = createFindPanel(); 433 configureSharedFindPanel(); 434 } else { 435 // JW: temporary hack around #718-swingx 436 // no longer needed with cleanup of hideSharedFindPanel 437// if (findPanel.getParent() == null) { 438// SwingUtilities.updateComponentTreeUI(findPanel); 439// } 440 } 441 return findPanel; 442 } 443 444 /** 445 * Factory method to create a JXFindPanel. 446 * 447 * @return <code>JXFindPanel</code> 448 */ 449 public JXFindPanel createFindPanel() { 450 return new JXFindPanel(); 451 } 452 453 454 /** 455 * Configures the shared FindPanel. This method is 456 * called once after creation of the shared FindPanel. 457 * Subclasses can override to add configuration code. <p> 458 * 459 * Here: no-op 460 * PRE: findPanel != null. 461 * 462 */ 463 protected void configureSharedFindPanel() { 464 } 465 466 467 468 private JXDialog getDialogForSharedFindPanel() { 469 if (findPanel == null) return null; 470 Window window = SwingUtilities.getWindowAncestor(findPanel); 471 return (window instanceof JXDialog) ? (JXDialog) window : null; 472 } 473 474 475 /** 476 * Hides the findPanel's toplevel window and returns its location. 477 * If the dispose is true, the findPanel is removed from its parent 478 * and the toplevel window is disposed. 479 * 480 * @param dispose boolean to indicate whether the findPanels toplevel 481 * window should be disposed. 482 * @return the location of the window if visible, or the last known 483 * location. 484 */ 485 protected Point hideSharedFindPanel(boolean dispose) { 486 if (findPanel == null) return null; 487 Window window = SwingUtilities.getWindowAncestor(findPanel); 488 Point location = lastFindDialogLocation; 489 if (window != null) { 490 // PENDING JW: can't remember why it it removed always? 491 if (window.isVisible()) { 492 location = window.getLocationOnScreen(); 493 window.setVisible(false); 494 } 495 if (dispose) { 496 findPanel.getParent().remove(findPanel); 497 window.dispose(); 498 } 499 } 500 return location; 501 } 502 503 public class FindRemover implements PropertyChangeListener { 504 KeyboardFocusManager focusManager; 505 Set<Container> targets; 506 507 public FindRemover() { 508 updateManager(); 509 } 510 511 public void addTarget(Container target) { 512 getTargets().add(target); 513 } 514 515 public void removeTarget(Container target) { 516 getTargets().remove(target); 517 } 518 519 private Set<Container> getTargets() { 520 if (targets == null) { 521 targets = new HashSet<Container>(); 522 } 523 return targets; 524 } 525 526 private void updateManager() { 527 if (focusManager != null) { 528 focusManager.removePropertyChangeListener("permanentFocusOwner", this); 529 } 530 this.focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); 531 focusManager.addPropertyChangeListener("permanentFocusOwner", this); 532 } 533 534 @Override 535 public void propertyChange(PropertyChangeEvent ev) { 536 537 Component c = focusManager.getPermanentFocusOwner(); 538 if (c == null) return; 539 for (Iterator<Container> iter = getTargets().iterator(); iter.hasNext();) { 540 Container element = iter.next(); 541 if ((element == c) || (SwingUtilities.isDescendingFrom(c, element))) { 542 return; 543 } 544 } 545 endSearching(); 546 } 547 548 public void endSearching() { 549 getTargets().clear(); 550 stopSearching(); 551 } 552 } 553 554 /** 555 * {@inheritDoc} 556 */ 557 @Override 558 public void updateUI() { 559 if (findBar != null) { 560 SwingUtilities.updateComponentTreeUI(findBar); 561 } 562 563 if (findPanel != null) { 564 SwingUtilities.updateComponentTreeUI(findPanel); 565 } 566 } 567}