001    /*
002     * $Id: AbstractPatternPanel.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;
022    
023    import java.awt.Dimension;
024    import java.beans.PropertyChangeEvent;
025    import java.beans.PropertyChangeListener;
026    import java.util.Locale;
027    
028    import javax.swing.Action;
029    import javax.swing.ActionMap;
030    import javax.swing.JCheckBox;
031    import javax.swing.JLabel;
032    import javax.swing.JTextField;
033    import javax.swing.SwingUtilities;
034    import javax.swing.event.DocumentEvent;
035    import javax.swing.event.DocumentListener;
036    
037    import org.jdesktop.swingx.action.AbstractActionExt;
038    import org.jdesktop.swingx.action.ActionContainerFactory;
039    import org.jdesktop.swingx.action.BoundAction;
040    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
041    import org.jdesktop.swingx.plaf.UIManagerExt;
042    import 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     */
058    public 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                public void propertyChange(PropertyChangeEvent evt) {
298                    String property = evt.getPropertyName();
299                    if ("pattern".equals(property)) {
300                        refreshPatternFromModel();
301                    } else if ("rawText".equals(property)) {
302                        refreshDocumentFromModel();
303                    } else if ("caseSensitive".equals(property)){
304                        getAction(PatternModel.MATCH_CASE_ACTION_COMMAND).
305                            setSelected(((Boolean) evt.getNewValue()).booleanValue());
306                    } else if ("wrapping".equals(property)) {
307                        getAction(PatternModel.MATCH_WRAP_ACTION_COMMAND).
308                        setSelected(((Boolean) evt.getNewValue()).booleanValue());
309                    } else if ("backwards".equals(property)) {
310                        getAction(PatternModel.MATCH_BACKWARDS_ACTION_COMMAND).
311                        setSelected(((Boolean) evt.getNewValue()).booleanValue());
312                    } else if ("incremental".equals(property)) {
313                        getAction(PatternModel.MATCH_INCREMENTAL_ACTION_COMMAND).
314                        setSelected(((Boolean) evt.getNewValue()).booleanValue());
315    
316                    } else if ("empty".equals(property)) {
317                        refreshEmptyFromModel();
318                    }   
319        
320                }
321        
322            };
323        }
324    
325        /**
326         * called from listening to empty property of PatternModel.
327         * 
328         * this implementation synch's the enabled state of the action with
329         * MATCH_ACTION_COMMAND to !empty.
330         * 
331         */
332        protected void refreshEmptyFromModel() {
333            boolean enabled = !getPatternModel().isEmpty();
334            getAction(MATCH_ACTION_COMMAND).setEnabled(enabled);
335            
336        }
337    
338        /**
339         * callback method from listening to searchField.
340         *
341         */
342        protected void refreshModelFromDocument() {
343            getPatternModel().setRawText(searchField.getText());
344        }
345    
346        /**
347         * callback method that updates document from the search field
348         *
349         */
350        protected void refreshDocumentFromModel() {
351            if (searchField.getText().equals(getPatternModel().getRawText())) return;
352            SwingUtilities.invokeLater(new Runnable() {
353                public void run() {
354                    searchField.setText(getPatternModel().getRawText());
355                }
356            });
357        }
358    
359        /**
360         * Create <code>DocumentListener</code> for the search field that calls
361         * corresponding callback method whenever the search field contents is being changed
362         *
363         * @return newly created <code>DocumentListener</code>
364         */
365        protected DocumentListener getSearchFieldListener() {
366            return new DocumentListener() {
367                public void changedUpdate(DocumentEvent ev) {
368                    // JW - really?? we've a PlainDoc without Attributes
369                    refreshModelFromDocument();
370                }
371        
372                public void insertUpdate(DocumentEvent ev) {
373                    refreshModelFromDocument();
374                }
375        
376                public void removeUpdate(DocumentEvent ev) {
377                    refreshModelFromDocument();
378                }
379        
380            };
381        }
382    
383    //-------------------------- config helpers
384    
385        /**
386         * configure and bind components to/from PatternModel
387         */
388        protected void bind() {
389           bindSearchLabel(getLocale());
390            searchField.getDocument().addDocumentListener(getSearchFieldListener());
391            getActionContainerFactory().configureButton(matchCheck, 
392                    (AbstractActionExt) getActionMap().get(PatternModel.MATCH_CASE_ACTION_COMMAND),
393                    null);
394            
395        }
396    
397        /**
398         * Configures the searchLabel.
399         * Here: sets text and mnenomic properties form ui values, 
400         * configures as label for searchField.
401         */
402        protected void bindSearchLabel(Locale locale) {
403            searchLabel.setText(getUIString(SEARCH_FIELD_LABEL, locale));
404              String mnemonic = getUIString(SEARCH_FIELD_MNEMONIC, locale);
405              if (mnemonic != SEARCH_FIELD_MNEMONIC) {
406                  searchLabel.setDisplayedMnemonic(mnemonic.charAt(0));
407              }
408              searchLabel.setLabelFor(searchField);
409        }
410        
411        /**
412         * @return current <code>ActionContainerFactory</code>. 
413         * Will lazily create new factory if it does not exist
414         */
415        protected ActionContainerFactory getActionContainerFactory() {
416            if (actionFactory == null) {
417                actionFactory = new ActionContainerFactory(null);
418            }
419            return actionFactory;
420        }
421        
422        /**
423         * Initialize all the incorporated components and models
424         */
425        protected void initComponents() {
426            searchLabel = new JLabel();
427            searchField = new JTextField(getSearchFieldWidth()) {
428                @Override
429                public Dimension getMaximumSize() {
430                    Dimension superMax = super.getMaximumSize();
431                    superMax.height = getPreferredSize().height;
432                    return superMax;
433                }
434            };
435            matchCheck = new JCheckBox();
436        }
437    
438        /**
439         * @return width in characters of the search field
440         */
441        protected int getSearchFieldWidth() {
442            return 15;
443        }
444    }