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 }