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