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}