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    }