001/*
002 * $Id: AbstractPatternPanel.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;
022
023import java.awt.Dimension;
024import java.beans.PropertyChangeEvent;
025import java.beans.PropertyChangeListener;
026import java.util.Locale;
027
028import javax.swing.Action;
029import javax.swing.ActionMap;
030import javax.swing.JCheckBox;
031import javax.swing.JLabel;
032import javax.swing.JTextField;
033import javax.swing.SwingUtilities;
034import javax.swing.event.DocumentEvent;
035import javax.swing.event.DocumentListener;
036
037import org.jdesktop.swingx.action.AbstractActionExt;
038import org.jdesktop.swingx.action.ActionContainerFactory;
039import org.jdesktop.swingx.action.BoundAction;
040import org.jdesktop.swingx.plaf.LookAndFeelAddons;
041import org.jdesktop.swingx.plaf.UIManagerExt;
042import org.jdesktop.swingx.search.PatternModel;
043
044/**
045 * Common base class of ui clients.
046 * 
047 * Implements basic synchronization between PatternModel state and
048 * actions bound to it.
049 * 
050 * 
051 * 
052 * PENDING: extending JXPanel is a convenience measure, should be extracted
053 *   into a dedicated controller.
054 * PENDING: should be re-visited when swingx goes binding-aware
055 * 
056 * @author Jeanette Winzenburg
057 */
058public abstract class AbstractPatternPanel extends JXPanel {
059
060    public static final String SEARCH_FIELD_LABEL = "searchFieldLabel";
061    public static final String SEARCH_FIELD_MNEMONIC = SEARCH_FIELD_LABEL + ".mnemonic";
062    public static final String SEARCH_TITLE = "searchTitle";
063    public static final String MATCH_ACTION_COMMAND = "match";
064
065    static {
066        // Hack to enforce loading of SwingX framework ResourceBundle
067        LookAndFeelAddons.getAddon();
068    }
069
070    protected JLabel searchLabel;
071    protected JTextField searchField;
072    protected JCheckBox matchCheck;
073    
074    protected PatternModel patternModel;
075    private ActionContainerFactory actionFactory;
076
077
078//------------------------ actions
079
080    /**
081     * Callback action bound to MATCH_ACTION_COMMAND. 
082     */
083    public abstract void match();
084    
085    /** 
086     * convenience method for type-cast to AbstractActionExt.
087     * 
088     * @param key Key to retrieve action
089     * @return Action bound to this key
090     * @see AbstractActionExt
091     */
092    protected AbstractActionExt getAction(String key) {
093        // PENDING: outside clients might add different types?
094        return (AbstractActionExt) getActionMap().get(key);
095    }
096
097    /**
098     * creates and registers all actions for the default the actionMap.
099     */
100    protected void initActions() {
101        initPatternActions();
102        initExecutables();
103    }
104    
105    /**
106     * creates and registers all "executable" actions.
107     * Meaning: the actions bound to a callback method on this.
108     * 
109     * PENDING: not quite correctly factored? Name?
110     *
111     */
112    protected void initExecutables() {
113        Action execute = createBoundAction(MATCH_ACTION_COMMAND, "match");
114        getActionMap().put(JXDialog.EXECUTE_ACTION_COMMAND, 
115                execute);
116        getActionMap().put(MATCH_ACTION_COMMAND, execute);
117        refreshEmptyFromModel();
118    }
119    
120    /**
121     * creates actions bound to PatternModel's state.
122     */
123    protected void initPatternActions() {
124        ActionMap map = getActionMap();
125        map.put(PatternModel.MATCH_CASE_ACTION_COMMAND, 
126                createModelStateAction(PatternModel.MATCH_CASE_ACTION_COMMAND, 
127                        "setCaseSensitive", getPatternModel().isCaseSensitive()));
128        map.put(PatternModel.MATCH_WRAP_ACTION_COMMAND, 
129                createModelStateAction(PatternModel.MATCH_WRAP_ACTION_COMMAND, 
130                        "setWrapping", getPatternModel().isWrapping()));
131        map.put(PatternModel.MATCH_BACKWARDS_ACTION_COMMAND, 
132                createModelStateAction(PatternModel.MATCH_BACKWARDS_ACTION_COMMAND, 
133                        "setBackwards", getPatternModel().isBackwards()));
134        map.put(PatternModel.MATCH_INCREMENTAL_ACTION_COMMAND, 
135                createModelStateAction(PatternModel.MATCH_INCREMENTAL_ACTION_COMMAND, 
136                        "setIncremental", getPatternModel().isIncremental()));
137    }
138
139    /**
140     * Returns a potentially localized value from the UIManager. The given key
141     * is prefixed by this component|s <code>UIPREFIX</code> before doing the
142     * lookup. The lookup respects this table's current <code>locale</code>
143     * property. Returns the key, if no value is found.
144     * 
145     * @param key the bare key to look up in the UIManager.
146     * @return the value mapped to UIPREFIX + key or key if no value is found.
147     */
148    protected String getUIString(String key) {
149        return getUIString(key, getLocale());
150    }
151
152    /**
153     * Returns a potentially localized value from the UIManager for the 
154     * given locale. The given key
155     * is prefixed by this component's <code>UIPREFIX</code> before doing the
156     * lookup. Returns the key, if no value is found.
157     * 
158     * @param key the bare key to look up in the UIManager.
159     * @param locale the locale use for lookup
160     * @return the value mapped to UIPREFIX + key in the given locale,
161     *    or key if no value is found.
162     */
163    protected String getUIString(String key, Locale locale) {
164        String text = UIManagerExt.getString(PatternModel.SEARCH_PREFIX + key, locale);
165        return text != null ? text : key;
166    }
167
168
169    /**
170     * creates, configures and returns a bound state action on a boolean property
171     * of the PatternModel.
172     * 
173     * @param command the actionCommand - same as key to find localizable resources
174     * @param methodName the method on the PatternModel to call on item state changed
175     * @param initial the initial value of the property
176     * @return newly created action
177     */
178    protected AbstractActionExt createModelStateAction(String command, String methodName, boolean initial) {
179        String actionName = getUIString(command);
180        BoundAction action = new BoundAction(actionName,
181                command);
182        action.setStateAction();
183        action.registerCallback(getPatternModel(), methodName);
184        action.setSelected(initial);
185        return action;
186    }
187
188    /**
189     * creates, configures and returns a bound action to the given method of 
190     * this.
191     * 
192     * @param actionCommand the actionCommand, same as key to find localizable resources
193     * @param methodName the method to call an actionPerformed.
194     * @return newly created action
195     */
196    protected AbstractActionExt createBoundAction(String actionCommand, String methodName) {
197        String actionName = getUIString(actionCommand);
198        BoundAction action = new BoundAction(actionName,
199                actionCommand);
200        action.registerCallback(this, methodName);
201        return action;
202    }
203
204//------------------------ dynamic locale support
205    
206
207    /**
208     * {@inheritDoc} <p>
209     * Overridden to update locale-dependent properties. 
210     * 
211     * @see #updateLocaleState(Locale) 
212     */
213    @Override
214    public void setLocale(Locale l) {
215        updateLocaleState(l);
216        super.setLocale(l);
217    }
218    
219    /**
220     * Updates locale-dependent state.
221     * 
222     * Here: updates registered column actions' locale-dependent state.
223     * <p>
224     * 
225     * PENDING: Try better to find all column actions including custom
226     * additions? Or move to columnControl?
227     * 
228     * @see #setLocale(Locale)
229     */
230    protected void updateLocaleState(Locale locale) {
231        for (Object key : getActionMap().allKeys()) {
232            if (key instanceof String) {
233                String keyString = getUIString((String) key, locale);
234                if (!key.equals(keyString)) {
235                    getActionMap().get(key).putValue(Action.NAME, keyString);
236                    
237                }
238            }
239        }
240        bindSearchLabel(locale);
241    }
242    
243
244    //---------------------- synch patternModel <--> components
245
246    /**
247     * called from listening to pattern property of PatternModel.
248     * 
249     * This implementation calls match() if the model is in
250     * incremental state.
251     *
252     */
253    protected void refreshPatternFromModel() {
254        if (getPatternModel().isIncremental()) {
255            match();
256        }
257    }
258
259
260    /**
261     * returns the patternModel. Lazyly creates and registers a
262     * propertyChangeListener if null.
263     * 
264     * @return current <code>PatternModel</code> if it exists or newly created 
265     * one if it was not initialized before this call
266     */
267    protected PatternModel getPatternModel() {
268        if (patternModel == null) {
269            patternModel = createPatternModel();
270            patternModel.addPropertyChangeListener(getPatternModelListener());
271        }
272        return patternModel;
273    }
274
275
276    /**
277     * factory method to create the PatternModel.
278     * Hook for subclasses to install custom models.
279     *
280     * @return newly created <code>PatternModel</code>
281     */
282    protected PatternModel createPatternModel() {
283        return new PatternModel();
284    }
285
286    /**
287     * creates and returns a PropertyChangeListener to the PatternModel.
288     * 
289     * NOTE: the patternModel is totally under control of this class - currently
290     * there's no need to keep a reference to the listener.
291     * 
292     * @return created and bound to appropriate callback methods 
293     *  <code>PropertyChangeListener</code>
294     */
295    protected PropertyChangeListener getPatternModelListener() {
296        return new PropertyChangeListener() {    
297            @Override
298            public void propertyChange(PropertyChangeEvent evt) {
299                String property = evt.getPropertyName();
300                if ("pattern".equals(property)) {
301                    refreshPatternFromModel();
302                } else if ("rawText".equals(property)) {
303                    refreshDocumentFromModel();
304                } else if ("caseSensitive".equals(property)){
305                    getAction(PatternModel.MATCH_CASE_ACTION_COMMAND).
306                        setSelected(((Boolean) evt.getNewValue()).booleanValue());
307                } else if ("wrapping".equals(property)) {
308                    getAction(PatternModel.MATCH_WRAP_ACTION_COMMAND).
309                    setSelected(((Boolean) evt.getNewValue()).booleanValue());
310                } else if ("backwards".equals(property)) {
311                    getAction(PatternModel.MATCH_BACKWARDS_ACTION_COMMAND).
312                    setSelected(((Boolean) evt.getNewValue()).booleanValue());
313                } else if ("incremental".equals(property)) {
314                    getAction(PatternModel.MATCH_INCREMENTAL_ACTION_COMMAND).
315                    setSelected(((Boolean) evt.getNewValue()).booleanValue());
316
317                } else if ("empty".equals(property)) {
318                    refreshEmptyFromModel();
319                }   
320    
321            }
322    
323        };
324    }
325
326    /**
327     * called from listening to empty property of PatternModel.
328     * 
329     * this implementation synch's the enabled state of the action with
330     * MATCH_ACTION_COMMAND to !empty.
331     * 
332     */
333    protected void refreshEmptyFromModel() {
334        boolean enabled = !getPatternModel().isEmpty();
335        getAction(MATCH_ACTION_COMMAND).setEnabled(enabled);
336        
337    }
338
339    /**
340     * callback method from listening to searchField.
341     *
342     */
343    protected void refreshModelFromDocument() {
344        getPatternModel().setRawText(searchField.getText());
345    }
346
347    /**
348     * callback method that updates document from the search field
349     *
350     */
351    protected void refreshDocumentFromModel() {
352        if (searchField.getText().equals(getPatternModel().getRawText())) return;
353        SwingUtilities.invokeLater(new Runnable() {
354            @Override
355            public void run() {
356                searchField.setText(getPatternModel().getRawText());
357            }
358        });
359    }
360
361    /**
362     * Create <code>DocumentListener</code> for the search field that calls
363     * corresponding callback method whenever the search field contents is being changed
364     *
365     * @return newly created <code>DocumentListener</code>
366     */
367    protected DocumentListener getSearchFieldListener() {
368        return new DocumentListener() {
369            @Override
370            public void changedUpdate(DocumentEvent ev) {
371                // JW - really?? we've a PlainDoc without Attributes
372                refreshModelFromDocument();
373            }
374    
375            @Override
376            public void insertUpdate(DocumentEvent ev) {
377                refreshModelFromDocument();
378            }
379    
380            @Override
381            public void removeUpdate(DocumentEvent ev) {
382                refreshModelFromDocument();
383            }
384    
385        };
386    }
387
388//-------------------------- config helpers
389
390    /**
391     * configure and bind components to/from PatternModel
392     */
393    protected void bind() {
394       bindSearchLabel(getLocale());
395        searchField.getDocument().addDocumentListener(getSearchFieldListener());
396        getActionContainerFactory().configureButton(matchCheck, 
397                (AbstractActionExt) getActionMap().get(PatternModel.MATCH_CASE_ACTION_COMMAND),
398                null);
399        
400    }
401
402    /**
403     * Configures the searchLabel.
404     * Here: sets text and mnenomic properties form ui values, 
405     * configures as label for searchField.
406     */
407    protected void bindSearchLabel(Locale locale) {
408        searchLabel.setText(getUIString(SEARCH_FIELD_LABEL, locale));
409          String mnemonic = getUIString(SEARCH_FIELD_MNEMONIC, locale);
410          if (mnemonic != SEARCH_FIELD_MNEMONIC) {
411              searchLabel.setDisplayedMnemonic(mnemonic.charAt(0));
412          }
413          searchLabel.setLabelFor(searchField);
414    }
415    
416    /**
417     * @return current <code>ActionContainerFactory</code>. 
418     * Will lazily create new factory if it does not exist
419     */
420    protected ActionContainerFactory getActionContainerFactory() {
421        if (actionFactory == null) {
422            actionFactory = new ActionContainerFactory(null);
423        }
424        return actionFactory;
425    }
426    
427    /**
428     * Initialize all the incorporated components and models
429     */
430    protected void initComponents() {
431        searchLabel = new JLabel();
432        searchField = new JTextField(getSearchFieldWidth()) {
433            @Override
434            public Dimension getMaximumSize() {
435                Dimension superMax = super.getMaximumSize();
436                superMax.height = getPreferredSize().height;
437                return superMax;
438            }
439        };
440        matchCheck = new JCheckBox();
441    }
442
443    /**
444     * @return width in characters of the search field
445     */
446    protected int getSearchFieldWidth() {
447        return 15;
448    }
449}