swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/search/SearchFactory.java
/*
 * $Id$
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jdesktop.swingx.search;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

import org.jdesktop.swingx.JXDialog;
import org.jdesktop.swingx.JXFindBar;
import org.jdesktop.swingx.JXFindPanel;
import org.jdesktop.swingx.JXFrame;
import org.jdesktop.swingx.JXRootPane;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.UIDependent;
import org.jdesktop.swingx.util.Utilities;

/**
 * Factory to create, configure and show application consistent
 * search and find widgets.
 * 
 * Typically a shared JXFindBar is used for incremental search, while
 * a shared JXFindPanel is used for batch search. This implementation 
 * 
 * <ul>
 *  <li> JXFindBar - adds and shows it in the target's toplevel container's
 *    toolbar (assuming a JXRootPane)
 *  <li> JXFindPanel - creates a JXDialog, adds and shows the findPanel in the
 *    Dialog 
 * </ul>
 * 
 * 
 * PENDING: JW - update (?) views/wiring on focus change. Started brute force - 
 * stop searching. This looks extreme confusing for findBars added to ToolBars 
 * which are empty except for the findbar. Weird problem if triggered from 
 * menu - find widget disappears after having been shown for an instance. 
 * Where's the focus?
 * 
 * 
 * PENDING: add methods to return JXSearchPanels (for use by PatternMatchers).
 * 
 * @author Jeanette Winzenburg
 */
public class SearchFactory implements UIDependent {
    private static class LaFListener implements PropertyChangeListener {
        private final WeakReference<SearchFactory> ref;
        
        public LaFListener(SearchFactory sf) {
            this.ref = new WeakReference<SearchFactory>(sf);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            SearchFactory sf = ref.get();
            
            if (sf == null) {
                UIManager.removePropertyChangeListener(this);
            } else if ("lookAndFeel".equals(evt.getPropertyName())) {
                sf.updateUI();
            }
        }
    }
    
    // PENDING: rename methods to batch/incremental instead of dialog/toolbar

    static {
        // Hack to enforce loading of SwingX framework ResourceBundle
        LookAndFeelAddons.getAddon();
    }

    private static SearchFactory searchFactory;

   
    /** the shared find widget for batch-find. */
    protected JXFindPanel findPanel;
   
    /** the shared find widget for incremental-find. */
    protected JXFindBar findBar;
    /** this is a temporary hack: need to remove the useSearchHighlighter property. */ 
    protected JComponent lastFindBarTarget;
    
    private boolean useFindBar;

    private Point lastFindDialogLocation;

    private FindRemover findRemover;
    
    /** 
     * Returns the shared SearchFactory.
     * 
     * @return the shared <code>SearchFactory</code>
     */
    public static SearchFactory getInstance() {
          if (searchFactory == null) {
              searchFactory = new SearchFactory();
          }
          return searchFactory;
      }

    /**
     * Sets the shared SearchFactory.
     * 
     * @param factory
     */
    public static void setInstance(SearchFactory factory) {
        searchFactory = factory;
    }

    public SearchFactory() {
        UIManager.addPropertyChangeListener(new LaFListener(this));
    }
    
    /**
     * Returns a common Keystroke for triggering 
     * a search. Tries to be OS-specific. <p>
     * 
     * PENDING: this should be done in the LF and the
     * keyStroke looked up in the UIManager. 
     * 
     * @return the keyStroke to register with a findAction.
     */
    public KeyStroke getSearchAccelerator() {
        // JW: this should be handled by the LF! 
        // get the accelerator mnemonic from the UIManager
        String findMnemonic = "F";
        KeyStroke findStroke = Utilities.stringToKey("D-" + findMnemonic);
        // fallback for sandbox (this should be handled in Utilities instead!)
        if (findStroke == null) {
            findStroke = KeyStroke.getKeyStroke("control F");
        }
        return findStroke;
        
    }
    /**
     * Returns decision about using a batch- vs. incremental-find for the
     * searchable. This implementation returns the useFindBar property directly.
     * 
     * @param target - the component associated with the searchable
     * @param searchable - the object to search.
     * @return true if a incremental-find should be used, false otherwise.
     */
    public boolean isUseFindBar(JComponent target, Searchable searchable) {
        return useFindBar;
    }
 
    /**
     * Sets the default search type to incremental or batch, for a
     * true/false boolean. The default value is false (== batch).
     * 
     * @param incremental a boolean to indicate the default search
     * type, true for incremental and false for batch.
     */
    public void setUseFindBar(boolean incremental) {
        if (incremental == useFindBar) return;
        this.useFindBar = incremental;
        getFindRemover().endSearching();
    }

    
    /**
     * Shows an appropriate find widget targeted at the searchable.
     * Opens a batch-find or incremental-find 
     * widget based on the return value of <code>isUseFindBar</code>. 
     *  
     * @param target - the component associated with the searchable
     * @param searchable - the object to search.
     * 
     * @see #isUseFindBar(JComponent, Searchable)
     * @see #setUseFindBar(boolean)
     */
    public void showFindInput(JComponent target, Searchable searchable) {
        if (isUseFindBar(target, searchable)) {
            showFindBar(target, searchable);
        } else {
            showFindDialog(target, searchable);
        }
    }

//------------------------- incremental search
    
    /**
     * Show a incremental-find widget targeted at the searchable.
     * 
     * This implementation uses a JXFindBar and inserts it into the
     * target's toplevel container toolbar. 
     * 
     * PENDING: Nothing shown if there is no toolbar found. 
     * 
     * @param target - the component associated with the searchable
     * @param searchable - the object to search.
     */
    public void showFindBar(JComponent target, Searchable searchable) {
        if (target == null) return;
        if (findBar == null) {
            findBar = getSharedFindBar();
        } else {
            releaseFindBar();
        }
        Window topLevel = SwingUtilities.getWindowAncestor(target);
        if (topLevel instanceof JXFrame) {
            JXRootPane rootPane = ((JXFrame) topLevel).getRootPaneExt();
            JToolBar toolBar = rootPane.getToolBar();
            if (toolBar == null) {
                toolBar = new JToolBar();
                rootPane.setToolBar(toolBar);
            }
            toolBar.add(findBar, 0);
            rootPane.revalidate();
            KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findBar);
            
        }
        lastFindBarTarget = target;
        findBar.setLocale(target.getLocale());
        target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.TRUE);
        getSharedFindBar().setSearchable(searchable);
        installFindRemover(target, findBar);
    }

    /**
     * Returns the shared JXFindBar. Creates and configures on 
     * first call.
     * 
     * @return the shared <code>JXFindBar</code>
     */
    public JXFindBar getSharedFindBar() {
        if (findBar == null) {
            findBar = createFindBar();
            configureSharedFindBar();
        }
        return findBar;
    }

    /**
     * Factory method to create a JXFindBar.
     * 
     * @return the <code>JXFindBar</code>
     */
    public JXFindBar createFindBar() {
        return new JXFindBar();
    }


    protected void installFindRemover(Container target, Container findWidget) {
        if (target != null) {
            getFindRemover().addTarget(target);
        }
        getFindRemover().addTarget(findWidget);
    }

    private FindRemover getFindRemover() {
        if (findRemover == null) {
            findRemover = new FindRemover();
        }
        return findRemover;
    }

    /**
     * convenience method to remove a component from its parent
     * and revalidate the parent
     */
    protected void removeFromParent(JComponent component) {
        Container oldParent = component.getParent();
        if (oldParent != null) {
            oldParent.remove(component);
            if (oldParent instanceof JComponent) {
                ((JComponent) oldParent).revalidate();
            } else {
                // not sure... never have non-j comps
                oldParent.invalidate();
                oldParent.validate();
            }
        }
    }

    protected void stopSearching() {
        if (findPanel != null) {
            lastFindDialogLocation = hideSharedFindPanel(false);
            findPanel.setSearchable(null);
        }
        if (findBar != null) {
            releaseFindBar();
         }
    }

    /**
     * Pre: findbar != null.
     */
    protected void releaseFindBar() {
        findBar.setSearchable(null);
        if (lastFindBarTarget != null) {
            lastFindBarTarget.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE);
            lastFindBarTarget = null;
        }
        removeFromParent(findBar);
    }

    
    /**
     * Configures the shared FindBar. This method is
     * called once after creation of the shared FindBar.
     * Subclasses can override to add configuration code. <p>
     * 
     * Here: registers a custom action to remove the 
     * findbar from its ancestor container.
     * 
     * PRE: findBar != null.
     *
     */
    protected void configureSharedFindBar() {
        Action removeAction = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                removeFromParent(findBar);
//                stopSearching();
//                releaseFindBar();
                
            }
            
        };
        findBar.getActionMap().put(JXDialog.CLOSE_ACTION_COMMAND, removeAction);
    }

//------------------------ batch search

    /**
     * Show a batch-find widget targeted at the given Searchable.
     * 
     * This implementation uses a shared JXFindPanel contained 
     * JXDialog.
     * 
     * @param target -
     *            the component associated with the searchable
     * @param searchable -
     *            the object to search.
     */
    public void showFindDialog(JComponent target, Searchable searchable) {
        Window frame = null; //JOptionPane.getRootFrame();
        if (target != null) {
            target.putClientProperty(AbstractSearchable.MATCH_HIGHLIGHTER, Boolean.FALSE);
            frame = SwingUtilities.getWindowAncestor(target);
//            if (window instanceof Frame) {
//                frame = (Frame) window;
//            }
        }
        JXDialog topLevel = getDialogForSharedFindPanel();
        JXDialog findDialog;
        if ((topLevel != null) && (topLevel.getOwner().equals(frame))) {
            findDialog = topLevel;
            // JW: #635-swingx - quick hack to update title to current locale ...
//            findDialog.setTitle(getSharedFindPanel().getName());
            KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(findDialog);
        } else {
            Point location = hideSharedFindPanel(true);
            if (frame instanceof Frame) {
                findDialog = new JXDialog((Frame) frame, getSharedFindPanel());
            } else if (frame instanceof Dialog) {
                // fix #215-swingx: had problems with secondary modal dialogs.
                findDialog = new JXDialog((Dialog) frame, getSharedFindPanel());
            } else {
                findDialog = new JXDialog(JOptionPane.getRootFrame(), getSharedFindPanel());
            }
            // RJO: shouldn't we avoid overloaded useage like this in a JSR296 world? swap getName() for getTitle() here?            
//            findDialog.setTitle(getSharedFindPanel().getName());
            // JW: don't - this will stay on top of all applications!
            // findDialog.setAlwaysOnTop(true);
            findDialog.pack();
            if (location == null) {
                findDialog.setLocationRelativeTo(frame);
            } else {
                findDialog.setLocation(location);
            }
        } 
        if (target != null) {
            findDialog.setLocale(target.getLocale());
        }
        getSharedFindPanel().setSearchable(searchable);
        installFindRemover(target, findDialog);
        findDialog.setVisible(true);
    }


    /**
     * Returns the shared JXFindPanel. Lazyly creates and configures on 
     * first call.
     * 
     * @return the shared <code>JXFindPanel</code>
     */
    public JXFindPanel getSharedFindPanel() {
        if (findPanel == null) {
            findPanel = createFindPanel();
            configureSharedFindPanel();
        } else {
            // JW: temporary hack around #718-swingx
            // no longer needed with cleanup of hideSharedFindPanel
//            if (findPanel.getParent() == null) {
//                SwingUtilities.updateComponentTreeUI(findPanel);
//            }
        }
        return findPanel;
    }

    /**
     * Factory method to create a JXFindPanel.
     * 
     * @return <code>JXFindPanel</code>
     */
    public JXFindPanel createFindPanel() {
        return new JXFindPanel();
    }


    /**
     * Configures the shared FindPanel. This method is
     * called once after creation of the shared FindPanel.
     * Subclasses can override to add configuration code. <p>
     * 
     * Here: no-op
     * PRE: findPanel != null.
     *
     */
    protected void configureSharedFindPanel() {
    }


    
    private JXDialog getDialogForSharedFindPanel() {
        if (findPanel == null) return null;
        Window window = SwingUtilities.getWindowAncestor(findPanel);
        return (window instanceof JXDialog) ? (JXDialog) window : null;
    }


    /**
     * Hides the findPanel's toplevel window and returns its location.
     * If the dispose is true, the findPanel is removed from its parent
     * and the toplevel window is disposed.
     * 
     * @param dispose boolean to indicate whether the findPanels toplevel
     *   window should be disposed.
     * @return the location of the window if visible, or the last known
     *   location.
     */
    protected Point hideSharedFindPanel(boolean dispose) {
        if (findPanel == null) return null;
        Window window = SwingUtilities.getWindowAncestor(findPanel);
        Point location = lastFindDialogLocation;
        if (window != null) {
            // PENDING JW: can't remember why it it removed always?
            if (window.isVisible()) {
                location = window.getLocationOnScreen();
                window.setVisible(false);
            }
            if (dispose) {
                findPanel.getParent().remove(findPanel);
                window.dispose();
            } 
        }
        return location;
    }

    public class FindRemover implements PropertyChangeListener {
        KeyboardFocusManager focusManager;
        Set<Container> targets;
        
        public FindRemover() {
            updateManager();
        }

        public void addTarget(Container target) {
            getTargets().add(target);
        }
        
        public void removeTarget(Container target) {
            getTargets().remove(target);
        }
        
        private Set<Container> getTargets() {
            if (targets == null) {
                targets = new HashSet<Container>();
            }
            return targets;
        }

        private void updateManager() {
            if (focusManager != null) {
                focusManager.removePropertyChangeListener("permanentFocusOwner", this);
            }
            this.focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
            focusManager.addPropertyChangeListener("permanentFocusOwner", this);
        }

        @Override
        public void propertyChange(PropertyChangeEvent ev) {

            Component c = focusManager.getPermanentFocusOwner();
            if (c == null) return;
            for (Iterator<Container> iter = getTargets().iterator(); iter.hasNext();) {
                Container element = iter.next();
                if ((element == c) || (SwingUtilities.isDescendingFrom(c, element))) {
                    return;
                }
            }
            endSearching();
       }

        public void endSearching() {
            getTargets().clear();
            stopSearching();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateUI() {
        if (findBar != null) {
            SwingUtilities.updateComponentTreeUI(findBar);
        }
        
        if (findPanel != null) {
            SwingUtilities.updateComponentTreeUI(findPanel);
        }
    }
}
