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}