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