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 }