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